diff --git a/api/kubernetes.go b/api/kubernetes.go index a363a0a63..45acea995 100644 --- a/api/kubernetes.go +++ b/api/kubernetes.go @@ -5,9 +5,8 @@ func KubernetesDefault() KubernetesData { Configuration: KubernetesConfiguration{ UseLoadBalancer: false, UseServerMetrics: false, - UseIngress: false, StorageClasses: []KubernetesStorageClassConfig{}, - IngressClasses: []string{}, + IngressClasses: []KubernetesIngressClassConfig{}, }, Snapshots: []KubernetesSnapshot{}, } diff --git a/api/portainer.go b/api/portainer.go index 8e03fb709..99ee05c25 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -332,9 +332,8 @@ type ( KubernetesConfiguration struct { UseLoadBalancer bool `json:"UseLoadBalancer"` UseServerMetrics bool `json:"UseServerMetrics"` - UseIngress bool `json:"UseIngress"` StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` - IngressClasses []string `json:"IngressClasses"` + IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration @@ -345,6 +344,12 @@ type ( AllowVolumeExpansion bool `json:"AllowVolumeExpansion"` } + // KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration + KubernetesIngressClassConfig struct { + Name string `json:"Name"` + Type string `json:"Type"` + } + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index af1fe6b38..56cfdf2bb 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -7,6 +7,7 @@ import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPersistedFolder, + KubernetesApplicationPort, KubernetesApplicationPublishingTypes, KubernetesApplicationTypes, KubernetesPortainerApplicationNameLabel, @@ -25,7 +26,6 @@ import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; import KubernetesServiceConverter from 'Kubernetes/converters/service'; import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; import PortainerError from 'Portainer/error'; -import { KubernetesApplicationPort } from 'Kubernetes/models/application/models'; import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper'; function _apiPortsToPublishedPorts(pList, pRefs) { @@ -270,9 +270,9 @@ class KubernetesApplicationConverter { const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length; if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; - } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && !isIngress) { + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) { res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER; - } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && isIngress) { + } else if (app.ServiceType === KubernetesServiceTypes.CLUSTER_IP && isIngress) { res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS; } else { res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js index 44755f04b..6376bf3d6 100644 --- a/app/kubernetes/converters/service.js +++ b/app/kubernetes/converters/service.js @@ -3,12 +3,12 @@ import * as JsonPatch from 'fast-json-patch'; import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; import { - KubernetesPortainerApplicationStackNameLabel, + KubernetesApplicationPublishingTypes, KubernetesPortainerApplicationNameLabel, KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationStackNameLabel, } from 'Kubernetes/models/application/models'; -import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; -import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; +import { KubernetesService, KubernetesServiceHeadlessClusterIP, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; function _publishedPortToServicePort(formValues, publishedPort, type) { @@ -42,7 +42,7 @@ class KubernetesServiceConverter { res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; - if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER || formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) { res.Type = KubernetesServiceTypes.NODE_PORT; } else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { res.Type = KubernetesServiceTypes.LOAD_BALANCER; diff --git a/app/kubernetes/ingress/constants.js b/app/kubernetes/ingress/constants.js index 22af57a87..bd7a5cfea 100644 --- a/app/kubernetes/ingress/constants.js +++ b/app/kubernetes/ingress/constants.js @@ -1,4 +1,13 @@ export const KubernetesIngressClassAnnotation = 'kubernetes.io/ingress.class'; -export const KubernetesIngressClassMandatoryAnnotations = Object.freeze({ - nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/$1' }, + +// keys must match KubernetesIngressClassTypes values to map them quickly using the ingress type +// KubernetesIngressClassRewriteTargetAnnotations[KubernetesIngressClassTypes.NGINX] for example +export const KubernetesIngressClassRewriteTargetAnnotations = Object.freeze({ + nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/' }, + traefik: { 'traefik.ingress.kubernetes.io/rewrite-target': '/' }, +}); + +export const KubernetesIngressClassTypes = Object.freeze({ + NGINX: 'nginx', + TRAEFIK: 'traefik', }); diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index 311d22e08..966a61ca6 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -2,9 +2,10 @@ import * as _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; -import { KubernetesIngressRule, KubernetesIngress } from './models'; +import { KubernetesResourcePoolIngressClassAnnotationFormValue, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues'; +import { KubernetesIngress, KubernetesIngressRule } from './models'; import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads'; -import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants'; +import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants'; export class KubernetesIngressConverter { // TODO: refactor @LP @@ -64,17 +65,68 @@ export class KubernetesIngressConverter { return ingresses; } + /** + * + * @param {KubernetesResourcePoolIngressClassFormValue} formValues + */ + static resourcePoolIngressClassFormValueToIngress(formValues) { + const res = new KubernetesIngress(); + res.Name = formValues.IngressClass.Name; + res.Namespace = formValues.Namespace; + const pairs = _.map(formValues.Annotations, (a) => [a.Key, a.Value]); + res.Annotations = _.fromPairs(pairs); + if (formValues.RewriteTarget) { + _.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]); + } + res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name; + res.Host = formValues.Host; + return res; + } + + /** + * + * @param {KubernetesIngressClass} ics Ingress classes (saved in Portainer DB) + * @param {KubernetesIngress} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and passed for RP EDIT VIEW + */ + static ingressClassesToFormValues(ics, ingresses) { + const res = _.map(ics, (ic) => { + const fv = new KubernetesResourcePoolIngressClassFormValue(); + fv.IngressClass = ic; + const ingress = _.find(ingresses, { Name: ic.Name }); + if (ingress) { + fv.Selected = true; + fv.WasSelected = true; + fv.Host = ingress.Host; + const [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]); + const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => { + if (key === rewriteKey) { + fv.RewriteTarget = true; + } else if (key !== KubernetesIngressClassAnnotation) { + const annotation = new KubernetesResourcePoolIngressClassAnnotationFormValue(); + annotation.Key = key; + annotation.Value = value; + return annotation; + } + }); + fv.Annotations = _.without(annotations, undefined); + fv.AdvancedConfig = fv.Annotations.length > 0; + } + return fv; + }); + return res; + } + static createPayload(data) { const res = new KubernetesIngressCreatePayload(); res.metadata.name = data.Name; res.metadata.namespace = data.Namespace; - res.metadata.annotations = data.Annotations || {}; - res.metadata.annotations[KubernetesIngressClassAnnotation] = data.IngressClassName; - const annotations = KubernetesIngressClassMandatoryAnnotations[data.Name]; - if (annotations) { - _.extend(res.metadata.annotations, annotations); - } + res.metadata.annotations = data.Annotations; if (data.Paths && data.Paths.length) { + _.forEach(data.Paths, (p) => { + if (p.Host === 'undefined' || p.Host === undefined) { + p.Host = ''; + } + }); const groups = _.groupBy(data.Paths, 'Host'); const rules = _.map(groups, (paths, host) => { const rule = new KubernetesIngressRuleCreatePayload(); diff --git a/app/kubernetes/ingress/models.js b/app/kubernetes/ingress/models.js index e7181a48f..ea6ccd1a8 100644 --- a/app/kubernetes/ingress/models.js +++ b/app/kubernetes/ingress/models.js @@ -24,3 +24,12 @@ export function KubernetesIngressRule() { Path: '', }; } + +export function KubernetesIngressClass() { + return { + Name: '', + Type: undefined, + NeedsDeletion: false, + IsNew: true, + }; +} diff --git a/app/kubernetes/ingress/service.js b/app/kubernetes/ingress/service.js index c31f30585..d5c97d527 100644 --- a/app/kubernetes/ingress/service.js +++ b/app/kubernetes/ingress/service.js @@ -55,10 +55,10 @@ class KubernetesIngressService { /** * CREATE */ - async createAsync(formValues) { + async createAsync(ingress) { try { const params = {}; - const payload = KubernetesIngressConverter.createPayload(formValues); + const payload = KubernetesIngressConverter.createPayload(ingress); const namespace = payload.metadata.namespace; const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise; return data; @@ -67,8 +67,8 @@ class KubernetesIngressService { } } - create(formValues) { - return this.$async(this.createAsync, formValues); + create(ingress) { + return this.$async(this.createAsync, ingress); } /** @@ -100,7 +100,7 @@ class KubernetesIngressService { async deleteAsync(ingress) { try { const params = new KubernetesCommonParams(); - params.id = ingress.Name; + params.id = ingress.IngressClass.Name; const namespace = ingress.Namespace; await this.KubernetesIngresses(namespace).delete(params).$promise; } catch (err) { diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 0fcedf6b1..1f1df1197 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -151,7 +151,7 @@ export class KubernetesApplicationAutoScalerFormValue { } } -export function KubernetesApplicationFormValidationDuplicate() { +export function KubernetesFormValueDuplicate() { return { refs: {}, hasDuplicates: false, diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index f59446cee..9d2277f5d 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -8,15 +8,24 @@ export function KubernetesResourcePoolFormValues(defaults) { } /** - * @param {string} ingressClassName + * @param {KubernetesIngressClass} ingressClass */ -export function KubernetesResourcePoolIngressClassFormValue(ingressClassName) { +export function KubernetesResourcePoolIngressClassFormValue(ingressClass) { return { - Name: ingressClassName, - IngressClassName: ingressClassName, + Namespace: undefined, // will be filled inside ResourcePoolService.create + IngressClass: ingressClass, + RewriteTarget: false, + Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue Host: undefined, Selected: false, WasSelected: false, - Namespace: undefined, // will be filled inside ResourcePoolService.create + AdvancedConfig: false, + }; +} + +export function KubernetesResourcePoolIngressClassAnnotationFormValue() { + return { + Key: '', + Value: '', }; } diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index 7de236f21..4320bf750 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -6,6 +6,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool' import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; class KubernetesResourcePoolService { /* @ngInject */ @@ -89,7 +90,8 @@ class KubernetesResourcePoolService { const ingressPromises = _.map(formValues.IngressClasses, (c) => { if (c.Selected) { c.Namespace = namespace.Name; - return this.KubernetesIngressService.create(c); + const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); + return this.KubernetesIngressService.create(ingress); } }); await Promise.all(ingressPromises); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 8e9f58fa7..78e7b2674 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1329,10 +1329,10 @@ class="form-control" name="ingress_route_{{ $index }}" ng-model="publishedPort.IngressRoute" - placeholder="foo" + placeholder="route" 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-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/" ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)" /> diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index b41711b41..01d250d04 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -20,7 +20,7 @@ import { KubernetesApplicationPersistedFolderFormValue, KubernetesApplicationPublishedPortFormValue, KubernetesApplicationPlacementFormValue, - KubernetesApplicationFormValidationDuplicate, + KubernetesFormValueDuplicate, } from 'Kubernetes/models/application/formValues'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import KubernetesApplicationConverter from 'Kubernetes/converters/application'; @@ -367,14 +367,16 @@ class KubernetesCreateApplicationController { const publishedPort = this.formValues.PublishedPorts[index]; const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName }); publishedPort.IngressHost = ingress.Host; + this.onChangePublishedPorts(); } 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 newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); + const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); + const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path)); const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes); _.forEach(newRoutes, (route, idx) => { if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) { @@ -814,7 +816,6 @@ class KubernetesCreateApplicationController { actionInProgress: false, useLoadBalancer: false, useServerMetrics: false, - canUseIngress: false, sliders: { cpu: { min: 0, @@ -834,17 +835,17 @@ class KubernetesCreateApplicationController { availableSizeUnits: ['MB', 'GB', 'TB'], alreadyExists: false, duplicates: { - environmentVariables: new KubernetesApplicationFormValidationDuplicate(), - persistedFolders: new KubernetesApplicationFormValidationDuplicate(), - configurationPaths: new KubernetesApplicationFormValidationDuplicate(), - existingVolumes: new KubernetesApplicationFormValidationDuplicate(), + environmentVariables: new KubernetesFormValueDuplicate(), + persistedFolders: new KubernetesFormValueDuplicate(), + configurationPaths: new KubernetesFormValueDuplicate(), + existingVolumes: new KubernetesFormValueDuplicate(), publishedPorts: { - containerPorts: new KubernetesApplicationFormValidationDuplicate(), - nodePorts: new KubernetesApplicationFormValidationDuplicate(), - ingressRoutes: new KubernetesApplicationFormValidationDuplicate(), - loadBalancerPorts: new KubernetesApplicationFormValidationDuplicate(), + containerPorts: new KubernetesFormValueDuplicate(), + nodePorts: new KubernetesFormValueDuplicate(), + ingressRoutes: new KubernetesFormValueDuplicate(), + loadBalancerPorts: new KubernetesFormValueDuplicate(), }, - placements: new KubernetesApplicationFormValidationDuplicate(), + placements: new KubernetesFormValueDuplicate(), }, isEdit: false, params: { diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index a19f2f015..197a3e5ea 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -247,7 +247,7 @@ -
+
This application is only available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }} @@ -261,6 +261,24 @@
+ +
+
+

+ + This application is available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }} + Copy + +

+

It can also be accessed via specific HTTP route(s).

+
+
+

Refer to the below port configuration to access the application.

+
+
+
diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 1c0be50fd..c784540ca 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -295,6 +295,10 @@ class KubernetesApplicationController { this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision }); } + this.state.useIngress = _.find(application.PublishedPorts, (p) => { + return this.portHasIngressRules(p); + }); + this.placements = computePlacements(nodes, this.application); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve application details'); @@ -322,6 +326,7 @@ class KubernetesApplicationController { }, eventWarningCount: 0, expandedNote: false, + useIngress: false, }; this.state.activeTab = this.LocalStorage.getActiveTab('application'); diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index bbd342b13..995055edf 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -33,42 +33,91 @@
- Enabling the ingress feature will allow users to expose application they deploy over a HTTP route.
+ Adding ingress controllers will allow users to expose application they deploy over a HTTP route.

- Ingress classes (controllers) must be manually specified for each one you want to use in the cluster. Make sure that each controller is running inside your - cluster. + Ingress classes must be manually specified for each controller you want to use in the cluster. Make sure that each controller is running inside your cluster.

- -
- - -
- -
- -
- +
+
+ + + add ingress controller +
-
-
-

This field is required.

+
+
+
+ Ingress class + +
+
+ Type + +
+
+ + +
+
+ +
+
+
+
+

Ingress class name is required.

+

This field must consist of lower case alphanumeric characters or '-', start with an + alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+

+ This ingress class is already defined. +

+
+
+
+
+
+

Ingress class type is required.

+
+
+
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index c5d17fef7..d9025000e 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -1,10 +1,15 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import angular from 'angular'; -import { KubernetesStorageClassAccessPolicies, KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; +import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models'; +import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues'; +import { KubernetesIngressClass } from 'Kubernetes/ingress/models'; +import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; +import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; class KubernetesConfigureController { + /* #region CONSTRUCTOR */ /* @ngInject */ - constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider) { + constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider, ModalService) { this.$async = $async; this.$state = $state; this.$stateParams = $stateParams; @@ -12,11 +17,16 @@ class KubernetesConfigureController { this.KubernetesStorageService = KubernetesStorageService; this.EndpointService = EndpointService; this.EndpointProvider = EndpointProvider; + this.ModalService = ModalService; + + this.IngressClassTypes = KubernetesIngressClassTypes; this.onInit = this.onInit.bind(this); this.configureAsync = this.configureAsync.bind(this); } + /* #endregion */ + /* #region STORAGE CLASSES UI MANAGEMENT */ storageClassAvailable() { return this.StorageClasses && this.StorageClasses.length > 0; } @@ -28,55 +38,99 @@ class KubernetesConfigureController { valid = false; } }); - return valid; } + /* #endregion */ + + /* #region INGRESS CLASSES UI MANAGEMENT */ + addIngressClass() { + this.formValues.IngressClasses.push(new KubernetesIngressClass()); + this.onChangeIngressClass(); + } + + restoreIngressClass(index) { + this.formValues.IngressClasses[index].NeedsDeletion = false; + this.onChangeIngressClass(); + } + + removeIngressClass(index) { + if (!this.formValues.IngressClasses[index].IsNew) { + this.formValues.IngressClasses[index].NeedsDeletion = true; + } else { + this.formValues.IngressClasses.splice(index, 1); + } + this.onChangeIngressClass(); + } + + onChangeIngressClass() { + const state = this.state.duplicates.ingressClasses; + const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name)); + const duplicates = KubernetesFormValidationHelper.getDuplicates(source); + state.refs = duplicates; + state.hasDuplicates = Object.keys(duplicates).length > 0; + } + + onChangeIngressClassName(index) { + const fv = this.formValues.IngressClasses[index]; + if (_.includes(fv.Name, KubernetesIngressClassTypes.NGINX)) { + fv.Type = KubernetesIngressClassTypes.NGINX; + } else if (_.includes(fv.Name, KubernetesIngressClassTypes.TRAEFIK)) { + fv.Type = KubernetesIngressClassTypes.TRAEFIK; + } + this.onChangeIngressClass(); + } + /* #endregion */ + + /* #region CONFIGURE */ + assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) { + endpoint.Kubernetes.Configuration.StorageClasses = storageClasses; + endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; + endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses; + } + + transformFormValues() { + const storageClasses = _.map(this.StorageClasses, (item) => { + if (item.selected) { + const res = new KubernetesStorageClass(); + res.Name = item.Name; + res.AccessModes = _.map(item.AccessModes, 'Name'); + res.Provisioner = item.Provisioner; + res.AllowVolumeExpansion = item.AllowVolumeExpansion; + return res; + } + }); + _.pull(storageClasses, undefined); + + const ingressClasses = _.without( + _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic)), + undefined + ); + _.pull(ingressClasses, undefined); + + return [storageClasses, ingressClasses]; + } async configureAsync() { try { this.state.actionInProgress = true; - const classes = _.without( - _.map(this.StorageClasses, (item) => { - if (item.selected) { - const res = new KubernetesStorageClass(); - res.Name = item.Name; - res.AccessModes = _.map(item.AccessModes, 'Name'); - res.Provisioner = item.Provisioner; - res.AllowVolumeExpansion = item.AllowVolumeExpansion; - return res; - } - }), - undefined - ); + const [storageClasses, ingressClasses] = this.transformFormValues(); - this.endpoint.Kubernetes.Configuration.StorageClasses = classes; - this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; - this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; - this.endpoint.Kubernetes.Configuration.UseIngress = this.formValues.UseIngress; - if (this.formValues.UseIngress) { - this.endpoint.Kubernetes.Configuration.IngressClasses = _.split(this.formValues.IngressClasses, ','); - } + this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses); await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint); - const storagePromises = _.map(classes, (storageClass) => { + const storagePromises = _.map(storageClasses, (storageClass) => { const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name }); if (oldStorageClass) { return this.KubernetesStorageService.patch(this.state.endpointId, oldStorageClass, storageClass); } }); - await Promise.all(storagePromises); const endpoints = this.EndpointProvider.endpoints(); const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id); if (modifiedEndpoint) { - modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes; - modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; - modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; - modifiedEndpoint.Kubernetes.Configuration.UseIngress = this.formValues.UseIngress; - if (this.formValues.UseIngress) { - modifiedEndpoint.Kubernetes.Configuration.IngressClasses = _.split(this.formValues.IngressClasses, ','); - } + this.assignFormValuesToEndpoint(modifiedEndpoint, storageClasses, ingressClasses); this.EndpointProvider.setEndpoints(endpoints); } this.Notifications.success('Configuration successfully applied'); @@ -89,22 +143,38 @@ class KubernetesConfigureController { } configure() { - return this.$async(this.configureAsync); + const toDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true }); + if (toDel.length) { + this.ModalService.confirmUpdate( + `Removing ingress controllers will make them unavailable for future use.
Existing resources linked to these ingress controllers will continue to live in cluster but you will not be able to remove them from Portainer.

Do you wish to continue?`, + (confirmed) => { + if (confirmed) { + return this.$async(this.configureAsync); + } + } + ); + } else { + return this.$async(this.configureAsync); + } } + /* #endregion */ + /* #region ON INIT */ async onInit() { this.state = { actionInProgress: false, displayConfigureClassPanel: {}, viewReady: false, endpointId: this.$stateParams.id, + duplicates: { + ingressClasses: new KubernetesFormValueDuplicate(), + }, }; this.formValues = { UseLoadBalancer: false, UseServerMetrics: false, - UseIngress: false, - IngressClasses: '', + IngressClasses: [], }; try { @@ -127,8 +197,11 @@ class KubernetesConfigureController { this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; - this.formValues.UseIngress = this.endpoint.Kubernetes.Configuration.UseIngress; - this.formValues.IngressClasses = _.join(this.endpoint.Kubernetes.Configuration.IngressClasses); + this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => { + ic.IsNew = false; + ic.NeedsDeletion = false; + return ic; + }); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration'); } finally { @@ -139,6 +212,7 @@ class KubernetesConfigureController { $onInit() { return this.$async(this.onInit); } + /* #endregion */ } export default KubernetesConfigureController; diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 460468cfa..08fc73cb2 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -129,54 +129,126 @@
-
- Ingresses -
- -
-
- The ingress feature must be enabled in the - endpoint configuration view to be able to register ingresses inside this - resource pool. +
+
+ Ingresses
-
-
-
-

- - You can enable one or multiple ingresses to be used when deploying an application inside this resource pool. -

+ +
+
+ The ingress feature must be enabled in the + endpoint configuration view to be able to register ingresses inside this + resource pool. +
-
-
- - - - - - - - - - -
Ingress controller - Hostname - - -
-
- - {{ class.Name }} -
-
- -
+ +
+
+

+ + Enable and configure ingresses available to users when deploying applications. +

+
+ +
+
+
+ {{ ic.IngressClass.Name }} +
+
+ +
+ + +
+
+ +
+
+ +
+ +
+
+
+
+

+ + This host is already used. +

+
+
+ +
+
+ + +
+
+
+
+ + +
+

+ + You can specify a list of annotations that will be associated to the ingress. +

+
+ +
+ + + add annotation + +
+ +
+
+
+ Key + +
+
+ Value + +
+
+ +
+
+
+
+
-
Actions @@ -187,7 +259,7 @@
-
+
Ingresses
-
+ +
The ingress feature must be enabled in the endpoint configuration view to be able to register ingresses inside this resource pool.
-
- - - - - - - - - - - -
Ingress controller - Hostname - - -
-
- - {{ class.Name }} -
-
- -
+ +
+
+

+ + Enable and configure ingresses available to users when deploying applications. +

+
+ +
+
+
+ {{ ic.IngressClass.Name }} +
+
+ +
+ + +
+
+ +
+
+ +
+ +
+
+
+
+

+ + This host is already used. +

+
+
+ +
+
+ + +
+
+
+
+ + +
+

+ + You can specify a list of annotations that will be associated to the ingress. +

+
+ +
+ + + add annotation + +
+ +
+
+
+ Key + +
+
+ Value + +
+
+ +
+
+
+
+
@@ -169,7 +249,7 @@