mirror of https://github.com/portainer/portainer
feat(kuberenetes): add annotations to kube objects EE-4089 (#8499)
* add annotations BE teaser * fix settings icon click on home screen for kube env * add debouce to namespace validation * ingress button tooltip fixed * fix tooltip textpull/8504/head
parent
5f66020e42
commit
defce0cf6d
|
@ -1,7 +1,7 @@
|
||||||
<!-- use registry -->
|
<!-- use registry -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group" ng-if="$ctrl.model.UseRegistry">
|
<div class="form-group" ng-if="$ctrl.model.UseRegistry">
|
||||||
<label for="image_registry" class="control-label col-sm-3 col-lg-2 text-left" ng-class="$ctrl.labelClass"> Registry </label>
|
<label for="image_registry" class="control-label col-sm-3 col-lg-2 required text-left" ng-class="$ctrl.labelClass"> Registry </label>
|
||||||
<div ng-class="$ctrl.inputClass" class="col-sm-8">
|
<div ng-class="$ctrl.inputClass" class="col-sm-8">
|
||||||
<select
|
<select
|
||||||
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
|
||||||
|
|
|
@ -20,9 +20,6 @@
|
||||||
>
|
>
|
||||||
<div ng-show="!$ctrl.multiItemDisable" class="vertical-center mt-5 mb-5">
|
<div ng-show="!$ctrl.multiItemDisable" class="vertical-center mt-5 mb-5">
|
||||||
<label class="control-label !pt-0 text-left">Published ports</label>
|
<label class="control-label !pt-0 text-left">Published ports</label>
|
||||||
<span class="label label-default interactive vertical-center ml-2.5" ng-click="$ctrl.addPort()" data-cy="k8sAppCreate-addNewPortButton">
|
|
||||||
<pr-icon icon="'plus'" mode="'alt'" size="'sm'"></pr-icon> publish a new port
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div ng-repeat="servicePort in $ctrl.service.Ports" class="service-form row mt-5">
|
<div ng-repeat="servicePort in $ctrl.service.Ports" class="service-form row mt-5">
|
||||||
<div class="form-group col-sm-3 !mx-0 !pl-0">
|
<div class="form-group col-sm-3 !mx-0 !pl-0">
|
||||||
|
@ -182,5 +179,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0" ng-click="$ctrl.addPort()" data-cy="k8sAppCreate-addNewPortButton">
|
||||||
|
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Publish a new port
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-form>
|
</ng-form>
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
|
<form class="form-horizontal mt-4" name="kubernetesApplicationCreationForm" autocomplete="off">
|
||||||
<div ng-if="!ctrl.isExternalApplication()">
|
<div ng-if="!ctrl.isExternalApplication()">
|
||||||
<git-form-info-panel
|
<git-form-info-panel
|
||||||
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
|
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
|
||||||
|
@ -69,7 +69,6 @@
|
||||||
additional-files="ctrl.stack.AdditionalFiles"
|
additional-files="ctrl.stack.AdditionalFiles"
|
||||||
type="'application'"
|
type="'application'"
|
||||||
></git-form-info-panel>
|
></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 -->
|
<!-- #region NAMESPACE -->
|
||||||
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
|
<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>
|
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label text-left">Namespace</label>
|
||||||
|
@ -124,8 +123,9 @@
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Portainer no longer supports <a href="https://docs.docker.com/compose/compose-file/" target="_blank">docker-compose</a> format manifests for Kubernetes
|
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
|
deployments, and we have removed the <a href="https://kompose.io/" target="_blank">Kompose</a>
|
||||||
because Kompose now poses a security risk, since it has a number of Common Vulnerabilities and Exposures (CVEs).
|
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>
|
||||||
<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
|
>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
|
||||||
|
@ -151,7 +151,6 @@
|
||||||
</web-editor-form>
|
</web-editor-form>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
<div ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
<div ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
|
||||||
<div class="col-sm-12 form-section-title"> Application </div>
|
|
||||||
<!-- #region NAME FIELD -->
|
<!-- #region NAME FIELD -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="application_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
|
<label for="application_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
|
||||||
|
@ -195,7 +194,7 @@
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region IMAGE FIELD -->
|
<!-- #region IMAGE FIELD -->
|
||||||
<div class="form-group mb-0">
|
<div class="form-group mb-2">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-image-registry
|
<por-image-registry
|
||||||
model="ctrl.formValues.ImageModel"
|
model="ctrl.formValues.ImageModel"
|
||||||
|
@ -213,8 +212,11 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #end region IMAGE FIELD -->
|
<!-- #end region IMAGE FIELD -->
|
||||||
|
|
||||||
|
<div class="col-sm-12 !p-0">
|
||||||
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.formValues.ResourcePool">
|
<div ng-if="ctrl.formValues.ResourcePool">
|
||||||
<div class="col-sm-12 form-section-title"> Stack </div>
|
|
||||||
<!-- #region STACK -->
|
<!-- #region STACK -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
<div class="col-sm-12 small text-muted vertical-center">
|
||||||
|
@ -241,26 +243,16 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Environment </div>
|
|
||||||
<!-- #region ENVIRONMENT VARIABLES -->
|
<!-- #region ENVIRONMENT VARIABLES -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 vertical-center pt-2.5">
|
<div class="col-sm-12 vertical-center">
|
||||||
<label class="control-label !pt-0 text-left">Environment variables</label>
|
<label class="control-label !pt-0 text-left">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>
|
||||||
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
<div class="col-sm-12 form-inline mt-2">
|
||||||
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables | orderBy: 'NameIndex'" style="margin-top: 2px">
|
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables | orderBy: 'NameIndex'" class="mt-2">
|
||||||
<div style="margin-top: 2px">
|
<div style="margin-top: 2px">
|
||||||
<div class="col-sm-4 input-group input-group-sm">
|
<div class="col-sm-4 input-group input-group-sm mr-2">
|
||||||
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||||
<span class="input-group-addon required">name</span>
|
<span class="input-group-addon required">name</span>
|
||||||
<input
|
<input
|
||||||
|
@ -278,7 +270,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4 input-group input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
|
<div class="col-sm-4 input-group input-group-sm mr-2" ng-class="{ striked: envVar.NeedsDeletion }">
|
||||||
<span class="input-group-addon">value</span>
|
<span class="input-group-addon">value</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -343,67 +335,73 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-4">
|
||||||
|
<span
|
||||||
|
ng-if="ctrl.formValues.Containers.length <= 1"
|
||||||
|
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
||||||
|
ng-click="ctrl.addEnvironmentVariable()"
|
||||||
|
data-cy="k8sAppCreate-addEnvVarButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add environment variable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Configurations </div>
|
|
||||||
<!-- #region CONFIGURATIONS -->
|
<!-- #region CONFIGURATIONS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 vertical-center pt-2.5">
|
<div class="col-sm-12 vertical-center">
|
||||||
<label class="control-label !pt-0 text-left">Configurations</label>
|
<label class="control-label !pt-0 text-left">ConfigMap or Secret</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>
|
||||||
<div class="col-sm-12 small text-muted vertical-center" style="margin-top: 15px" ng-if="ctrl.formValues.Configurations.length">
|
<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>
|
<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
|
Portainer will automatically expose all the keys of a ConfigMap or Secret as environment variables. This behavior can be overridden to filesystem mounts for
|
||||||
key via the override button.
|
each key via the override option.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- config-element -->
|
<!-- config-element -->
|
||||||
<div class="form-group" ng-repeat="(index, config) in ctrl.formValues.Configurations">
|
<div class="form-inline clearfix" 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-12 !p-0">
|
||||||
<div class="col-sm-6">
|
<div class="input-group input-group-sm !mr-1">
|
||||||
<select
|
<span class="input-group-addon">name</span>
|
||||||
class="form-control"
|
<select
|
||||||
ng-model="config.SelectedConfiguration"
|
class="form-control col-sm-6"
|
||||||
ng-options="c as c.Name for c in ctrl.configurations track by c.Name"
|
ng-model="config.SelectedConfiguration"
|
||||||
ng-change="ctrl.resetConfiguration(index)"
|
ng-options="c as c.Name for c in ctrl.configurations track by c.Name"
|
||||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
ng-change="ctrl.resetConfiguration(index)"
|
||||||
data-cy="k8sAppCreate-addConfigSelect_{{ $index }}"
|
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||||
></select>
|
data-cy="k8sAppCreate-addConfigSelect_{{ $index }}"
|
||||||
</div>
|
></select>
|
||||||
<div class="col-sm-3">
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group btn-group btn-group-sm">
|
||||||
|
<label
|
||||||
|
class="btn btn-md btn-light vertical-center !ml-0"
|
||||||
|
type="button"
|
||||||
|
ng-click="ctrl.resetConfiguration(index)"
|
||||||
|
ng-disabled="ctrl.formValues.Containers.length > 1"
|
||||||
|
data-cy="k8sAppCreate-configAutoButton_{{ $index }}"
|
||||||
|
uib-btn-radio="false"
|
||||||
|
ng-model="config.Overriden"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'rotate-cw'" size="'md'"></pr-icon> Auto
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="btn btn-md btn-light vertical-center !ml-0"
|
||||||
|
ng-click="ctrl.overrideConfiguration(index)"
|
||||||
|
ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1"
|
||||||
|
data-cy="k8sAppCreate-configOverrideButton_{{ $index }}"
|
||||||
|
uib-btn-radio="true"
|
||||||
|
ng-model="config.Overriden"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'list'" size="'md'"></pr-icon> Override
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-md btn-light vertical-center !ml-0"
|
class="btn btn-md btn-dangerlight btn-only-icon vertical-center"
|
||||||
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"
|
type="button"
|
||||||
ng-click="ctrl.removeConfiguration(index)"
|
ng-click="ctrl.removeConfiguration(index)"
|
||||||
ng-if="ctrl.formValues.Containers.length <= 1"
|
ng-if="ctrl.formValues.Containers.length <= 1"
|
||||||
|
@ -413,10 +411,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- no-override -->
|
<!-- no-override -->
|
||||||
<div class="col-sm-12" style="margin-top: 10px" ng-if="config.SelectedConfiguration && !config.Overriden">
|
<div class="row clearfix" ng-if="config.SelectedConfiguration && !config.Overriden">
|
||||||
<div class="col-sm-3 col-lg-2"></div>
|
<div class="col-sm-9 small text-muted !mt-2 !p-0">
|
||||||
<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>
|
||||||
The following keys will be loaded from the <code>{{ config.SelectedConfiguration.Name }}</code> configuration as environment variables:
|
configuration as environment variables:
|
||||||
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
|
<span ng-repeat="(key, _) in config.SelectedConfiguration.Data">
|
||||||
<code>{{ key }}</code
|
<code>{{ key }}</code
|
||||||
>{{ $last ? '' : ', ' }}
|
>{{ $last ? '' : ', ' }}
|
||||||
|
@ -426,66 +424,56 @@
|
||||||
<!-- !no-override -->
|
<!-- !no-override -->
|
||||||
|
|
||||||
<!-- has-override -->
|
<!-- has-override -->
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px" ng-if="config.Overriden">
|
<div class="col-sm-12 !mt-2 !mb-4 !p-0" ng-if="config.Overriden" ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px">
|
||||||
<div ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px">
|
<div class="input-group input-group-sm !mr-1">
|
||||||
<div class="row">
|
<span class="input-group-addon">key</span>
|
||||||
<div class="col-sm-3 col-lg-2 form-group !m-0"><span> </span></div>
|
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
|
||||||
<div class="col-sm-3 form-group !mr-1" style="margin-left: -11px">
|
</div>
|
||||||
<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 btn-group btn-group-sm !mr-1">
|
||||||
<div class="input-group input-group-sm">
|
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
|
||||||
<span class="input-group-addon required">path on disk</span>
|
<pr-icon icon="'list'"></pr-icon> Environment
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<label class="btn btn-light" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||||
class="form-control"
|
<pr-icon icon="'file-text'"></pr-icon> Filesystem
|
||||||
ng-model="overridenKey.Path"
|
</label>
|
||||||
placeholder="/etc/myapp/conf.d"
|
</div>
|
||||||
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
|
|
||||||
ng-disabled="ctrl.formValues.Containers.length > 1"
|
<div class="form-group !ml-0 !mr-0 !align-top" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||||
required
|
<div class="input-group input-group-sm">
|
||||||
ng-change="ctrl.onChangeConfigurationPath()"
|
<span class="input-group-addon required">path on disk</span>
|
||||||
data-cy="k8sAppCreate-pathOnDiskInput"
|
<input
|
||||||
/>
|
type="text"
|
||||||
</div>
|
class="form-control"
|
||||||
<span
|
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>
|
||||||
|
<div
|
||||||
|
class="small"
|
||||||
|
ng-show="
|
||||||
|
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||||
|
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="text-warning" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
||||||
|
<div
|
||||||
ng-show="
|
ng-show="
|
||||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="input-group input-group-sm text-warning" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
|
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
||||||
<div
|
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Path is required.</p>
|
||||||
class="small"
|
</ng-messages>
|
||||||
style="margin-top: 5px"
|
<p class="vertical-center" ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined">
|
||||||
ng-show="
|
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This path is already used.
|
||||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
</p>
|
||||||
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>
|
</div>
|
||||||
|
@ -494,9 +482,18 @@
|
||||||
<!-- !has-override -->
|
<!-- !has-override -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !config-element -->
|
<!-- !config-element -->
|
||||||
|
<div class="col-sm-12 !p-0">
|
||||||
|
<span
|
||||||
|
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
||||||
|
ng-click="ctrl.addConfiguration()"
|
||||||
|
ng-if="ctrl.formValues.Containers.length <= 1"
|
||||||
|
data-cy="k8sAppCreate-addConfigButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add ConfigMap and Secret
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Persisting data </div>
|
|
||||||
<!-- #region PERSISTED FOLDERS -->
|
<!-- #region PERSISTED FOLDERS -->
|
||||||
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
<div class="col-sm-12 small text-muted vertical-center">
|
||||||
|
@ -508,15 +505,6 @@
|
||||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
<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()">
|
<div class="col-sm-12 vertical-center pt-2.5" style="margin-top: 5px" ng-if="!ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
|
||||||
<label class="control-label !pt-0 text-left">Persisted folders</label>
|
<label class="control-label !pt-0 text-left">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>
|
||||||
|
|
||||||
<div class="col-sm-12" style="margin-top: 5px" ng-if="ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
|
<div class="col-sm-12" style="margin-top: 5px" ng-if="ctrl.allQuotasExhaustedAndNoVolumesAvailable()">
|
||||||
|
@ -543,16 +531,15 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-2 input-group-sm">
|
<div
|
||||||
<span
|
class="input-group col-sm-2 input-group-sm"
|
||||||
class="btn-group btn-group-sm"
|
ng-if="
|
||||||
ng-class="{ striked: persistedFolder.NeedsDeletion }"
|
!ctrl.isEditAndExistingPersistedFolder($index) &&
|
||||||
ng-if="
|
ctrl.application.ApplicationType !== ctrl.ApplicationTypes.STATEFULSET &&
|
||||||
!ctrl.isEditAndExistingPersistedFolder($index) &&
|
ctrl.formValues.Containers.length <= 1
|
||||||
ctrl.application.ApplicationType !== ctrl.ApplicationTypes.STATEFULSET &&
|
"
|
||||||
ctrl.formValues.Containers.length <= 1
|
>
|
||||||
"
|
<span class="btn-group btn-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||||
>
|
|
||||||
<label
|
<label
|
||||||
class="btn btn-light"
|
class="btn btn-light"
|
||||||
ng-model="persistedFolder.UseNewVolume"
|
ng-model="persistedFolder.UseNewVolume"
|
||||||
|
@ -684,7 +671,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-offset-2 col-sm-3 input-group-sm">
|
<div class="input-group col-sm-offset-3 col-sm-3 input-group-sm">
|
||||||
<div
|
<div
|
||||||
class="small text-warning"
|
class="small text-warning"
|
||||||
style="margin-top: 5px"
|
style="margin-top: 5px"
|
||||||
|
@ -719,6 +706,17 @@
|
||||||
<div class="input-group col-sm-1 input-group-sm"> </div>
|
<div class="input-group col-sm-1 input-group-sm"> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-2">
|
||||||
|
<span
|
||||||
|
class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0"
|
||||||
|
ng-click="ctrl.addPersistedFolder()"
|
||||||
|
ng-if="ctrl.isAddPersistentFolderButtonShowed()"
|
||||||
|
data-cy="k8sAppCreate-addPersistentFolderButton"
|
||||||
|
>
|
||||||
|
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add persisted folder
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
@ -856,7 +854,7 @@
|
||||||
|
|
||||||
<!-- replica count -->
|
<!-- replica count -->
|
||||||
<div class="form-group" ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
<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>
|
<label for="replica_count" class="col-sm-3 col-lg-2 control-label required text-left">Instance count </label>
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -915,7 +913,8 @@
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
<div class="col-sm-12 small text-muted vertical-center">
|
||||||
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
<pr-icon icon="'alert-circle'" mode="'warning'"></pr-icon>
|
||||||
<div>
|
<div>
|
||||||
The following storage option(s) do not support concurrent access from multiples instances: <code>{{ ctrl.getNonScalableStorage() }}</code
|
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.
|
>. You will not be able to scale that application.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -923,9 +922,18 @@
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- #region AUTO SCALING -->
|
<!-- #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="form-group !mb-0" 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-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<div class="col-sm-3 col-lg-2 pl-0 pt-0">
|
<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>
|
<label for="enable_auto_scaling" class="control-label text-left"> Enable auto scaling for this application </label>
|
||||||
|
@ -937,22 +945,13 @@
|
||||||
name="enable_auto_scaling"
|
name="enable_auto_scaling"
|
||||||
ng-model="ctrl.formValues.AutoScaler.IsUsed"
|
ng-model="ctrl.formValues.AutoScaler.IsUsed"
|
||||||
data-cy="k8sAppCreate-autoScaleCheckbox"
|
data-cy="k8sAppCreate-autoScaleCheckbox"
|
||||||
|
ng-disabled="!(ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.state.useServerMetrics)"
|
||||||
/>
|
/>
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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="form-inline" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.formValues.AutoScaler.IsUsed">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4 pl-0">
|
<div class="col-sm-4 pl-0">
|
||||||
|
@ -1048,118 +1047,117 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
<div class="mt-4 mb-2" ng-if="ctrl.formValues.DeploymentType === ctrl.ApplicationDeploymentTypes.REPLICATED">
|
||||||
<div class="col-sm-12 form-section-title"> Placement preferences and constraints </div>
|
<div class="col-sm-12 control-label !mb-2 !p-0 text-left"> Placement preferences and constraints </div>
|
||||||
|
|
||||||
<!-- #region PLACEMENTS -->
|
<!-- #region PLACEMENTS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 vertical-center pt-2.5">
|
<div class="col-sm-12 small text-muted vertical-center !mb-2" ng-if="ctrl.formValues.Placements.length > 0">
|
||||||
<label class="control-label !pt-0 text-left">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>
|
<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> 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>
|
||||||
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
<div class="col-sm-12 form-inline">
|
||||||
<div ng-repeat-start="placement in ctrl.formValues.Placements" style="margin-top: 2px">
|
<div ng-repeat-start="placement in ctrl.formValues.Placements" class="!mb-2">
|
||||||
<div class="col-sm-5 input-group" ng-class="{ striked: placement.NeedsDeletion }">
|
<div class="col-sm-5 input-group mr-2 ng-class=" { striked: placement.NeedsDeletion }">
|
||||||
<select
|
<select
|
||||||
class="form-control !rounded"
|
class="form-control !rounded"
|
||||||
ng-model="placement.Label"
|
ng-model="placement.Label"
|
||||||
ng-options="label as (label.Key | kubernetesNodeLabelHumanReadbleText) for label in ctrl.nodesLabels"
|
ng-options="label as (label.Key | kubernetesNodeLabelHumanReadbleText) for label in ctrl.nodesLabels"
|
||||||
ng-change="ctrl.onChangePlacementLabel($index)"
|
ng-change="ctrl.onChangePlacementLabel($index)"
|
||||||
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
|
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
|
||||||
data-cy="k8sAppCreate-placementLabel_{{ $index }}"
|
data-cy="k8sAppCreate-placementLabel_{{ $index }}"
|
||||||
>
|
>
|
||||||
</select>
|
</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>
|
||||||
<div ng-repeat-end ng-show="ctrl.state.duplicates.placements.refs[$index] !== undefined">
|
<div class="col-sm-5 input-group mr-2" ng-class="{ striked: placement.NeedsDeletion }">
|
||||||
<div class="col-sm-5 input-group">
|
<select
|
||||||
<div class="small text-warning" style="margin-top: 5px" ng-if="ctrl.state.duplicates.placements.refs[$index] !== undefined">
|
class="form-control !rounded"
|
||||||
<p class="vertical-center" ng-if="ctrl.state.duplicates.placements.refs[$index] !== undefined">
|
ng-model="placement.Value"
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> This label is already defined.
|
ng-options="value for value in placement.Label.Values"
|
||||||
</p>
|
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
|
||||||
</div>
|
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" 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>
|
||||||
</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">
|
||||||
<div class="col-sm-12 small text-muted"> Specify the policy associated to the placement rules. </div>
|
<span class="btn btn-primary btn-sm btn btn-sm btn-light mb-2 !ml-0 mt-2" ng-click="ctrl.addPlacement()">
|
||||||
</div>
|
<pr-icon icon="'plus'" size="'sm'"></pr-icon> Add rule
|
||||||
|
</span>
|
||||||
<box-selector
|
|
||||||
ng-if="ctrl.formValues.Placements.length"
|
|
||||||
options="ctrl.placementOptions"
|
|
||||||
slim="true"
|
|
||||||
value="ctrl.formValues.PlacementType"
|
|
||||||
on-change="(ctrl.onChangePlacementType)"
|
|
||||||
radio-name="'placementType'"
|
|
||||||
></box-selector>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
|
||||||
</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>
|
||||||
|
|
||||||
<!-- kubernetes services options -->
|
<div class="form-group">
|
||||||
<kube-services-view
|
<div class="col-sm-12 small text-muted"> Specify the policy associated to the placement rules. </div>
|
||||||
form-values="ctrl.formValues"
|
</div>
|
||||||
is-edit="ctrl.state.isEdit"
|
|
||||||
namespaces="ctrl.allNamespaces"
|
|
||||||
loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
|
||||||
></kube-services-view>
|
|
||||||
<!-- kubernetes services options -->
|
|
||||||
|
|
||||||
<!-- summary -->
|
<box-selector
|
||||||
<kubernetes-summary-view
|
ng-if="ctrl.formValues.Placements.length"
|
||||||
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
|
options="ctrl.placementOptions"
|
||||||
form-values="ctrl.formValues"
|
slim="true"
|
||||||
old-form-values="ctrl.savedFormValues"
|
value="ctrl.formValues.PlacementType"
|
||||||
></kubernetes-summary-view>
|
on-change="(ctrl.onChangePlacementType)"
|
||||||
|
radio-name="'placementType'"
|
||||||
|
></box-selector>
|
||||||
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- kubernetes services options -->
|
||||||
|
<kube-services-view
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
is-edit="ctrl.state.isEdit"
|
||||||
|
namespaces="ctrl.allNamespaces"
|
||||||
|
configurations="ctrl.configurations"
|
||||||
|
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>
|
||||||
<div ng-if="ctrl.isExternalApplication()">
|
<div ng-if="ctrl.isExternalApplication()">
|
||||||
|
|
|
@ -13,8 +13,37 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
<form class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||||
|
<!-- resource-pool -->
|
||||||
|
<div class="form-group" ng-if="ctrl.formValues.ResourcePool">
|
||||||
|
<label for="resource-pool-selector" class="col-sm-3 col-lg-2 control-label required text-left">Namespace</label>
|
||||||
|
<div class="col-sm-8 col-lg-9">
|
||||||
|
<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()"
|
||||||
|
data-cy="k8sConfigCreate-namespaceDropdown"
|
||||||
|
></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded() && ctrl.formValues.ResourcePool">
|
||||||
|
<div class="col-sm-12 small text-warning vertical-center">
|
||||||
|
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||||
|
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. 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 vertical-center">
|
||||||
|
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||||
|
You do not have access to any namespace. Contact your administrator to get access to a namespace.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !resource-pool -->
|
||||||
|
|
||||||
<!-- name -->
|
<!-- name -->
|
||||||
<div class="form-group mb-0">
|
<div class="form-group">
|
||||||
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
|
<label for="configuration_name" class="col-sm-3 col-lg-2 control-label required text-left">Name</label>
|
||||||
<div class="col-sm-8 col-lg-9 mb-0">
|
<div class="col-sm-8 col-lg-9 mb-0">
|
||||||
<input
|
<input
|
||||||
|
@ -47,36 +76,9 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !name -->
|
<!-- !name -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Namespace </div>
|
<div class="col-sm-12 !p-0">
|
||||||
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
<!-- resource-pool -->
|
|
||||||
<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 col-lg-9">
|
|
||||||
<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()"
|
|
||||||
data-cy="k8sConfigCreate-namespaceDropdown"
|
|
||||||
></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
|
||||||
<div class="col-sm-12 small text-warning vertical-center">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
|
||||||
This namespace has exhausted its resource capacity and you will not be able to deploy the configuration. 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 vertical-center">
|
|
||||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
|
||||||
You do not have access to any namespace. Contact your administrator to get access to a namespace.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !resource-pool -->
|
|
||||||
|
|
||||||
<div ng-if="ctrl.formValues.ResourcePool">
|
<div ng-if="ctrl.formValues.ResourcePool">
|
||||||
<div class="col-sm-12 form-section-title"> Configuration kind </div>
|
<div class="col-sm-12 form-section-title"> Configuration kind </div>
|
||||||
|
|
|
@ -102,6 +102,10 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
<form ng-if="!ctrl.isSystemConfig()" class="form-horizontal" name="kubernetesConfigurationCreationForm" autocomplete="off">
|
||||||
|
<div class="col-sm-12 !p-0">
|
||||||
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
|
</div>
|
||||||
|
|
||||||
<kubernetes-configuration-data
|
<kubernetes-configuration-data
|
||||||
ng-if="ctrl.formValues"
|
ng-if="ctrl.formValues"
|
||||||
form-values="ctrl.formValues"
|
form-values="ctrl.formValues"
|
||||||
|
|
|
@ -51,6 +51,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 !p-0">
|
||||||
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Quota </div>
|
<div class="col-sm-12 form-section-title"> Quota </div>
|
||||||
|
|
|
@ -32,6 +32,11 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 !p-0">
|
||||||
|
<annotations-be-teaser></annotations-be-teaser>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">Resource quota</div>
|
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">Resource quota</div>
|
||||||
<!-- quotas-switch -->
|
<!-- quotas-switch -->
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const componentsModule = angular
|
||||||
'buttonText',
|
'buttonText',
|
||||||
'className',
|
'className',
|
||||||
'icon',
|
'icon',
|
||||||
|
'buttonClassName',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface Props {
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
|
buttonClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BETeaserButton({
|
export function BETeaserButton({
|
||||||
|
@ -21,6 +22,7 @@ export function BETeaserButton({
|
||||||
buttonText,
|
buttonText,
|
||||||
className,
|
className,
|
||||||
icon,
|
icon,
|
||||||
|
buttonClassName,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<TooltipWithChildren
|
<TooltipWithChildren
|
||||||
|
@ -31,6 +33,7 @@ export function BETeaserButton({
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
|
className={buttonClassName}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
type="button"
|
type="button"
|
||||||
color="warninglight"
|
color="warninglight"
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
|
||||||
|
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { BETeaserButton } from '@@/BETeaserButton';
|
||||||
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
|
||||||
|
export function AnnotationsBeTeaser() {
|
||||||
|
return (
|
||||||
|
<div className="col-sm-12 text-muted mb-2 block px-0">
|
||||||
|
<div className="control-label !mb-2 text-left font-medium">
|
||||||
|
Annotations
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
<div className="vertical-center">
|
||||||
|
<span>
|
||||||
|
You can specify{' '}
|
||||||
|
<a
|
||||||
|
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||||
|
target="_black"
|
||||||
|
>
|
||||||
|
annotations
|
||||||
|
</a>{' '}
|
||||||
|
for the object. See further Kubernetes documentation on{' '}
|
||||||
|
<a
|
||||||
|
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
|
||||||
|
target="_black"
|
||||||
|
>
|
||||||
|
well-known annotations
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
setHtmlMessage
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="block">
|
||||||
|
<BETeaserButton
|
||||||
|
className="!p-0"
|
||||||
|
heading="Add annotation"
|
||||||
|
buttonText="Add annotation"
|
||||||
|
message="Allows specifying of annotations on this resource."
|
||||||
|
featureId={FeatureId.K8S_ANNOTATIONS}
|
||||||
|
buttonClassName="!ml-0"
|
||||||
|
icon={Plus}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useMemo, ReactNode } from 'react';
|
import { useState, useEffect, useMemo, ReactNode, useCallback } from 'react';
|
||||||
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useConfigurations } from '@/react/kubernetes/configs/queries';
|
import { useConfigurations } from '@/react/kubernetes/configs/queries';
|
||||||
|
@ -286,9 +287,155 @@ export function CreateIngressView() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ingressRule, servicePorts]);
|
}, [ingressRule, servicePorts]);
|
||||||
|
|
||||||
|
const validate = useCallback(
|
||||||
|
(
|
||||||
|
ingressRule: Rule,
|
||||||
|
ingressNames: string[],
|
||||||
|
serviceOptions: Option<string>[],
|
||||||
|
existingIngressClass?: IngressController
|
||||||
|
) => {
|
||||||
|
const errors: Record<string, ReactNode> = {};
|
||||||
|
const rule = { ...ingressRule };
|
||||||
|
|
||||||
|
// User cannot edit the namespace and the ingress name
|
||||||
|
if (!isEdit) {
|
||||||
|
if (!rule.Namespace) {
|
||||||
|
errors.namespace = 'Namespace is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||||
|
if (!rule.IngressName) {
|
||||||
|
errors.ingressName = 'Ingress name is required';
|
||||||
|
} else if (!nameRegex.test(rule.IngressName)) {
|
||||||
|
errors.ingressName =
|
||||||
|
"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').";
|
||||||
|
} else if (ingressNames.includes(rule.IngressName)) {
|
||||||
|
errors.ingressName = 'Ingress name already exists';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.IngressClassName) {
|
||||||
|
errors.className = 'Ingress class is required';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEdit && !ingressRule.IngressClassName) {
|
||||||
|
errors.className =
|
||||||
|
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
(!existingIngressClass ||
|
||||||
|
(existingIngressClass && !existingIngressClass.Availability)) &&
|
||||||
|
ingressRule.IngressClassName
|
||||||
|
) {
|
||||||
|
if (!rule.IngressType) {
|
||||||
|
errors.className =
|
||||||
|
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.';
|
||||||
|
} else {
|
||||||
|
errors.className =
|
||||||
|
'Currently set to an ingress class that you do not have access to - you must select a valid class.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicatedAnnotations: string[] = [];
|
||||||
|
rule.Annotations?.forEach((a, i) => {
|
||||||
|
if (!a.Key) {
|
||||||
|
errors[`annotations.key[${i}]`] = 'Annotation key is required';
|
||||||
|
} else if (duplicatedAnnotations.includes(a.Key)) {
|
||||||
|
errors[`annotations.key[${i}]`] = 'Annotation cannot be duplicated';
|
||||||
|
}
|
||||||
|
if (!a.Value) {
|
||||||
|
errors[`annotations.value[${i}]`] = 'Annotation value is required';
|
||||||
|
}
|
||||||
|
duplicatedAnnotations.push(a.Key);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicatedHosts: string[] = [];
|
||||||
|
// Check if the paths are duplicates
|
||||||
|
rule.Hosts?.forEach((host, hi) => {
|
||||||
|
if (!host.NoHost) {
|
||||||
|
if (!host.Host) {
|
||||||
|
errors[`hosts[${hi}].host`] = 'Host is required';
|
||||||
|
} else if (duplicatedHosts.includes(host.Host)) {
|
||||||
|
errors[`hosts[${hi}].host`] = 'Host cannot be duplicated';
|
||||||
|
}
|
||||||
|
duplicatedHosts.push(host.Host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate service
|
||||||
|
host.Paths?.forEach((path, pi) => {
|
||||||
|
if (!path.ServiceName) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].servicename`] =
|
||||||
|
'Service name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isEdit &&
|
||||||
|
path.ServiceName &&
|
||||||
|
!serviceOptions.find((s) => s.value === path.ServiceName)
|
||||||
|
) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].servicename`] = (
|
||||||
|
<span>
|
||||||
|
Currently set to {path.ServiceName}, which does not exist. You
|
||||||
|
can create a service with this name for a particular deployment
|
||||||
|
via{' '}
|
||||||
|
<Link
|
||||||
|
to="kubernetes.applications"
|
||||||
|
params={{ id: environmentId }}
|
||||||
|
className="text-primary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Applications
|
||||||
|
</Link>
|
||||||
|
, and on returning here it will be picked up.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.ServicePort) {
|
||||||
|
errors[`hosts[${hi}].paths[${pi}].serviceport`] =
|
||||||
|
'Service port is required';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Validate paths
|
||||||
|
const paths = host.Paths.map((path) => path.Route);
|
||||||
|
paths.forEach((item, idx) => {
|
||||||
|
if (!item) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] = 'Path cannot be empty';
|
||||||
|
} else if (paths.indexOf(item) !== idx) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] =
|
||||||
|
'Paths cannot be duplicated';
|
||||||
|
} else {
|
||||||
|
// Validate host and path combination globally
|
||||||
|
const isExists = checkIfPathExistsWithHost(
|
||||||
|
ingresses,
|
||||||
|
host.Host,
|
||||||
|
item,
|
||||||
|
params.name
|
||||||
|
);
|
||||||
|
if (isExists) {
|
||||||
|
errors[`hosts[${hi}].paths[${idx}].path`] =
|
||||||
|
'Path is already in use with the same host';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setErrors(errors);
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[ingresses, environmentId, isEdit, params.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedValidate = useMemo(() => debounce(validate, 300), [validate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (namespace.length > 0) {
|
if (namespace.length > 0) {
|
||||||
validate(
|
debouncedValidate(
|
||||||
ingressRule,
|
ingressRule,
|
||||||
ingressNames || [],
|
ingressNames || [],
|
||||||
servicesOptions || [],
|
servicesOptions || [],
|
||||||
|
@ -302,6 +449,7 @@ export function CreateIngressView() {
|
||||||
ingressNames,
|
ingressNames,
|
||||||
servicesOptions,
|
servicesOptions,
|
||||||
existingIngressClass,
|
existingIngressClass,
|
||||||
|
debouncedValidate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -361,146 +509,6 @@ export function CreateIngressView() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function validate(
|
|
||||||
ingressRule: Rule,
|
|
||||||
ingressNames: string[],
|
|
||||||
serviceOptions: Option<string>[],
|
|
||||||
existingIngressClass?: IngressController
|
|
||||||
) {
|
|
||||||
const errors: Record<string, ReactNode> = {};
|
|
||||||
const rule = { ...ingressRule };
|
|
||||||
|
|
||||||
// User cannot edit the namespace and the ingress name
|
|
||||||
if (!isEdit) {
|
|
||||||
if (!rule.Namespace) {
|
|
||||||
errors.namespace = 'Namespace is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameRegex = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
||||||
if (!rule.IngressName) {
|
|
||||||
errors.ingressName = 'Ingress name is required';
|
|
||||||
} else if (!nameRegex.test(rule.IngressName)) {
|
|
||||||
errors.ingressName =
|
|
||||||
"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').";
|
|
||||||
} else if (ingressNames.includes(rule.IngressName)) {
|
|
||||||
errors.ingressName = 'Ingress name already exists';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rule.IngressClassName) {
|
|
||||||
errors.className = 'Ingress class is required';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEdit && !ingressRule.IngressClassName) {
|
|
||||||
errors.className =
|
|
||||||
'No ingress class is currently set for this ingress - use of the Portainer UI requires one to be set.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isEdit &&
|
|
||||||
(!existingIngressClass ||
|
|
||||||
(existingIngressClass && !existingIngressClass.Availability)) &&
|
|
||||||
ingressRule.IngressClassName
|
|
||||||
) {
|
|
||||||
if (!rule.IngressType) {
|
|
||||||
errors.className =
|
|
||||||
'Currently set to an ingress class that cannot be found in the cluster - you must select a valid class.';
|
|
||||||
} else {
|
|
||||||
errors.className =
|
|
||||||
'Currently set to an ingress class that you do not have access to - you must select a valid class.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicatedAnnotations: string[] = [];
|
|
||||||
rule.Annotations?.forEach((a, i) => {
|
|
||||||
if (!a.Key) {
|
|
||||||
errors[`annotations.key[${i}]`] = 'Annotation key is required';
|
|
||||||
} else if (duplicatedAnnotations.includes(a.Key)) {
|
|
||||||
errors[`annotations.key[${i}]`] = 'Annotation cannot be duplicated';
|
|
||||||
}
|
|
||||||
if (!a.Value) {
|
|
||||||
errors[`annotations.value[${i}]`] = 'Annotation value is required';
|
|
||||||
}
|
|
||||||
duplicatedAnnotations.push(a.Key);
|
|
||||||
});
|
|
||||||
|
|
||||||
const duplicatedHosts: string[] = [];
|
|
||||||
// Check if the paths are duplicates
|
|
||||||
rule.Hosts?.forEach((host, hi) => {
|
|
||||||
if (!host.NoHost) {
|
|
||||||
if (!host.Host) {
|
|
||||||
errors[`hosts[${hi}].host`] = 'Host is required';
|
|
||||||
} else if (duplicatedHosts.includes(host.Host)) {
|
|
||||||
errors[`hosts[${hi}].host`] = 'Host cannot be duplicated';
|
|
||||||
}
|
|
||||||
duplicatedHosts.push(host.Host);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate service
|
|
||||||
host.Paths?.forEach((path, pi) => {
|
|
||||||
if (!path.ServiceName) {
|
|
||||||
errors[`hosts[${hi}].paths[${pi}].servicename`] =
|
|
||||||
'Service name is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
isEdit &&
|
|
||||||
path.ServiceName &&
|
|
||||||
!serviceOptions.find((s) => s.value === path.ServiceName)
|
|
||||||
) {
|
|
||||||
errors[`hosts[${hi}].paths[${pi}].servicename`] = (
|
|
||||||
<span>
|
|
||||||
Currently set to {path.ServiceName}, which does not exist. You can
|
|
||||||
create a service with this name for a particular deployment via{' '}
|
|
||||||
<Link
|
|
||||||
to="kubernetes.applications"
|
|
||||||
params={{ id: environmentId }}
|
|
||||||
className="text-primary"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Applications
|
|
||||||
</Link>
|
|
||||||
, and on returning here it will be picked up.
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.ServicePort) {
|
|
||||||
errors[`hosts[${hi}].paths[${pi}].serviceport`] =
|
|
||||||
'Service port is required';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Validate paths
|
|
||||||
const paths = host.Paths.map((path) => path.Route);
|
|
||||||
paths.forEach((item, idx) => {
|
|
||||||
if (!item) {
|
|
||||||
errors[`hosts[${hi}].paths[${idx}].path`] = 'Path cannot be empty';
|
|
||||||
} else if (paths.indexOf(item) !== idx) {
|
|
||||||
errors[`hosts[${hi}].paths[${idx}].path`] =
|
|
||||||
'Paths cannot be duplicated';
|
|
||||||
} else {
|
|
||||||
// Validate host and path combination globally
|
|
||||||
const isExists = checkIfPathExistsWithHost(
|
|
||||||
ingresses,
|
|
||||||
host.Host,
|
|
||||||
item,
|
|
||||||
params.name
|
|
||||||
);
|
|
||||||
if (isExists) {
|
|
||||||
errors[`hosts[${hi}].paths[${idx}].path`] =
|
|
||||||
'Path is already in use with the same host';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
setErrors(errors);
|
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNamespaceChange(ns: string) {
|
function handleNamespaceChange(ns: string) {
|
||||||
setNamespace(ns);
|
setNamespace(ns);
|
||||||
if (!isEdit) {
|
if (!isEdit) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { FormError } from '@@/form-components/FormError';
|
||||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||||
|
|
||||||
import { Annotations } from './Annotations';
|
import { Annotations } from './Annotations';
|
||||||
import { Rule, ServicePorts } from './types';
|
import { Rule, ServicePorts } from './types';
|
||||||
|
@ -199,27 +200,33 @@ export function IngressForm({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-12 text-muted !mb-0 px-0">
|
<div className="col-sm-12 text-muted !mb-0 px-0">
|
||||||
<div className="mb-2">Annotations</div>
|
<div className="control-label !mb-3 text-left font-medium">
|
||||||
<p className="vertical-center text-muted small">
|
Annotations
|
||||||
<Icon icon={Info} mode="primary" />
|
<Tooltip
|
||||||
<span>
|
message={
|
||||||
You can specify{' '}
|
<div className="vertical-center">
|
||||||
<a
|
<span>
|
||||||
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
You can specify{' '}
|
||||||
target="_black"
|
<a
|
||||||
>
|
href="https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/"
|
||||||
annotations
|
target="_black"
|
||||||
</a>{' '}
|
>
|
||||||
for the object. See further Kubernetes documentation on{' '}
|
annotations
|
||||||
<a
|
</a>{' '}
|
||||||
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
|
for the object. See further Kubernetes documentation on{' '}
|
||||||
target="_black"
|
<a
|
||||||
>
|
href="https://kubernetes.io/docs/reference/labels-annotations-taints/"
|
||||||
well-known annotations
|
target="_black"
|
||||||
</a>
|
>
|
||||||
.
|
well-known annotations
|
||||||
</span>
|
</a>
|
||||||
</p>
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
setHtmlMessage
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{rule?.Annotations && (
|
{rule?.Annotations && (
|
||||||
|
@ -233,38 +240,46 @@ export function IngressForm({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="col-sm-12 anntation-actions p-0">
|
<div className="col-sm-12 anntation-actions p-0">
|
||||||
<Button
|
<TooltipWithChildren message="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type.">
|
||||||
className="btn btn-sm btn-light mb-2 !ml-0"
|
<span>
|
||||||
onClick={() => addNewAnnotation()}
|
<Button
|
||||||
icon={Plus}
|
className="btn btn-sm btn-light mb-2 !ml-0"
|
||||||
title="Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type."
|
onClick={() => addNewAnnotation()}
|
||||||
>
|
icon={Plus}
|
||||||
{' '}
|
>
|
||||||
add annotation
|
{' '}
|
||||||
</Button>
|
Add annotation
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipWithChildren>
|
||||||
|
|
||||||
{rule.IngressType === 'nginx' && (
|
{rule.IngressType === 'nginx' && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<TooltipWithChildren message="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to.">
|
||||||
className="btn btn-sm btn-light mb-2 ml-2"
|
<span>
|
||||||
onClick={() => addNewAnnotation('rewrite')}
|
<Button
|
||||||
icon={Plus}
|
className="btn btn-sm btn-light mb-2 ml-2"
|
||||||
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
|
onClick={() => addNewAnnotation('rewrite')}
|
||||||
data-cy="add-rewrite-annotation"
|
icon={Plus}
|
||||||
>
|
data-cy="add-rewrite-annotation"
|
||||||
{' '}
|
>
|
||||||
Add rewrite annotation
|
Add rewrite annotation
|
||||||
</Button>
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipWithChildren>
|
||||||
|
|
||||||
<Button
|
<TooltipWithChildren message="Enable use of regular expressions in ingress paths (set in the ingress details of an application). Use this along with rewrite-target to specify the regex capturing group to be replaced, e.g. path regex of ^/foo/(,*) and rewrite-target of /bar/$1 rewrites example.com/foo/account to example.com/bar/account.">
|
||||||
className="btn btn-sm btn-light mb-2 ml-2"
|
<span>
|
||||||
onClick={() => addNewAnnotation('regex')}
|
<Button
|
||||||
icon={Plus}
|
className="btn btn-sm btn-light mb-2 ml-2"
|
||||||
title="When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to."
|
onClick={() => addNewAnnotation('regex')}
|
||||||
data-cy="add-regex-annotation"
|
icon={Plus}
|
||||||
>
|
data-cy="add-regex-annotation"
|
||||||
Add regular expression annotation
|
>
|
||||||
</Button>
|
Add regular expression annotation
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipWithChildren>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ function getConfigRoute(environment: Environment) {
|
||||||
case PlatformType.Docker:
|
case PlatformType.Docker:
|
||||||
return getDockerConfigRoute(environment);
|
return getDockerConfigRoute(environment);
|
||||||
case PlatformType.Kubernetes:
|
case PlatformType.Kubernetes:
|
||||||
return 'kubernetes.cluster';
|
return 'kubernetes.cluster.setup';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,4 +37,5 @@ export enum FeatureId {
|
||||||
ENFORCE_DEPLOYMENT_OPTIONS = 'k8s-enforce-deployment-options',
|
ENFORCE_DEPLOYMENT_OPTIONS = 'k8s-enforce-deployment-options',
|
||||||
K8S_ADM_ONLY_USR_INGRESS_DEPLY = 'k8s-admin-only-ingress-deploy',
|
K8S_ADM_ONLY_USR_INGRESS_DEPLY = 'k8s-admin-only-ingress-deploy',
|
||||||
K8S_ROLLING_RESTART = 'k8s-rolling-restart',
|
K8S_ROLLING_RESTART = 'k8s-rolling-restart',
|
||||||
|
K8S_ANNOTATIONS = 'k8s-annotations',
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ export async function init(edition: Edition) {
|
||||||
[FeatureId.ENFORCE_DEPLOYMENT_OPTIONS]: Edition.BE,
|
[FeatureId.ENFORCE_DEPLOYMENT_OPTIONS]: Edition.BE,
|
||||||
[FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE,
|
[FeatureId.K8S_ADM_ONLY_USR_INGRESS_DEPLY]: Edition.BE,
|
||||||
[FeatureId.K8S_ROLLING_RESTART]: Edition.BE,
|
[FeatureId.K8S_ROLLING_RESTART]: Edition.BE,
|
||||||
|
[FeatureId.K8S_ANNOTATIONS]: Edition.BE,
|
||||||
};
|
};
|
||||||
|
|
||||||
state.currentEdition = currentEdition;
|
state.currentEdition = currentEdition;
|
||||||
|
|
|
@ -14,3 +14,4 @@ export const FORCE_REDEPLOYMENT = 'force-redeployment';
|
||||||
export const STACK_PULL_IMAGE = 'stack-pull-image';
|
export const STACK_PULL_IMAGE = 'stack-pull-image';
|
||||||
export const STACK_WEBHOOK = 'stack-webhook';
|
export const STACK_WEBHOOK = 'stack-webhook';
|
||||||
export const CONTAINER_WEBHOOK = 'container-webhook';
|
export const CONTAINER_WEBHOOK = 'container-webhook';
|
||||||
|
export const K8S_ANNOTATIONS = 'k8s-annotations';
|
||||||
|
|
Loading…
Reference in New Issue