1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 13:25:26 +02:00

feat(k8s/applications): expose applications via ingress (#4136)

* feat(k8s/endpoint): expose ingress controllers on endpoints

* feat(k8s/applications): add ability to expose applications over ingress - missing RP and app edits

* feat(k8s/application): add validation for ingress routes

* feat(k8s/resource-pools): edit available ingress classes

* fix(k8s/ingress): var name refactor was partially applied

* feat(kubernetes): double validation on RP edit

* feat(k8s/application): app edit ingress update + formvalidation + UI rework

* feat(k8s/ingress): dictionary for default annotations on ingress creation

* fix(k8s/application): temporary fix + TODO dev notice

* feat(k8s/application): select default ingress of selected resource pool

* feat(k8s/ingress): revert ingressClassName removal

* feat(k8s/ingress): admins can now add an host to ingress in a resource pool

* feat(k8s/resource-pool): list applications using RP ingresses

* feat(k8s/configure): minor UI update

* feat(k8s/configure): minor UI update

* feat(k8s/configure): minor UI update

* feat(k8s/configure): minor UI update

* feat(k8s/configure): minor UI update

* fix(k8s/ingresses): remove host if undefined

* feat(k8s/resource-pool): remove the activate ingresses switch

* fix(k8s/resource-pool): edditing an ingress host was deleting all the routes of the ingress

* feat(k8s/application): prevent app deploy if no ports to publish and publishing type not internal

* feat(k8s/ingress): minor UI update

* fix(k8s/ingress): allow routes without prepending /

* feat(k8s/application): add form validation on ingress route

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
xAt0mZ 2020-08-13 01:30:23 +02:00 committed by GitHub
parent 201c3ac143
commit f91d3f1ca3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1595 additions and 443 deletions

View file

@ -16,7 +16,7 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
<!-- name -->
<!-- #region NAME FIELD -->
<div class="form-group">
<label for="application_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
@ -48,9 +48,9 @@
>
</div>
</div>
<!-- !name -->
<!-- #endregion -->
<!-- image -->
<!-- #region IMAGE FIELD -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
@ -64,13 +64,12 @@
</div>
</div>
</div>
<!-- !image -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Resource pool
</div>
<!-- resource-pool -->
<!-- #region RESOURCE POOL -->
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
<div class="col-sm-11">
@ -91,12 +90,12 @@
resource pool.
</div>
</div>
<!-- !resource-pool -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Stack
</div>
<!-- #region STACK -->
<div class="form-group">
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -105,7 +104,6 @@
</div>
</div>
<!-- stack -->
<div class="form-group">
<label for="stack_name" class="col-sm-1 control-label text-left">Stack</label>
<div class="col-sm-11">
@ -121,13 +119,12 @@
/>
</div>
</div>
<!-- !stack -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Environment
</div>
<!-- environment-variables -->
<!-- #region ENVIRONMENT VARIABLES -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Environment variables</label>
@ -146,7 +143,7 @@
name="environment_variable_name_{{ $index }}"
class="form-control"
ng-model="envVar.Name"
ng-change="ctrl.onChangeEnvironmentName($index)"
ng-change="ctrl.onChangeEnvironmentName()"
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
placeholder="foo"
required
@ -155,7 +152,9 @@
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
@ -164,7 +163,7 @@
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
>
</ng-messages>
<p ng-if="ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
>
</div>
@ -177,22 +176,21 @@
<div class="input-group col-sm-2 input-group-sm">
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable($index)">
<i class="fa fa-times" aria-hidden="true"></i>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable($index)">
Restore
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<!-- !environment-variables -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Configurations
</div>
<!-- configurations -->
<!-- #region CONFIGURATIONS -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Configurations</label>
@ -231,7 +229,7 @@
<button class="btn btn-sm btn-primary" type="button" ng-if="config.Overriden" ng-click="ctrl.resetConfiguration(index)">
<i class="fa fa-undo" aria-hidden="true"></i> Auto
</button>
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)"> <i class="fa fa-trash" aria-hidden="true"></i> Remove </button>
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)"> <i class="fa fa-trash-alt" aria-hidden="true"></i> Remove </button>
</div>
<!-- no-override -->
<div class="col-sm-12" style="margin-top: 10px;" ng-if="config.SelectedConfiguration && !config.Overriden">
@ -277,13 +275,13 @@
style="margin-top: 5px;"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined"
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
>
</div>
@ -302,12 +300,12 @@
<!-- !has-override -->
</div>
<!-- !config-element -->
<!-- !configurations -->
<!-- #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">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -315,7 +313,6 @@
</div>
</div>
<!-- persisted folders -->
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Persisted folders</label>
@ -333,7 +330,7 @@
class="form-control"
name="persisted_folder_path_{{ $index }}"
ng-model="persistedFolder.ContainerPath"
ng-change="ctrl.onChangePersistedFolderPath($index)"
ng-change="ctrl.onChangePersistedFolderPath()"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
placeholder="/data"
required
@ -360,7 +357,7 @@
uib-btn-radio="false"
ng-change="ctrl.useExistingVolume($index)"
ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET"
>Use an existing volume</label
>Existing volume</label
>
</span>
</div>
@ -422,10 +419,10 @@
<div class="input-group col-sm-1 input-group-sm">
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()" ng-if="!ctrl.state.useExistingVolume[$index]">
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
<i class="fa fa-times" aria-hidden="true"></i>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
Restore
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
</div>
@ -434,22 +431,22 @@
<div
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined ||
ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined ||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
ctrl.state.duplicateExistingVolumes[$index] !== undefined
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.duplicatePersistedFolderPaths[$index] !== undefined"
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 ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined"
<p ng-if="ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already defined.</p
>
</div>
@ -466,12 +463,12 @@
</div>
<div
class="small text-warning"
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicateExistingVolumes[$index] !== undefined"
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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Volume is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicateExistingVolumes[$index] !== undefined"
<p ng-if="ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This volume is already used.</p
>
</div>
@ -483,8 +480,9 @@
</div>
</div>
</div>
<!-- !persisted folders -->
<!-- #endregion -->
<!-- #region DATA ACCESS POLICY -->
<div ng-if="ctrl.showDataAccessPolicySection()">
<div class="form-group">
<div class="col-sm-12">
@ -579,11 +577,12 @@
</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">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -668,11 +667,12 @@
</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.
@ -775,8 +775,9 @@
>. You will not be able to scale that application.
</div>
</div>
<!-- #endregion -->
<!-- auto scaling -->
<!-- #region AUTO SCALING -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL">
Auto-scaling
</div>
@ -884,12 +885,12 @@
</div>
</div>
</div>
<!-- !auto scaling -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Publishing the application
</div>
<!-- #region PUBLISHING OPTIONS -->
<div class="form-group">
<div class="col-sm-12 small text-muted">
Select how you want to publish your application.
@ -899,9 +900,36 @@
<!-- publishing options -->
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="publishing_internal" ng-value="ctrl.ApplicationPublishingTypes.INTERNAL" ng-model="ctrl.formValues.PublishingType" />
<label for="publishing_internal">
<div ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
<input
type="radio"
id="publishing_internal"
ng-value="ctrl.ApplicationPublishingTypes.INTERNAL"
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
/>
<label
for="publishing_internal"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INTERNAL)
"
>
<div class="boxselector_header">
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
Internal
</div>
<p>Internal communications inside the cluster only</p>
</label>
<label
for="publishing_internal"
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INTERNAL"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
Internal
@ -909,9 +937,37 @@
<p>Internal communications inside the cluster only</p>
</label>
</div>
<div>
<input type="radio" id="publishing_cluster" ng-value="ctrl.ApplicationPublishingTypes.CLUSTER" ng-model="ctrl.formValues.PublishingType" />
<label for="publishing_cluster">
<div ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
<input
type="radio"
id="publishing_cluster"
ng-value="ctrl.ApplicationPublishingTypes.CLUSTER"
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
/>
<label
for="publishing_cluster"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
"
>
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
Cluster
</div>
<p>Publish this application via a port on all nodes of the cluster</p>
</label>
<label
for="publishing_cluster"
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.CLUSTER"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
Cluster
@ -919,9 +975,74 @@
<p>Publish this application via a port on all nodes of the cluster</p>
</label>
</div>
<div ng-if="ctrl.publishViaLoadBalancerEnabled()">
<input type="radio" id="publishing_loadbalancer" ng-value="ctrl.ApplicationPublishingTypes.LOAD_BALANCER" ng-model="ctrl.formValues.PublishingType" />
<label for="publishing_loadbalancer">
<div ng-if="ctrl.publishViaIngressEnabled()" ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
<input
type="radio"
id="publishing_ingress"
ng-value="ctrl.ApplicationPublishingTypes.INGRESS"
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
/>
<label
for="publishing_ingress"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
"
>
<div class="boxselector_header">
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
Ingress
</div>
<p>Publish this application via a HTTP route</p>
</label>
<label
for="publishing_ingress"
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INGRESS"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
Ingress
</div>
<p>Publish this application via a HTTP route</p>
</label>
</div>
<div ng-if="ctrl.publishViaLoadBalancerEnabled()" ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
<input
type="radio"
id="publishing_loadbalancer"
ng-value="ctrl.ApplicationPublishingTypes.LOAD_BALANCER"
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
/>
<label
for="publishing_loadbalancer"
ng-if="
!ctrl.isPublishingTypeEditDisabled() ||
(ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER)
"
>
<div class="boxselector_header">
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
Load balancer
</div>
<p>Publish this application via a load balancer</p>
</label>
<label
for="publishing_loadbalancer"
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.LOAD_BALANCER"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
style="cursor: pointer; border-color: #767676;"
>
<div class="boxselector_header">
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
Load balancer
@ -931,9 +1052,9 @@
</div>
</div>
</div>
<!-- !publishing options -->
<!-- #endregion -->
<!-- published ports -->
<!-- #region PUBLISHED PORTS -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Published ports</label>
@ -951,8 +1072,12 @@
When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port
number inside the default range <code>30000-32767</code>.
</div>
<div ng-if="ctrl.isNotInternalAndHasNoPublishedPorts()" class="col-sm-12 small text-muted text-warning" style="margin-top: 12px;">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> At least one published port must be defined.
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<!-- #region INPUTS -->
<div
ng-repeat-start="publishedPort in ctrl.formValues.PublishedPorts"
style="margin-top: 2px;"
@ -960,9 +1085,9 @@
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
tooltip-enable="ctrl.disableLoadBalancerEdit()"
uib-tooltip="Edition is not allowed while the Load Balancer is in Pending state"
uib-tooltip="Edition is not allowed while the Load Balancer is in 'Pending' state"
>
<div class="col-sm-4 input-group input-group-sm">
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
<span class="input-group-addon">container port</span>
<input
type="number"
@ -972,12 +1097,20 @@
placeholder="80"
ng-min="1"
ng-max="65535"
required
ng-disabled="ctrl.disableLoadBalancerEdit()"
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingContainerPort()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
</div>
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
<div
class="col-sm-3 input-group input-group-sm"
ng-class="{ striked: publishedPort.NeedsDeletion }"
ng-if="
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER) ||
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
"
>
<span class="input-group-addon">node port</span>
<input
name="published_node_port_{{ $index }}"
@ -987,10 +1120,19 @@
placeholder="30080"
ng-min="30000"
ng-max="32767"
ng-change="ctrl.onChangePortMappingNodePort()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
</div>
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
<div
class="col-sm-3 input-group input-group-sm"
ng-class="{ striked: publishedPort.NeedsDeletion }"
ng-if="
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER) ||
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER)
"
>
<span class="input-group-addon">load balancer port</span>
<input
type="number"
@ -1001,69 +1143,206 @@
value="8080"
ng-min="1"
ng-max="65535"
required
ng-disabled="ctrl.disableLoadBalancerEdit()"
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingLoadBalancerPort()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
</div>
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'TCP'" ng-disabled="ctrl.disableLoadBalancerEdit()">TCP</label>
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'UDP'" ng-disabled="ctrl.disableLoadBalancerEdit()">UDP</label>
<div
class="col-sm-3 input-group input-group-sm"
ng-class="{ striked: publishedPort.NeedsDeletion }"
ng-if="
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
"
>
<span class="input-group-addon">ingress</span>
<select
class="form-control"
name="ingress_class_{{ $index }}"
ng-model="publishedPort.IngressName"
ng-options="ingress.Name as ingress.Name for ingress in ctrl.filteredIngresses"
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingIngress($index)"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
>
<option selected disabled hidden value="">Select an ingress</option>
</select>
</div>
<div
class="col-sm-3 input-group input-group-sm"
ng-class="{ striked: publishedPort.NeedsDeletion }"
ng-if="
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
"
>
<span class="input-group-addon">route</span>
<input
class="form-control"
name="ingress_route_{{ $index }}"
ng-model="publishedPort.IngressRoute"
placeholder="foo"
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingIngressRoute()"
ng-pattern="/^\/?([a-zA-Z0-9]+[a-zA-Z0-9-/_]*[a-zA-Z0-9]|[a-zA-Z0-9]+)$/"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
</div>
<div class="input-group col-sm-2 input-group-sm">
<div class="btn-group btn-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
<label
class="btn btn-primary"
ng-model="publishedPort.Protocol"
uib-btn-radio="'TCP'"
ng-change="ctrl.onChangePortMappingContainerPort()"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
>TCP</label
>
<label
class="btn btn-primary"
ng-model="publishedPort.Protocol"
uib-btn-radio="'UDP'"
ng-change="ctrl.onChangePortMappingContainerPort()"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
>UDP</label
>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePublishedPort($index)" ng-if="!ctrl.disableLoadBalancerEdit()">
<i class="fa fa-times" aria-hidden="true"></i>
<button
ng-if="!ctrl.disableLoadBalancerEdit() && !publishedPort.NeedsDeletion"
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removePublishedPort($index)"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button
ng-if="publishedPort.NeedsDeletion && ctrl.formValues.PublishingType === ctrl.savedFormValues.PublishingType"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePublishedPort($index)"
>
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- #endregion -->
<!-- #region VALIDATION -->
<div
ng-repeat-end
ng-if="
ng-show="
kubernetesApplicationCreationForm['container_port_' + $index].$invalid ||
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid ||
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid ||
kubernetesApplicationCreationForm['ingress_class_' + $index].$invalid ||
kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid ||
ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined) ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS && ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined) ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER &&
ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined)
"
>
<div class="col-sm-4 input-group input-group-sm">
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['container_port_' + $index].$invalid">
<div class="col-sm-3 input-group input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-if="
kubernetesApplicationCreationForm['container_port_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined
"
>
<div ng-messages="kubernetesApplicationCreationForm['container_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
</div>
<p ng-if="ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This port is already used.
</p>
</div>
</div>
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid">
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-if="
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined
"
>
<div ng-messages="kubernetesApplicationCreationForm['published_node_port_'+$index].$error">
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
</div>
<p ng-if="ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This port is already used.
</p>
</div>
</div>
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid">
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS">
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['ingress_class_' + $index].$invalid">
<div ng-messages="kubernetesApplicationCreationForm['ingress_class_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress selection is required.</p>
</div>
</div>
</div>
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-if="kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined"
>
<div ng-messages="kubernetesApplicationCreationForm['ingress_route_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_' or
'/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
>
</div>
<p ng-if="ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This route is already used.
</p>
</div>
</div>
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-if="
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid ||
ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined
"
>
<div ng-messages="kubernetesApplicationCreationForm['load_balancer_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
</div>
<p ng-if="ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
This port is already used.
</p>
</div>
</div>
<div class="input-group col-sm-1 input-group-sm"> </div>
</div>
<!-- #endregion -->
</div>
</div>
<!-- !published ports -->
<!-- #endregion -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<!-- #region ACTIONS -->
<div class="form-group">
<div class="col-sm-12">
<button
@ -1083,10 +1362,12 @@
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
>Cancel</button
>
Cancel
</button>
</div>
</div>
<!-- #endregion -->
</form>
</rd-widget-body>
</rd-widget>

View file

@ -1,5 +1,5 @@
import angular from 'angular';
import _ from 'lodash-es';
import * as _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import * as JsonPatch from 'fast-json-patch';
@ -27,6 +27,8 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
class KubernetesCreateApplicationController {
/* #region CONSTRUCTOR */
/* @ngInject */
constructor(
$async,
@ -40,6 +42,7 @@ class KubernetesCreateApplicationController {
KubernetesStackService,
KubernetesConfigurationService,
KubernetesNodeService,
KubernetesIngressService,
KubernetesPersistentVolumeClaimService,
KubernetesNamespaceHelper,
KubernetesVolumeService
@ -56,6 +59,7 @@ class KubernetesCreateApplicationController {
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
@ -73,38 +77,25 @@ class KubernetesCreateApplicationController {
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
this.refreshStacksConfigsAppsAsync = this.refreshStacksConfigsAppsAsync.bind(this);
this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this);
this.getApplicationAsync = this.getApplicationAsync.bind(this);
}
isValid() {
return (
!this.state.alreadyExists &&
!this.state.hasDuplicateEnvironmentVariables &&
!this.state.hasDuplicatePersistedFolderPaths &&
!this.state.hasDuplicateConfigurationPaths &&
!this.state.hasDuplicateExistingVolumes
);
}
/* #endregion */
onChangeName() {
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
}
/**
* AUTO SCALER UI MANAGEMENT
*/
/* #region AUTO SCLAER UI MANAGEMENT */
unselectAutoScaler() {
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
this.formValues.AutoScaler.IsUsed = false;
}
}
/* #endregion */
/**
* CONFIGURATION UI MANAGEMENT
*/
/* #region CONFIGURATION UI MANAGEMENT */
addConfiguration() {
let config = new KubernetesApplicationConfigurationFormValue();
config.SelectedConfiguration = this.configurations[0];
@ -133,8 +124,12 @@ class KubernetesCreateApplicationController {
this.onChangeConfigurationPath();
}
clearConfigurations() {
this.formValues.Configurations = [];
}
onChangeConfigurationPath() {
this.state.duplicateConfigurationPaths = [];
this.state.duplicates.configurationPaths.refs = [];
const paths = _.reduce(
this.formValues.Configurations,
@ -151,33 +146,20 @@ class KubernetesCreateApplicationController {
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
if (findPath) {
this.state.duplicateConfigurationPaths[index + '_' + keyIndex] = findPath;
this.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] = findPath;
}
});
});
this.state.hasDuplicateConfigurationPaths = Object.keys(this.state.duplicateConfigurationPaths).length > 0;
this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
}
/**
* !CONFIGURATION UI MANAGEMENT
*/
/* #endregion */
/**
* ENVIRONMENT UI MANAGEMENT
*/
/* #region ENVIRONMENT UI MANAGEMENT */
addEnvironmentVariable() {
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
}
hasEnvironmentVariables() {
return this.formValues.EnvironmentVariables.length > 0;
}
onChangeEnvironmentName() {
this.state.duplicateEnvironmentVariables = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
this.state.hasDuplicateEnvironmentVariables = Object.keys(this.state.duplicateEnvironmentVariables).length > 0;
}
restoreEnvironmentVariable(index) {
this.formValues.EnvironmentVariables[index].NeedsDeletion = false;
}
@ -190,13 +172,14 @@ class KubernetesCreateApplicationController {
}
this.onChangeEnvironmentName();
}
/**
* !ENVIRONMENT UI MANAGEMENT
*/
/**
* PERSISTENT FOLDERS UI MANAGEMENT
*/
onChangeEnvironmentName() {
this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
}
/* #endregion */
/* #region PERSISTENT FOLDERS UI MANAGEMENT */
addPersistedFolder() {
let storageClass = {};
if (this.storageClasses.length > 0) {
@ -207,22 +190,17 @@ class KubernetesCreateApplicationController {
this.resetDeploymentType();
}
onChangePersistedFolderPath() {
this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
if (persistedFolder.NeedsDeletion) {
return undefined;
}
return persistedFolder.ContainerPath;
})
);
this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0;
}
restorePersistedFolder(index) {
this.formValues.PersistedFolders[index].NeedsDeletion = false;
}
resetPersistedFolders() {
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
persistedFolder.ExistingVolume = null;
persistedFolder.UseNewVolume = true;
});
}
removePersistedFolder(index) {
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
this.formValues.PersistedFolders[index].NeedsDeletion = true;
@ -233,10 +211,25 @@ class KubernetesCreateApplicationController {
this.onChangeExistingVolumeSelection();
}
onChangeExistingVolume(index) {
if (this.formValues.PersistedFolders[index].UseNewVolume) {
this.formValues.PersistedFolders[index].ExistingVolume = null;
}
onChangePersistedFolderPath() {
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
if (persistedFolder.NeedsDeletion) {
return undefined;
}
return persistedFolder.ContainerPath;
})
);
this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
}
onChangeExistingVolumeSelection() {
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
})
);
this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
}
useNewVolume(index) {
@ -253,46 +246,129 @@ class KubernetesCreateApplicationController {
this.resetDeploymentType();
}
}
/* #endregion */
onChangeExistingVolumeSelection() {
this.state.duplicateExistingVolumes = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
})
);
this.state.hasDuplicateExistingVolumes = Object.keys(this.state.duplicateExistingVolumes).length > 0;
}
filterAvailableVolumes() {
const filteredVolumes = _.filter(this.volumes, (volume) => {
const isSameNamespace = volume.ResourcePool.Namespace.Name === this.formValues.ResourcePool.Namespace.Name;
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
const isRWX = _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
return isSameNamespace && (isUnused || isRWX);
});
this.availableVolumes = filteredVolumes;
}
/**
* !PERSISTENT FOLDERS UI MANAGEMENT
*/
/**
* PUBLISHED PORTS UI MANAGEMENT
*/
/* #region PUBLISHED PORTS UI MANAGEMENT */
addPublishedPort() {
this.formValues.PublishedPorts.push(new KubernetesApplicationPublishedPortFormValue());
const p = new KubernetesApplicationPublishedPortFormValue();
const ingresses = this.filteredIngresses;
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
this.formValues.PublishedPorts.push(p);
}
resetPublishedPorts() {
const ingresses = this.filteredIngresses;
_.forEach(this.formValues.PublishedPorts, (p) => {
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
});
}
restorePublishedPort(index) {
this.formValues.PublishedPorts[index].NeedsDeletion = false;
this.onChangePublishedPorts();
}
removePublishedPort(index) {
this.formValues.PublishedPorts.splice(index, 1);
if (this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew) {
this.formValues.PublishedPorts[index].NeedsDeletion = true;
} else {
this.formValues.PublishedPorts.splice(index, 1);
}
this.onChangePublishedPorts();
}
/* #endregion */
/* #region PUBLISHED PORTS ON CHANGE VALIDATION */
onChangePublishedPorts() {
this.onChangePortMappingContainerPort();
this.onChangePortMappingNodePort();
this.onChangePortMappingIngressRoute();
this.onChangePortMappingLoadBalancer();
}
onChangePortMappingContainerPort() {
const state = this.state.duplicates.publishedPorts.containerPorts;
if (this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INGRESS) {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
}
}
onChangePortMappingNodePort() {
const state = this.state.duplicates.publishedPorts.nodePorts;
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
}
}
onChangePortMappingIngress(index) {
const publishedPort = this.formValues.PublishedPorts[index];
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
publishedPort.IngressHost = ingress.Host;
}
onChangePortMappingIngressRoute() {
const state = this.state.duplicates.publishedPorts.ingressRoutes;
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew ? p.IngressRoute : undefined));
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? p.IngressRoute : undefined));
const allRoutes = _.flatMapDeep(this.ingresses, (c) => _.map(c.Paths, 'Path'));
const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
_.forEach(newRoutes, (route, idx) => {
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
duplicates[idx] = route;
}
});
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
}
}
onChangePortMappingLoadBalancer() {
const state = this.state.duplicates.publishedPorts.loadBalancerPorts;
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
}
}
/* #endregion */
/* #region STATE VALIDATION FUNCTIONS */
isValid() {
return (
!this.state.alreadyExists &&
!this.state.duplicates.environmentVariables.hasDuplicates &&
!this.state.duplicates.persistedFolders.hasDuplicates &&
!this.state.duplicates.configurationPaths.hasDuplicates &&
!this.state.duplicates.existingVolumes.hasDuplicates &&
!this.state.duplicates.publishedPorts.containerPorts.hasDuplicates &&
!this.state.duplicates.publishedPorts.nodePorts.hasDuplicates &&
!this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates &&
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates
);
}
/**
* !PUBLISHED PORTS UI MANAGEMENT
*/
/**
* STATE VALIDATION FUNCTIONS
*/
storageClassAvailable() {
return this.storageClasses && this.storageClasses.length > 0;
}
@ -411,6 +487,10 @@ class KubernetesCreateApplicationController {
return this.state.useLoadBalancer;
}
publishViaIngressEnabled() {
return this.filteredIngresses.length;
}
isEditAndNoChangesMade() {
if (!this.state.isEdit) return false;
const changes = JsonPatch.compare(this.savedFormValues, this.formValues);
@ -422,6 +502,16 @@ class KubernetesCreateApplicationController {
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
}
isEditAndNotNewPublishedPort(index) {
return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew;
}
isNotInternalAndHasNoPublishedPorts() {
const toDelPorts = _.filter(this.formValues.PublishedPorts, { NeedsDeletion: true });
const toKeepPorts = _.without(this.formValues.PublishedPorts, ...toDelPorts);
return this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INTERNAL && toKeepPorts.length === 0;
}
isNonScalable() {
const scalable = this.supportScalableReplicaDeployment();
const global = this.supportGlobalDeployment();
@ -438,8 +528,8 @@ class KubernetesCreateApplicationController {
const invalid = !this.isValid();
const hasNoChanges = this.isEditAndNoChangesMade();
const nonScalable = this.isNonScalable();
const res = overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
return res;
const notInternalNoPorts = this.isNotInternalAndHasNoPublishedPorts();
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || notInternalNoPorts;
}
disableLoadBalancerEdit() {
@ -450,13 +540,19 @@ class KubernetesCreateApplicationController {
this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER
);
}
/**
* !STATE VALIDATION FUNCTIONS
*/
/**
* DATA AUTO REFRESH
*/
isPublishingTypeEditDisabled() {
const ports = _.filter(this.formValues.PublishedPorts, { IsNew: false, NeedsDeletion: false });
return this.state.isEdit && this.formValues.PublishedPorts.length > 0 && ports.length > 0;
}
isProtocolOptionDisabled(index, protocol) {
return this.disableLoadBalancerEdit() || (this.isEditAndNotNewPublishedPort(index) && this.formValues.PublishedPorts[index].Protocol !== protocol);
}
/* #endregion */
/* #region DATA AUTO REFRESH */
async updateSlidersAsync() {
try {
const quota = this.formValues.ResourcePool.Quota;
@ -546,33 +642,54 @@ class KubernetesCreateApplicationController {
return this.$async(this.refreshApplicationsAsync, namespace);
}
async refreshStacksConfigsAppsAsync(namespace) {
await Promise.all([this.refreshStacks(namespace), this.refreshConfigurations(namespace), this.refreshApplications(namespace)]);
refreshVolumes(namespace) {
const filteredVolumes = _.filter(this.volumes, (volume) => {
const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace;
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
return isSameNamespace && (isUnused || isRWX);
});
this.availableVolumes = filteredVolumes;
}
refreshIngresses(namespace) {
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
if (!this.publishViaIngressEnabled()) {
this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
}
this.formValues.OriginalIngresses = this.filteredIngresses;
}
async refreshNamespaceDataAsync(namespace) {
await Promise.all([
this.refreshStacks(namespace),
this.refreshConfigurations(namespace),
this.refreshApplications(namespace),
this.refreshIngresses(namespace),
this.refreshVolumes(namespace),
]);
this.onChangeName();
}
refreshStacksConfigsApps(namespace) {
return this.$async(this.refreshStacksConfigsAppsAsync, namespace);
refreshNamespaceData(namespace) {
return this.$async(this.refreshNamespaceDataAsync, namespace);
}
resetFormValues() {
this.clearConfigurations();
this.resetPersistedFolders();
this.resetPublishedPorts();
}
onResourcePoolSelectionChange() {
const namespace = this.formValues.ResourcePool.Namespace.Name;
this.updateSliders();
this.refreshStacksConfigsApps(namespace);
this.filterAvailableVolumes();
this.formValues.Configurations = [];
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
persistedFolder.ExistingVolume = null;
persistedFolder.UseNewVolume = true;
});
this.refreshNamespaceData(namespace);
this.resetFormValues();
}
/**
* !DATA AUTO REFRESH
*/
/* #endregion */
/**
* ACTIONS
*/
/* #region ACTIONS */
async deployApplicationAsync() {
this.state.actionInProgress = true;
try {
@ -595,7 +712,7 @@ class KubernetesCreateApplicationController {
this.Notifications.success('Application successfully updated');
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
this.Notifications.error('Failure', err, 'Unable to update application');
} finally {
this.state.actionInProgress = false;
}
@ -612,13 +729,9 @@ class KubernetesCreateApplicationController {
return this.$async(this.deployApplicationAsync);
}
}
/**
* !ACTIONS
*/
/* #endregion */
/**
* APPLICATION - used on edit context only
*/
/* #region APPLICATION - used on edit context only */
async getApplicationAsync() {
try {
const namespace = this.state.params.namespace;
@ -634,16 +747,16 @@ class KubernetesCreateApplicationController {
getApplication() {
return this.$async(this.getApplicationAsync);
}
/**
* !APPLICATION
*/
/* #endregion */
/* #region ON INIT */
async onInit() {
try {
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
canUseIngress: false,
sliders: {
cpu: {
min: 0,
@ -662,14 +775,42 @@ class KubernetesCreateApplicationController {
viewReady: false,
availableSizeUnits: ['MB', 'GB', 'TB'],
alreadyExists: false,
duplicateEnvironmentVariables: {},
hasDuplicateEnvironmentVariables: false,
duplicatePersistedFolderPaths: {},
hasDuplicatePersistedFolderPaths: false,
duplicateConfigurationPaths: {},
hasDuplicateConfigurationPaths: false,
duplicateExistingVolumes: {},
hasDuplicateExistingVolumes: false,
duplicates: {
environmentVariables: {
refs: {},
hasDuplicates: false,
},
persistedFolders: {
refs: {},
hasDuplicates: false,
},
configurationPaths: {
refs: {},
hasDuplicates: false,
},
existingVolumes: {
refs: {},
hasDuplicates: false,
},
publishedPorts: {
containerPorts: {
refs: {},
hasDuplicates: false,
},
nodePorts: {
refs: {},
hasDuplicates: false,
},
ingressRoutes: {
refs: {},
hasDuplicates: false,
},
loadBalancerPorts: {
refs: {},
hasDuplicates: false,
},
},
},
isEdit: false,
params: {
namespace: this.$transition$.params().namespace,
@ -694,20 +835,27 @@ class KubernetesCreateApplicationController {
this.formValues = new KubernetesApplicationFormValues();
const [resourcePools, nodes, volumes, applications] = await Promise.all([
const [resourcePools, nodes, ingresses] = await Promise.all([
this.KubernetesResourcePoolService.get(),
this.KubernetesNodeService.get(),
this.KubernetesVolumeService.get(),
this.KubernetesApplicationService.get(),
this.KubernetesIngressService.get(),
]);
this.ingresses = ingresses;
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues.ResourcePool = this.resourcePools[0];
this.volumes = volumes;
_.forEach(this.volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
});
this.filterAvailableVolumes();
// TODO: refactor @Max
// Don't pull all volumes and applications across all namespaces
// Use refreshNamespaceData flow (triggered on Init + on Namespace change)
// and query only accross the selected namespace
if (this.storageClassAvailable()) {
const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]);
this.volumes = volumes;
_.forEach(this.volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
});
}
_.forEach(nodes, (item) => {
this.state.nodes.memory += filesizeParser(item.Memory);
@ -715,11 +863,12 @@ class KubernetesCreateApplicationController {
});
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshStacksConfigsApps(namespace);
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
await this.getApplication();
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;
@ -734,6 +883,7 @@ class KubernetesCreateApplicationController {
}
} else {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
}
await this.updateSliders();
@ -747,6 +897,7 @@ class KubernetesCreateApplicationController {
$onInit() {
return this.$async(this.onInit);
}
/* #endregion */
}
export default KubernetesCreateApplicationController;