mirror of https://github.com/portainer/portainer
1405 lines
81 KiB
HTML
1405 lines
81 KiB
HTML
<page-header
|
|
ng-if="!ctrl.state.isEdit && !ctrl.stack.IsComposeFormat && ctrl.state.viewReady"
|
|
title="'Create application'"
|
|
breadcrumbs="[
|
|
{ label:'Applications', link:'kubernetes.applications' },
|
|
'Create an application'
|
|
]"
|
|
reload="true"
|
|
>
|
|
</page-header>
|
|
|
|
<page-header
|
|
ng-if="ctrl.state.isEdit && !ctrl.stack.IsComposeFormat && ctrl.state.viewReady"
|
|
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>
|
|
|
|
<page-header
|
|
ng-if="ctrl.stack.IsComposeFormat"
|
|
title="'View application'"
|
|
breadcrumbs="[
|
|
{ label:'Applications', link:'kubernetes.applications' },
|
|
'View application'
|
|
]"
|
|
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
|
|
read-only="ctrl.stack.IsComposeFormat"
|
|
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>
|
|
<div class="flex gap-1 text-muted small" ng-show="ctrl.stack.IsComposeFormat">
|
|
<pr-icon icon="'alert-circle'" mode="'warning'" class-name="'!mt-1'"></pr-icon>
|
|
<div>
|
|
<p>
|
|
Portainer no longer supports <a href="https://docs.docker.com/compose/compose-file/" target="_blank">docker-compose</a> format manifests for Kubernetes
|
|
deployments, and we have removed the <a href="https://kompose.io/" target="_blank">Kompose</a> 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
|
|
>
|
|
<p>
|
|
We advise installing your own instance of Kompose in a sandbox environment, performing conversions of your Docker Compose files to Kubernetes manifests and
|
|
using those manifests to set up applications.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<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"> </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> </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" ng-hide="ctrl.stack.IsComposeFormat"> Actions </div>
|
|
<!-- #region ACTIONS -->
|
|
<div class="form-group" ng-hide="ctrl.stack.IsComposeFormat">
|
|
<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>
|