diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index 0ce2d3831..88685f69e 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -132,4 +132,14 @@ angular }; return values[text] || text; }; + }) + .filter('kubernetesApplicationIngressEmptyHostname', function () { + 'use strict'; + return function (value) { + if (value === '') { + return ''; + } else { + return value; + } + }; }); diff --git a/app/kubernetes/helpers/formValidationHelper.js b/app/kubernetes/helpers/formValidationHelper.js index 7ac2897f4..5b3cc8989 100644 --- a/app/kubernetes/helpers/formValidationHelper.js +++ b/app/kubernetes/helpers/formValidationHelper.js @@ -16,7 +16,7 @@ class KubernetesFormValidationHelper { const groupped = _.groupBy(names); const res = {}; _.forEach(names, (name, index) => { - if (groupped[name].length > 1 && name) { + if (name && groupped[name].length > 1) { res[index] = name; } }); diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index c1e9915a6..5651e29da 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -2,22 +2,18 @@ import _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; -import { KubernetesResourcePoolIngressClassAnnotationFormValue, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues'; +import { + KubernetesResourcePoolIngressClassAnnotationFormValue, + KubernetesResourcePoolIngressClassFormValue, + KubernetesResourcePoolIngressClassHostFormValue, +} from 'Kubernetes/models/resource-pool/formValues'; import { KubernetesIngress, KubernetesIngressRule } from './models'; import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads'; import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants'; export class KubernetesIngressConverter { - // TODO: refactor @LP - // currently only allows the first non-empty host to be used as the "configured" host. - // As we currently only allow a single host to be used for a Portianer-managed ingress - // it's working as the only non-empty host will be the one defined by the admin - // but it will take a random existing host for non Portainer ingresses (CLI deployed) - // Also won't support multiple hosts if we make it available in the future static apiToModel(data) { - let host = undefined; const paths = _.flatMap(data.spec.rules, (rule) => { - host = host || rule.host; // TODO: refactor @LP - read above return !rule.http ? [] : _.map(rule.http.paths, (path) => { @@ -41,7 +37,11 @@ export class KubernetesIngressConverter { ? data.metadata.annotations[KubernetesIngressClassAnnotation] : data.spec.ingressClassName; res.Paths = paths; - res.Host = host; + res.Hosts = _.uniq(_.map(data.spec.rules, 'host')); + const idx = _.findIndex(res.Hosts, (h) => h === undefined); + if (idx >= 0) { + res.Hosts.splice(idx, 1, ''); + } return res; } @@ -79,7 +79,7 @@ export class KubernetesIngressConverter { _.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]); } res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name; - res.Host = formValues.Host; + res.Hosts = formValues.Hosts; res.Paths = formValues.Paths; return res; } @@ -96,7 +96,13 @@ export class KubernetesIngressConverter { if (ingress) { fv.Selected = true; fv.WasSelected = true; - fv.Host = ingress.Host; + fv.Hosts = _.map(ingress.Hosts, (host) => { + const hfv = new KubernetesResourcePoolIngressClassHostFormValue(); + hfv.Host = host; + hfv.PreviousHost = host; + hfv.IsNew = false; + return hfv; + }); const [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]); const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => { if (key === rewriteKey) { @@ -128,16 +134,17 @@ export class KubernetesIngressConverter { p.Host = ''; } }); + const hostsWithRules = []; const groups = _.groupBy(data.Paths, 'Host'); - const rules = _.map(groups, (paths, host) => { + let rules = _.map(groups, (paths, host) => { + const updatedHost = _.find(data.Hosts, (h) => { + return h === host || h.PreviousHost === host; + }); + host = updatedHost.Host || updatedHost; + if (updatedHost.NeedsDeletion) { + return; + } const rule = new KubernetesIngressRuleCreatePayload(); - - if (host === 'undefined' || _.isEmpty(host)) { - host = data.Host; - } - if (host === data.PreviousHost && host !== data.Host) { - host = data.Host; - } KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host); rule.http.paths = _.map(paths, (p) => { const path = new KubernetesIngressRulePathCreatePayload(); @@ -146,11 +153,27 @@ export class KubernetesIngressConverter { path.backend.servicePort = p.Port; return path; }); + hostsWithRules.push(host); return rule; }); + rules = _.without(rules, undefined); + const keptHosts = _.without( + _.map(data.Hosts, (h) => (h.NeedsDeletion ? undefined : h.Host || h)), + undefined + ); + const hostsWithoutRules = _.without(keptHosts, ...hostsWithRules); + const emptyRules = _.map(hostsWithoutRules, (host) => { + return { host: host }; + }); + rules = _.concat(rules, emptyRules); KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules); - } else if (data.Host) { - res.spec.rules = [{ host: data.Host }]; + } else if (data.Hosts) { + res.spec.rules = []; + _.forEach(data.Hosts, (host) => { + if (!host.NeedsDeletion) { + res.spec.rules.push({ host: host.Host }); + } + }); } else { delete res.spec.rules; } diff --git a/app/kubernetes/ingress/models.js b/app/kubernetes/ingress/models.js index ea6ccd1a8..4c8aadd2c 100644 --- a/app/kubernetes/ingress/models.js +++ b/app/kubernetes/ingress/models.js @@ -3,8 +3,9 @@ export function KubernetesIngress() { Name: '', Namespace: '', Annotations: {}, - Host: undefined, - PreviousHost: undefined, // only use for RP ingress host edit + // Host: undefined, + Hosts: [], + // PreviousHost: undefined, // only use for RP ingress host edit Paths: [], IngressClassName: '', }; diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index 4238086d6..bb3f598c0 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -17,7 +17,7 @@ export function KubernetesResourcePoolIngressClassFormValue(ingressClass) { IngressClass: ingressClass, RewriteTarget: false, Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue - Host: undefined, + Hosts: [], Selected: false, WasSelected: false, AdvancedConfig: false, @@ -31,3 +31,12 @@ export function KubernetesResourcePoolIngressClassAnnotationFormValue() { Value: '', }; } + +export function KubernetesResourcePoolIngressClassHostFormValue() { + return { + Host: '', + PreviousHost: '', + NeedsDeletion: false, + IsNew: true, + }; +} diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 5d2818c8d..106b2454d 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1264,7 +1264,14 @@ tooltip-enable="ctrl.disableLoadBalancerEdit()" uib-tooltip="Edition is not allowed while the Load Balancer is in 'Pending' state" > -
+
container port
+ hostname + +
+ +
+
{{ ic.IngressClass.Name }} @@ -225,30 +225,58 @@
- -
- +
+ + + add hostname +
-
-
-
-
-

This field is required.

+
+
+
+
+ Hostname + +
+
+ +
+
+
+ +

Hostname is required.

+
+

+ This hostname is already used. +

+
-

- - This host is already used. -

-
-
+
{{ ic.IngressClass.Name }} @@ -184,34 +184,57 @@
- -
- +
+ + + add hostname +
-
-
-
-
-

This field is required.

+
+
+
+
+ Hostname + +
+
+ + +
+
+
+ +

Hostname is required.

+
+

+ + This hostname is already used. +

+
-

- - This host is already used. -

diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index b91d337ff..051dc1fc9 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -4,7 +4,11 @@ import filesizeParser from 'filesize-parser'; import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; -import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues'; +import { + KubernetesResourcePoolFormValues, + KubernetesResourcePoolIngressClassAnnotationFormValue, + KubernetesResourcePoolIngressClassHostFormValue, +} from 'Kubernetes/models/resource-pool/formValues'; import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; @@ -62,22 +66,6 @@ class KubernetesResourcePoolController { } /* #endregion */ - onChangeIngressHostname() { - const state = this.state.duplicates.ingressHosts; - - const hosts = _.map(this.formValues.IngressClasses, 'Host'); - const otherIngresses = _.without(this.allIngresses, ...this.ingresses); - const allHosts = _.map(otherIngresses, 'Host'); - const duplicates = KubernetesFormValidationHelper.getDuplicates(hosts); - _.forEach(hosts, (host, idx) => { - if (_.includes(allHosts, host) && host !== undefined) { - duplicates[idx] = host; - } - }); - state.refs = duplicates; - state.hasRefs = Object.keys(duplicates).length > 0; - } - /* #region ANNOTATIONS MANAGEMENT */ addAnnotation(ingressClass) { ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue()); @@ -85,9 +73,59 @@ class KubernetesResourcePoolController { removeAnnotation(ingressClass, index) { ingressClass.Annotations.splice(index, 1); + this.onChangeIngressHostname(); } /* #endregion */ + /* #region INGRESS MANAGEMENT */ + onChangeIngressHostname() { + const state = this.state.duplicates.ingressHosts; + const otherIngresses = _.without(this.allIngresses, ...this.ingresses); + const allHosts = _.flatMap(otherIngresses, 'Hosts'); + + const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts'); + const hostsWithoutRemoved = _.filter(hosts, { NeedsDeletion: false }); + const hostnames = _.map(hostsWithoutRemoved, 'Host'); + const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnames); + _.forEach(hostnames, (host, idx) => { + if (host !== undefined && _.includes(allHosts, host)) { + formDuplicates[idx] = host; + } + }); + const duplicatedHostnames = Object.values(formDuplicates); + state.hasRefs = false; + _.forEach(this.formValues.IngressClasses, (ic) => { + _.forEach(ic.Hosts, (hostFV) => { + if (_.includes(duplicatedHostnames, hostFV.Host) && hostFV.NeedsDeletion === false) { + hostFV.Duplicate = true; + state.hasRefs = true; + } else { + hostFV.Duplicate = false; + } + }); + }); + } + + addHostname(ingressClass) { + ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); + } + + removeHostname(ingressClass, index) { + if (!ingressClass.Hosts[index].IsNew) { + ingressClass.Hosts[index].NeedsDeletion = true; + } else { + ingressClass.Hosts.splice(index, 1); + } + this.onChangeIngressHostname(); + } + + restoreHostname(host) { + if (!host.IsNew) { + host.NeedsDeletion = false; + } + } + /* #endregion*/ + selectTab(index) { this.LocalStorage.storeActiveTab('resourcePool', index); } @@ -312,6 +350,11 @@ class KubernetesResourcePoolController { await this.getIngresses(); const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses); + _.forEach(this.formValues.IngressClasses, (ic) => { + if (ic.Hosts.length === 0) { + ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue()); + } + }); } this.savedFormValues = angular.copy(this.formValues); } catch (err) {