portainer/app/kubernetes/views/applications/create/createApplication.html

1396 lines
81 KiB
HTML

<page-header
ng-if="!ctrl.state.isEdit"
title="'Create application'"
breadcrumbs="[
{ label:'Applications', link:'kubernetes.applications' },
'Create an application'
]"
reload="true"
>
</page-header>
<page-header
ng-if="ctrl.state.isEdit"
title="'Edit application'"
breadcrumbs="[
{ label:'Namespaces', link:'kubernetes.resourcePools' },
{
label:ctrl.application.ResourcePool,
link: 'kubernetes.resourcePools.resourcePool',
linkParams:{ id: ctrl.application.ResourcePool }
},
{ label:'Applications', link:'kubernetes.applications' },
{
label:ctrl.application.Name,
link: 'kubernetes.applications.application',
linkParams:{ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool }
},
'Edit',
]"
reload="true"
>
</page-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row kubernetes-create">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
<div ng-if="!ctrl.isExternalApplication()">
<git-form-info-panel
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
class-name="text-muted"
url="ctrl.stack.GitConfig.URL"
config-file-path="ctrl.stack.GitConfig.ConfigFilePath"
additional-files="ctrl.stack.AdditionalFiles"
type="application"
></git-form-info-panel>
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"> Namespace </div>
<!-- #region NAMESPACE -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
<div class="col-sm-8">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-nsSelect"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-danger">
<pr-icon icon="'alert-triangle'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-warning">
<pr-icon icon="'alert-triangle'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- #endregion -->
<!-- #region Git repository -->
<kubernetes-redeploy-app-git-form
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
stack="ctrl.stack"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kubernetes-redeploy-app-git-form>
<!-- #endregion -->
<!-- #region web editor -->
<web-editor-form
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT"
value="ctrl.stackFileContent"
yml="true"
identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here"
on-change="(ctrl.onChangeFileContent)"
>
<editor-description>
<span class="text-muted small" ng-show="ctrl.stack.IsComposeFormat">
<p class="vertical-center">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
<span>
Portainer uses <a href="https://kompose.io/" target="_blank">Kompose</a> to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that
not all the Compose format options are supported by Kompose at the moment.
</span>
</p>
<p>
You can get more information about Compose file format in the
<a href="https://docs.docker.com/compose/compose-file/" target="_blank">official documentation</a>.
</p>
<p
>In a forthcoming Portainer release, we plan to remove support for docker-compose format manifests for Kubernetes deployments, and the Kompose conversion tool
which enables this. The reason for this is because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).</p
>
<p
>Unfortunately, while the Kompose project has a maintainer and is part of the CNCF, it is not being actively maintained. Releases are very infrequent and new
pull requests to the project (including ones we've submitted) are taking months to be merged, with new CVEs arising in the meantime.</p
>
</span>
<span class="text-muted small" ng-show="!ctrl.stack.IsComposeFormat">
<p class="vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</span>
</editor-description>
</web-editor-form>
<!-- #endregion -->
<div ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
<div class="col-sm-12 form-section-title"> Application </div>
<!-- #region NAME FIELD -->
<div class="form-group">
<label for="application_name" class="col-sm-3 col-lg-2 control-label text-left required">Name</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
name="application_name"
ng-model="ctrl.formValues.Name"
ng-change="ctrl.onChangeName()"
placeholder="my-app"
ng-pattern="/^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/"
auto-focus
required
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-applicationName"
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesApplicationCreationForm.application_name.$invalid || ctrl.state.alreadyExists">
<div class="small">
<div class="col-sm-3 col-lg-2">&nbsp;</div>
<div class="col-sm-8" ng-messages="kubernetesApplicationCreationForm.application_name.$error">
<p class="text-warning vertical-center" ng-message="required"
><pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'"></pr-icon> This field is required.</p
>
<p class="text-warning vertical-center" ng-message="pattern">
<pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'"></pr-icon>
This field must consist of lower case alphanumeric characters or '-', contain at most 63 characters, start with an alphabetic character, and end with an
alphanumeric character (e.g. 'my-name', or 'abc-123').
</p>
</div>
<div class="col-sm-8" ng-if="ctrl.state.alreadyExists">
<p class="text-warning vertical-center">
<pr-icon class="vertical-center" icon="'alert-triangle'" mode="'warning'"></pr-icon>
An application with the same name already exists inside the selected namespace.
</p>
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region IMAGE FIELD -->
<div class="form-group mb-0">
<div class="col-sm-12">
<por-image-registry
model="ctrl.formValues.ImageModel"
ng-if="ctrl.formValues.ResourcePool"
auto-complete="false"
label-class="col-sm-3 col-lg-2"
input-class="col-sm-8"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
endpoint="ctrl.endpoint"
is-admin="ctrl.isAdmin"
check-rate-limits="true"
set-validity="ctrl.setPullImageValidity"
></por-image-registry>
</div>
</div>
<!-- #end region IMAGE FIELD -->
<div ng-if="ctrl.formValues.ResourcePool">
<div class="col-sm-12 form-section-title"> Stack </div>
<!-- #region STACK -->
<div class="form-group">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to
use the application name.
</div>
</div>
<div class="form-group">
<label for="stack_name" class="col-sm-3 col-lg-2 control-label text-left">Stack</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
placeholder="myStack"
ng-model="ctrl.formValues.StackName"
name="stack_name"
uib-typeahead="stack for stack in ctrl.stacks | filter:$viewValue | limitTo:7"
typeahead-min-length="0"
data-cy="k8sAppCreate-stackName"
/>
</div>
</div>
<!-- #endregion -->
<div class="col-sm-12 form-section-title"> Environment </div>
<!-- #region ENVIRONMENT VARIABLES -->
<div class="form-group">
<div class="col-sm-12 vertical-center pt-2.5">
<label class="control-label text-left !pt-0">Environment variables</label>
<span
ng-if="ctrl.formValues.Containers.length <= 1"
class="label label-default interactive vertical-center"
style="margin-left: 10px"
ng-click="ctrl.addEnvironmentVariable()"
data-cy="k8sAppCreate-addEnvVarButton"
>
<pr-icon icon="'plus'" mode="'alt'" size="'sm'"></pr-icon> add environment variable
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px">
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables | orderBy: 'NameIndex'" style="margin-top: 2px">
<div style="margin-top: 2px">
<div class="col-sm-4 input-group input-group-sm">
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon required">name</span>
<input
type="text"
name="environment_variable_name_{{ $index }}"
class="form-control"
ng-model="envVar.Name"
ng-change="ctrl.onChangeEnvironmentName()"
ng-pattern="/^[-._a-zA-Z][-._a-zA-Z0-9]*$/"
placeholder="foo"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-envVarName_{{ $index }}"
required
/>
</div>
</div>
<div class="col-sm-4 input-group input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon">value</span>
<input
type="text"
name="environment_variable_value_{{ $index }}"
class="form-control"
ng-model="envVar.Value"
placeholder="bar"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-envVarValue_{{ $index }}"
/>
</div>
<div class="col-sm-2 input-group input-group-sm" ng-if="ctrl.formValues.Containers.length <= 1">
<button
ng-if="!envVar.NeedsDeletion"
class="btn btn-md btn-dangerlight btn-only-icon !ml-0"
type="button"
ng-click="ctrl.removeEnvironmentVariable(envVar)"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
<button
ng-if="envVar.NeedsDeletion"
class="btn btn-sm btn-light btn-only-icon"
type="button"
ng-click="ctrl.restoreEnvironmentVariable(envVar)"
data-cy="k8sAppCreate-removeEnvVarButton_{{ $index }}"
>
<pr-icon icon="'rotate-cw'" size="'md'"></pr-icon>
</button>
</div>
</div>
<div
ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid ||
ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
"
>
<div class="col-sm-8 input-group input-group-sm">
<div
class="small"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid ||
ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required" class="text-warning vertical-center"
><pr-icon icon="'alert-triangle'" mode="'warning'" class-="vertical-center"></pr-icon> Environment variable name is required.</p
>
<p ng-message="pattern" class="text-warning vertical-center"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must
not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p
>
</ng-messages>
<p class="text-warning vertical-center" ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This environment variable is already defined.</p
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- #endregion -->
<div class="col-sm-12 form-section-title"> Configurations </div>
<!-- #region CONFIGURATIONS -->
<div class="form-group">
<div class="col-sm-12 vertical-center pt-2.5">
<label class="control-label text-left !pt-0">Configurations</label>
<span
class="label label-default interactive vertical-center"
style="margin-left: 10px"
ng-click="ctrl.addConfiguration()"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-addConfigButton"
>
<pr-icon icon="'plus'" mode="'alt'" size="'sm'"></pr-icon> add configuration
</span>
</div>
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.Configurations.length">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overridden to filesystem mounts for each
key via the override button.
</div>
</div>
<!-- config-element -->
<div class="form-group" ng-repeat="(index, config) in ctrl.formValues.Configurations">
<label for="stack_name" class="col-sm-3 col-lg-2 control-label text-left">Configuration</label>
<div class="col-sm-6">
<select
class="form-control"
ng-model="config.SelectedConfiguration"
ng-options="c as c.Name for c in ctrl.configurations track by c.Name"
ng-change="ctrl.resetConfiguration(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-addConfigSelect_{{ $index }}"
></select>
</div>
<div class="col-sm-3">
<button
class="btn btn-md btn-light vertical-center !ml-0"
type="button"
ng-if="!config.Overriden"
ng-click="ctrl.overrideConfiguration(index)"
ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configOverrideButton_{{ $index }}"
>
<pr-icon icon="'list'" size="'md'"></pr-icon> Override
</button>
<button
class="btn btn-md btn-light vertical-center !ml-0"
type="button"
ng-if="config.Overriden"
ng-click="ctrl.resetConfiguration(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configAutoButton_{{ $index }}"
>
<pr-icon icon="'rotate-cw'" size="'md'"></pr-icon> Auto
</button>
<button
class="btn btn-md btn-dangerlight vertical-center btn-only-icon h-[34px]"
type="button"
ng-click="ctrl.removeConfiguration(index)"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-configRemoveButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
</div>
<!-- no-override -->
<div class="col-sm-12" style="margin-top: 10px" ng-if="config.SelectedConfiguration && !config.Overriden">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-6 small text-muted" style="padding-left: 5px">
The following keys will be loaded from the <code>{{ config.SelectedConfiguration.Name }}</code> configuration as environment variables:
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
<code>{{ key }}</code
>{{ $last ? '' : ', ' }}
</span>
</div>
</div>
<!-- !no-override -->
<!-- has-override -->
<div class="col-sm-12 form-inline" style="margin-top: 10px" ng-if="config.Overriden">
<div ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px">
<div class="row">
<div class="col-sm-3 col-lg-2 form-group !m-0"><span>&nbsp;</span></div>
<div class="col-sm-3 form-group !mr-1" style="margin-left: -11px">
<div class="input-group input-group-sm">
<span class="input-group-addon">configuration key</span>
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
</div>
</div>
<div class="col-sm-3 form-group !mr-1" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div class="input-group input-group-sm">
<span class="input-group-addon required">path on disk</span>
<input
type="text"
class="form-control"
ng-model="overridenKey.Path"
placeholder="/etc/myapp/conf.d"
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeConfigurationPath()"
data-cy="k8sAppCreate-pathOnDiskInput"
/>
</div>
<span
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<div class="input-group input-group-sm text-warning" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div
class="small"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This path is already used.</p
>
</div>
</div>
</span>
</div>
<div class="col-sm-4 form-group">
<div class="input-group btn-group btn-group-sm">
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
<pr-icon icon="'list'"></pr-icon> Environment
</label>
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<pr-icon icon="'file-text'"></pr-icon> Filesystem
</label>
</div>
</div>
</div>
</div>
</div>
<!-- !has-override -->
</div>
<!-- !config-element -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title"> Persisting data </div>
<!-- #region PERSISTED FOLDERS -->
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
No storage option is available to persist data, contact your administrator to enable a storage option.
</div>
</div>
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
<div class="col-sm-12 vertical-center pt-2.5" style="margin-top: 5px" ng-if="!ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
<label class="control-label text-left !pt-0">Persisted folders</label>
<span
class="label label-default interactive vertical-center"
style="margin-left: 10px"
ng-click="ctrl.addPersistedFolder()"
ng-if="ctrl.isAddPersistentFolderButtonShowed()"
data-cy="k8sAppCreate-addPersistentFolderButton"
>
<pr-icon icon="'plus'" mode="'alt'" size="'sm'"></pr-icon> add persisted folder
</span>
</div>
<div class="col-sm-12" style="margin-top: 5px" ng-if="ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
<span class="small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
This namespace has exhausted its storage capacity. Contact your administrator to expand the capacity of the namespace.
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px" ng-repeat="persistedFolder in ctrl.formValues.PersistedFolders">
<div style="margin-top: 2px">
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
<span class="input-group-addon required">path in container</span>
<input
type="text"
class="form-control"
name="persisted_folder_path_{{ $index }}"
ng-model="persistedFolder.ContainerPath"
ng-change="ctrl.onChangePersistedFolderPath()"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
placeholder="/data"
required
data-cy="k8sAppCreate-containerPathInput_{{ $index }}"
/>
</div>
<div class="input-group col-sm-2 input-group-sm">
<span
class="btn-group btn-group-sm"
ng-class="{ striked: persistedFolder.NeedsDeletion }"
ng-if="
!ctrl.isEditAndExistingPersistedFolder($index) &&
ctrl.application.ApplicationType !== ctrl.ApplicationTypes.STATEFULSET &&
ctrl.formValues.Containers.length <= 1
"
>
<label
class="btn btn-light"
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="true"
ng-change="ctrl.useNewVolume($index)"
ng-disabled="ctrl.isNewVolumeButtonDisabled($index)"
>New volume</label
>
<label
class="btn btn-light"
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="false"
ng-change="ctrl.useExistingVolume($index)"
ng-disabled="ctrl.isExistingVolumeButtonDisabled()"
>Existing volume</label
>
</span>
</div>
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
<span class="input-group-addon required">requested size</span>
<input
type="number"
class="form-control !rounded-none"
name="persisted_folder_size_{{ $index }}"
ng-model="persistedFolder.Size"
placeholder="20"
min="0"
required
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
ng-change="ctrl.onChangeVolumeRequestedSize()"
/>
<span class="input-group-addon !p-0 !rounded-r-[5px]">
<select
class="form-control w-12 !h-[28px] !border-none !rounded-r-[5px] text-xs"
ng-model="persistedFolder.SizeUnit"
ng-style="{ height: '100%', cursor: ctrl.isEditAndExistingPersistedFolder($index) ? 'not-allowed' : 'auto' }"
ng-options="unit for unit in ctrl.state.availableSizeUnits"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
ng-change="ctrl.onChangeVolumeRequestedSize()"
></select>
</span>
</div>
<div class="input-group col-sm-2 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
<span class="input-group-addon">storage</span>
<select
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
class="form-control"
ng-model="persistedFolder.StorageClass"
ng-options="storageClass as storageClass.Name for storageClass in ctrl.storageClasses"
ng-disabled="ctrl.state.isEdit || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-storageSelect_{{ $index }}"
></select>
<input
ng-if="!ctrl.hasMultipleStorageClassesAvailable()"
type="text"
class="form-control"
disabled
ng-model="persistedFolder.StorageClass.Name"
data-cy="k8sAppCreate-storageClassNameInput_{{ $index }}"
/>
</div>
<div class="input-group col-sm-5 input-group-sm" ng-if="!persistedFolder.UseNewVolume" ng-class="{ striked: persistedFolder.NeedsDeletion }">
<span class="input-group-addon">volume</span>
<select
class="form-control"
name="existing_volumes_{{ $index }}"
ng-model="ctrl.formValues.PersistedFolders[$index].ExistingVolume"
ng-options="vol as vol.PersistentVolumeClaim.Name for vol in ctrl.availableVolumes"
ng-change="ctrl.onChangeExistingVolumeSelection()"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
required
>
<option selected disabled hidden value="">Select a volume</option>
</select>
</div>
<div class="input-group col-sm-1 input-group-sm">
<div ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
<button
ng-if="!persistedFolder.NeedsDeletion"
class="btn btn-sm btn-dangerlight !ml-0 h-[30px]"
type="button"
ng-click="ctrl.removePersistedFolder($index)"
data-cy="k8sAppCreate-rmPersistentFolderButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
<button
ng-if="persistedFolder.NeedsDeletion"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePersistedFolder($index)"
data-cy="k8sAppCreate-restorePersistentButton"
>
<pr-icon icon="'rotate-cw'"></pr-icon>
</button>
</div>
</div>
</div>
<div
class="flex flex-row gap-x-1"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined ||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined ||
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined
"
>
<div class="input-group col-sm-3 input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This path is already defined.</p
>
</div>
</div>
<div class="input-group col-sm-offset-2 col-sm-3 input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid || ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Size is required.</p>
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This value must be greater than zero.</p>
</ng-messages>
<p class="vertical-center" ng-if="ctrl.state.exceeded.persistedFolders.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
You can only request up to
{{ ctrl.state.storages.availabilities[persistedFolder.StorageClass.Name] | kubernetesAppStorageRequestSizeHumanReadable }} for
{{ persistedFolder.StorageClass.Name }}
</p>
</div>
<div
class="small text-warning"
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
>
<ng-messages for="kubernetesApplicationCreationForm['existing_volumes_' + $index].$error">
<p ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Volume is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This volume is already used.</p
>
</div>
</div>
<div class="input-group col-sm-1 input-group-sm"> </div>
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region DATA ACCESS POLICY -->
<div ng-if="ctrl.showDataAccessPolicySection()">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Data access policy</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 small text-muted"> Specify how the data will be used across instances. </div>
</div>
<!-- access policy options -->
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div
ng-if="
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)
"
>
<input
type="radio"
id="data_access_isolated"
ng-value="ctrl.ApplicationDataAccessPolicies.ISOLATED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_isolated">
<div class="boxselector_header">
<pr-icon icon="'boxes'"></pr-icon>
Isolated
</div>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div
style="color: #767676"
ng-if="
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes
"
>
<input type="radio" id="data_access_isolated" disabled />
<label
for="data_access_isolated"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
>
<div class="boxselector_header">
<pr-icon icon="'boxes'"></pr-icon>
Isolated
</div>
<p>Application will be deployed as a StatefulSet with each instantiating their own data</p>
</label>
</div>
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED)">
<input
type="radio"
id="data_access_shared"
ng-value="ctrl.ApplicationDataAccessPolicies.SHARED"
ng-model="ctrl.formValues.DataAccessPolicy"
ng-change="ctrl.resetDeploymentType()"
/>
<label for="data_access_shared">
<div class="boxselector_header">
<pr-icon icon="'box'"></pr-icon>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
<div style="color: #767676" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
<input type="radio" id="data_access_shared" disabled />
<label
for="data_access_shared"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the data access policy is not allowed"
style="cursor: pointer; border-color: #767676"
>
<div class="boxselector_header">
<pr-icon icon="'sliders'"></pr-icon>
Shared
</div>
<p>Application will be deployed as a Deployment with a shared storage access</p>
</label>
</div>
</div>
</div>
</div>
<!-- !access policy options -->
</div>
<!-- #endregion -->
<div class="col-sm-12 form-section-title"> Resource reservations </div>
<!-- #region RESOURCE RESERVATIONS -->
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
Resource reservations are applied per instance of the application.
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums
are inherited from the namespace quota.
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of
the namespace.
</div>
</div>
<!-- memory-limit-input -->
<div
class="form-group flex"
ng-if="
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
"
>
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left flex flex-row items-center">
Memory limit (MB)
<portainer-tooltip
message="'An instance of this application will reserve this amount of memory. If the instance memory usage exceeds the reservation, it might be subject to OOM.'"
>
</portainer-tooltip>
</label>
<div class="col-sm-6">
<slider model="ctrl.formValues.MemoryLimit" floor="ctrl.state.sliders.memory.min" ceil="ctrl.state.sliders.memory.max" step="128"></slider>
</div>
<div class="col-sm-2 vertical-center">
<input
name="memory_limit"
ng-model="ctrl.formValues.MemoryLimit"
type="number"
min="{{ ctrl.state.sliders.memory.min }}"
max="{{ ctrl.state.sliders.memory.max }}"
class="form-control"
id="memory-limit"
required
data-cy="k8sAppCreate-memoryLimit"
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesApplicationCreationForm.memory_limit.$invalid">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8 small text-warning">
<div ng-messages="kubernetesApplicationCreationForm.memory_limit.$error">
<p class="vertical-center"
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Value must be between {{ ctrl.state.sliders.memory.min }} and
{{ ctrl.state.sliders.memory.max }}
</p>
</div>
</div>
</div>
<!-- !memory-limit-input -->
<!-- cpu-limit-input -->
<div
class="form-group flex"
ng-if="
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
"
>
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left flex flex-row items-center">
CPU limit
<portainer-tooltip
message="'An instance of this application will reserve this amount of CPU. If the instance CPU usage exceeds the reservation, it might be subject to CPU throttling.'"
>
</portainer-tooltip>
</label>
<div class="col-sm-8">
<slider model="ctrl.formValues.CpuLimit" floor="ctrl.state.sliders.cpu.min" ceil="ctrl.state.sliders.cpu.max" step="0.10" precision="2"></slider>
</div>
</div>
<div class="form-group" ng-if="ctrl.nodeLimitsOverflow()">
<div class="col-sm-3 col-lg-2"></div>
<div class="col-sm-8 small text-muted">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
These reservations would exceed the resources currently available in the cluster.
</div>
</div>
<!-- !cpu-limit-input -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title"> Deployment </div>
<!-- #region DEPLOYMENT -->
<div class="form-group">
<div class="col-sm-12 small text-muted"> Select how you want to deploy your application inside the cluster. </div>
</div>
<!-- deployment options -->
<div class="form-group">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input
type="radio"
id="deployment_replicated"
ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED"
ng-model="ctrl.formValues.DeploymentType"
data-cy="k8sAppCreate-replicatedDeploymentButton"
/>
<label for="deployment_replicated">
<div class="boxselector_header">
<pr-icon icon="'sliders'"></pr-icon>
Replicated
</div>
<p>Run one or multiple instances of this container</p>
</label>
</div>
<div ng-if="!ctrl.supportGlobalDeployment()">
<input type="radio" id="deployment_global" disabled />
<label
for="deployment_global"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="The storage or access policy used for persisted folders cannot be used with this option"
>
<div class="boxselector_header">
<pr-icon icon="'boxes'"></pr-icon>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
<div ng-if="ctrl.supportGlobalDeployment()">
<input
type="radio"
id="deployment_global"
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-model="ctrl.formValues.DeploymentType"
ng-click="ctrl.unselectAutoScaler()"
data-cy="k8sAppCreate-globalDeployButton"
/>
<label for="deployment_global">
<div class="boxselector_header">
<pr-icon icon="'boxes'"></pr-icon>
Global
</div>
<p>Application will be deployed as a DaemonSet with an instance on each node of the cluster</p>
</label>
</div>
</div>
</div>
</div>
<!-- !deployment options -->
<!-- replica count -->
<div class="form-group" ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
<label for="replica_count" class="col-sm-1 control-label text-left">Instance count</label>
<div class="col-sm-2">
<input
type="number"
name="replica_count"
class="form-control"
min="1"
max="9999"
placeholder="1"
ng-model="ctrl.formValues.ReplicaCount"
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
ng-change="ctrl.enforceReplicaCountMinimum()"
required
data-cy="k8sAppCreate-replicaCountInput"
/>
<div class="help-block" ng-if="kubernetesApplicationCreationForm['replica_count'].$invalid">
<div class="small text-warning whitespace-nowrap">
<ng-messages for="kubernetesApplicationCreationForm['replica_count'].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Instance count is required.</p>
<p class="vertical-center" ng-message="min"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Instance count must be greater than 0.</p>
</ng-messages>
</div>
</div>
</div>
</div>
<!-- !replica count -->
<div
class="form-group"
ng-if="!ctrl.resourceReservationsOverflow() && ctrl.formValues.ReplicaCount > 1 && (ctrl.formValues.CpuLimit !== 0 || ctrl.formValues.MemoryLimit !== 0)"
>
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<div>
This application will reserve the following resources:
<b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
<b>{{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB</b> of memory.
</div>
</div>
</div>
<div class="form-group" ng-if="ctrl.resourceReservationsOverflow()">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This application would exceed available resources. Please review resource reservations or the instance count.
</div>
</div>
<div class="form-group" ng-if="ctrl.state.storages.quotaExceeded">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
This application would exceed available storage. Please review the persisted folders or the instance count.
</div>
</div>
<div class="form-group" ng-if="!ctrl.supportScalableReplicaDeployment()">
<div class="col-sm-12 small text-muted vertical-center">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
<div>
The following storage option(s) do not support concurrent access from multiples instances: <code>{{ ctrl.getNonScalableStorage() }}</code
>. You will not be able to scale that application.
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region AUTO SCALING -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL"> Auto-scaling </div>
<div class="form-group" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.state.useServerMetrics">
<div class="col-sm-12">
<div class="col-sm-3 col-lg-2 pl-0 pt-0">
<label for="enable_auto_scaling" class="control-label text-left"> Enable auto scaling for this application </label>
</div>
<label class="switch ml-4 mt-1">
<input
type="checkbox"
class="form-control"
name="enable_auto_scaling"
ng-model="ctrl.formValues.AutoScaler.IsUsed"
data-cy="k8sAppCreate-autoScaleCheckbox"
/>
<span class="slider round"></span>
</label>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && !ctrl.state.useServerMetrics">
<div class="col-sm-12 small text-muted">
<p ng-if="!ctrl.isAdmin"> This feature is currently disabled and must be enabled by an administrator user. </p>
<p ng-if="ctrl.isAdmin">
Server metrics features must be enabled in the
<a ui-sref="kubernetes.cluster.setup" class="ctrl.isAdmin">environment configuration view</a>.
</p>
</div>
</div>
<div class="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed">
<div class="row">
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_min">Minimum instances</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_min"
min="0"
ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
ng-model="ctrl.formValues.AutoScaler.MinReplicas"
data-cy="k8sAppCreate-autoScaleMin"
required
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error">
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances is required. </p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances must be greater than 0.
</p>
<p ng-message="max" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Minimum instances must be smaller than maximum instances.
</p>
</ng-messages>
</div>
</span>
</div>
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_max">Maximum instances</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_max"
ng-min="ctrl.formValues.AutoScaler.MinReplicas"
ng-model="ctrl.formValues.AutoScaler.MaxReplicas"
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error">
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Maximum instances is required. </p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Maximum instances must be greater than minimum instances.
</p>
</ng-messages>
</div>
</span>
</div>
<div class="col-sm-4 pl-0">
<label class="control-label text-left pb-2" for="auto_scaler_cpu">
Target CPU usage (<b>%</b>)
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
</portainer-tooltip>
</label>
<div class="input-group input-group-sm" style="width: 100%">
<input
type="number"
class="form-control"
name="auto_scaler_cpu"
ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization"
min="1"
max="100"
required
data-cy="k8sAppCreate-targetCPUInput"
/>
</div>
<span ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<div class="small text-warning" style="margin-top: 5px">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error">
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage is required. </p>
<p ng-message="min" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage must be greater than 0.
</p>
<p ng-message="max" class="vertical-center">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Target CPU usage must be smaller than 100.
</p>
</ng-messages>
</div>
</span>
</div>
</div>
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px">
<div class="col-sm-12 small text-danger">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
</div>
</div>
</div>
<!-- #endregion -->
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
<div class="col-sm-12 form-section-title"> Placement preferences and constraints </div>
<!-- #region PLACEMENTS -->
<div class="form-group">
<div class="col-sm-12 vertical-center pt-2.5">
<label class="control-label text-left !pt-0">Placement rules</label>
<span class="label label-default interactive vertical-center" style="margin-left: 10px" ng-click="ctrl.addPlacement()">
<pr-icon icon="'plus'" mode="'alt'" size="'sm'"></pr-icon> add rule
</span>
</div>
<div class="col-sm-12 small text-muted vertical-center" ng-if="ctrl.formValues.Placements.length > 0" style="margin-top: 10px">
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
<div> Deploy this application on nodes that respect <b>ALL</b> of the following placement rules. Placement rules are based on node labels. </div>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px">
<div ng-repeat-start="placement in ctrl.formValues.Placements" style="margin-top: 2px">
<div class="col-sm-5 input-group" ng-class="{ striked: placement.NeedsDeletion }">
<select
class="form-control !rounded"
ng-model="placement.Label"
ng-options="label as (label.Key | kubernetesNodeLabelHumanReadbleText) for label in ctrl.nodesLabels"
ng-change="ctrl.onChangePlacementLabel($index)"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
data-cy="k8sAppCreate-placementLabel_{{ $index }}"
>
</select>
</div>
<div class="col-sm-5 input-group" ng-class="{ striked: placement.NeedsDeletion }">
<select
class="form-control !rounded"
ng-model="placement.Value"
ng-options="value for value in placement.Label.Values"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
data-cy="k8sAppCreate-placementName_{{ $index }}"
>
</select>
</div>
<div class="col-sm-1 input-group">
<button
ng-if="!placement.NeedsDeletion"
class="btn btn-md btn-dangerlight btn-only-icon !ml-0"
type="button"
ng-click="ctrl.removePlacement($index)"
data-cy="k8sAppCreate-deletePlacementButton"
>
<pr-icon icon="'trash-2'" size="'md'"></pr-icon>
</button>
<button
ng-if="placement.NeedsDeletion"
class="btn btn-sm btn-light btn-only-icon !ml-0"
type="button"
ng-click="ctrl.restorePlacement($index)"
data-cy="k8sAppCreate-restorePlacementButton"
>
<pr-icon icon="'rotate-cw'" size="'md'"></pr-icon>
</button>
</div>
</div>
<div ng-repeat-end ng-show="ctrl.state.duplicates.placements.refs[$index] !== undefined">
<div class="col-sm-5 input-group">
<div class="small text-warning" style="margin-top: 5px" ng-if="ctrl.state.duplicates.placements.refs[$index] !== undefined">
<p class="vertical-center" ng-if="ctrl.state.duplicates.placements.refs[$index] !== undefined">
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This label is already defined.
</p>
</div>
</div>
</div>
</div>
</div>
<div ng-if="ctrl.showPlacementPolicySection()">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Placement policy</label>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 small text-muted"> Specify the policy associated to the placement rules. </div>
</div>
<!-- placement policy options -->
<div class="form-group" ng-if="ctrl.formValues.Placements.length">
<div class="col-sm-12">
<div class="boxselector_wrapper">
<div>
<input
type="radio"
id="placement_hard"
ng-value="ctrl.ApplicationPlacementTypes.MANDATORY"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-mandatoryPlacementButton"
/>
<label for="placement_hard">
<div class="boxselector_header">
<pr-icon icon="'sliders'"></pr-icon>
Mandatory
</div>
<p>Schedule this application <b>ONLY</b> on nodes that match <b>ALL</b> Rules</p>
</label>
</div>
<div>
<input
type="radio"
id="placement_soft"
ng-value="ctrl.ApplicationPlacementTypes.PREFERRED"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-prefferedPlacementButton"
/>
<label for="placement_soft">
<div class="boxselector_header">
<pr-icon icon="'align-justify'"></pr-icon>
Preferred
</div>
<p>Schedule this application on nodes that match the rules if possible</p>
</label>
</div>
</div>
</div>
</div>
<!-- !placement policy options -->
</div>
<!-- #endregion -->
</div>
<!-- kubernetes services options -->
<kube-services-view
form-values="ctrl.formValues"
is-edit="ctrl.state.isEdit"
namespaces="ctrl.allNamespaces"
loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
></kube-services-view>
<!-- kubernetes services options -->
<!-- summary -->
<kubernetes-summary-view
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view>
</div>
</div>
</div>
<div ng-if="ctrl.isExternalApplication()">
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"> Namespace </div>
<!-- #region NAMESPACE -->
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Namespace</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-nsSelect"
></select>
</div>
</div>
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-danger">
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the
namespace.
</div>
</div>
<div class="form-group" ng-if="!ctrl.formValues.ResourcePool">
<div class="col-sm-12 small text-muted">
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
You do not have access to any namespace. Contact your administrator to get access to a namespace.
</div>
</div>
<!-- kubernetes services options -->
<kube-services-view
namespaces="ctrl.allNamespaces"
form-values="ctrl.formValues"
is-edit="ctrl.state.isEdit"
loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
></kube-services-view>
<!-- kubernetes services options -->
</div>
<!-- kubernetes summary for external application -->
<kubernetes-summary-view ng-if="ctrl.isExternalApplication()" form-values="ctrl.formValues" old-form-values="ctrl.savedFormValues"></kubernetes-summary-view>
<!-- kubernetes summary for external application -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT"> Actions </div>
<!-- #region ACTIONS -->
<div class="form-group">
<div class="col-sm-12">
<button
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors()"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>
<span ng-show="ctrl.state.isEdit && !ctrl.state.actionInProgress">Update application</span>
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress && ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppCreate-appCancelButton"
>
Cancel
</button>
<!-- #Web editor buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="ctrl.updateApplicationViaWebEditor()"
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
style="margin-top: 7px; margin-left: 0"
button-spinner="ctrl.state.updateWebEditorInProgress"
>
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
</button>
</div>
</div>
<!-- #endregion -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>