mirror of https://github.com/portainer/portainer
feat(k8s/ingress): create multiple ingress network per kubernetes namespace (#4464)
* feat(k8s/ingress): introduce multiple hosts per ingress * feat(k8s/ingress): host selector in app create/edit * feat(k8s/ingress): save empty hosts * feat(k8s/ingress): fix empty host * feat(k8s/ingress): rename inputs + ensure hostnames unicity + fix remove hostname and routes * feat(k8s/ingress): fix duplicates hostname validation * feat(k8s/application): fix rebase * feat(k8s/resource-pool): fix error messages for ingress (wip) * fix(k8s/resource-pool): ingress duplicates detectionpull/5019/head
parent
ca849e31a1
commit
befccacc27
|
@ -132,4 +132,14 @@ angular
|
|||
};
|
||||
return values[text] || text;
|
||||
};
|
||||
})
|
||||
.filter('kubernetesApplicationIngressEmptyHostname', function () {
|
||||
'use strict';
|
||||
return function (value) {
|
||||
if (value === '') {
|
||||
return '<use IP>';
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: '',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1264,7 +1264,14 @@
|
|||
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
||||
uib-tooltip="Edition is not allowed while the Load Balancer is in 'Pending' state"
|
||||
>
|
||||
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
|
||||
<div
|
||||
class="input-group input-group-sm"
|
||||
ng-class="{
|
||||
striked: publishedPort.NeedsDeletion,
|
||||
'col-sm-2': ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS,
|
||||
'col-sm-3': ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INGRESS
|
||||
}"
|
||||
>
|
||||
<span class="input-group-addon">container port</span>
|
||||
<input
|
||||
type="number"
|
||||
|
@ -1327,7 +1334,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
class="col-sm-2 input-group input-group-sm"
|
||||
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||
ng-if="
|
||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||
|
@ -1349,7 +1356,28 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-3 input-group input-group-sm"
|
||||
class="col-sm-2 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">hostname</span>
|
||||
<select
|
||||
class="form-control"
|
||||
name="ingress_hostname_{{ $index }}"
|
||||
ng-model="publishedPort.IngressHost"
|
||||
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in ctrl.ingressHostnames"
|
||||
ng-change="ctrl.onChangePublishedPorts()"
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a hostname</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-sm-2 input-group input-group-sm"
|
||||
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||
ng-if="
|
||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||
|
|
|
@ -320,7 +320,7 @@ class KubernetesCreateApplicationController {
|
|||
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;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
if (this.formValues.PublishedPorts.length) {
|
||||
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
||||
}
|
||||
|
@ -331,7 +331,7 @@ class KubernetesCreateApplicationController {
|
|||
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;
|
||||
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -388,7 +388,8 @@ class KubernetesCreateApplicationController {
|
|||
onChangePortMappingIngress(index) {
|
||||
const publishedPort = this.formValues.PublishedPorts[index];
|
||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||
publishedPort.IngressHost = ingress.Host;
|
||||
this.ingressHostnames = ingress.Hosts;
|
||||
publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : [];
|
||||
this.onChangePublishedPorts();
|
||||
}
|
||||
|
||||
|
@ -780,6 +781,7 @@ class KubernetesCreateApplicationController {
|
|||
|
||||
refreshIngresses(namespace) {
|
||||
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||
this.ingressHostnames = this.filteredIngresses.length ? this.filteredIngresses[0].Hosts : [];
|
||||
if (!this.publishViaIngressEnabled()) {
|
||||
if (this.savedFormValues) {
|
||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||
|
|
|
@ -208,7 +208,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses">
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
||||
<div class="text-muted col-sm-12" style="width: 100%;">
|
||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
|
||||
|
@ -225,30 +225,58 @@
|
|||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<label class="control-label text-left col-sm-2">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostname associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via this hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" name="ingress_host_{{ $index }}" ng-model="ic.Host" placeholder="host.com" ng-change="ctrl.onChangeIngressHostname()" required />
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Hostnames
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostnames associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via one of these hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolCreationForm['ingress_host_' + $index].$invalid || ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolCreationForm['ingress_host_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<div ng-repeat="item in ic.Hosts track by $index" style="margin-top: 2px;">
|
||||
<div class="form-inline">
|
||||
<div class="col-sm-10 input-group input-group-sm">
|
||||
<span class="input-group-addon">Hostname</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
ng-model="item.Host"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
placeholder="foo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="
|
||||
resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
|
||||
ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
|
||||
"
|
||||
>
|
||||
<ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This hostname is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This host is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
|
|
|
@ -3,7 +3,11 @@ import _ from 'lodash-es';
|
|||
import filesizeParser from 'filesize-parser';
|
||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
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 KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
|
@ -34,17 +38,43 @@ class KubernetesCreateResourcePoolController {
|
|||
|
||||
onChangeIngressHostname() {
|
||||
const state = this.state.duplicates.ingressHosts;
|
||||
|
||||
const hosts = _.map(this.formValues.IngressClasses, 'Host');
|
||||
const allHosts = _.map(this.allIngresses, 'Host');
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(hosts);
|
||||
_.forEach(hosts, (host, idx) => {
|
||||
if (_.includes(allHosts, host) && host !== undefined) {
|
||||
duplicates[idx] = host;
|
||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||
const hostnames = _.map(hosts, 'Host');
|
||||
const hostnamesWithoutRemoved = _.filter(hostnames, (h) => !h.NeedsDeletion);
|
||||
const allHosts = _.flatMap(this.allIngresses, 'Hosts');
|
||||
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnamesWithoutRemoved);
|
||||
_.forEach(hostnames, (host, idx) => {
|
||||
if (host !== undefined && _.includes(allHosts, host)) {
|
||||
formDuplicates[idx] = host;
|
||||
}
|
||||
});
|
||||
const duplicates = {};
|
||||
let count = 0;
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
duplicates[ic.IngressClass.Name] = {};
|
||||
_.forEach(ic.Hosts, (hostFV, hostIdx) => {
|
||||
if (hostFV.Host === formDuplicates[count]) {
|
||||
duplicates[ic.IngressClass.Name][hostIdx] = hostFV.Host;
|
||||
}
|
||||
count++;
|
||||
});
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
state.hasRefs = false;
|
||||
_.forIn(duplicates, (value) => {
|
||||
if (Object.keys(value).length > 0) {
|
||||
state.hasRefs = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addHostname(ingressClass) {
|
||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
|
||||
removeHostname(ingressClass, index) {
|
||||
ingressClass.Hosts.splice(index, 1);
|
||||
this.onChangeIngressHostname();
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
|
@ -168,6 +198,11 @@ class KubernetesCreateResourcePoolController {
|
|||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
|
||||
}
|
||||
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||
if (ic.Hosts.length === 0) {
|
||||
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
} finally {
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses">
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses track by ic.IngressClass.Name">
|
||||
<div class="text-muted col-sm-12" style="width: 100%;">
|
||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
||||
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
|
||||
|
@ -184,34 +184,57 @@
|
|||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<label class="control-label text-left col-sm-2">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostname associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via this hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
class="form-control"
|
||||
name="ingress_host_{{ $index }}"
|
||||
ng-model="ic.Host"
|
||||
placeholder="host.com"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
required
|
||||
/>
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Hostnames
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Hostnames associated to the ingress inside this resource pool. Users will be able to expose and access their applications over the ingress via one of these hostname."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="resourcePoolEditForm['ingress_host_' + $index].$invalid || ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="resourcePoolEditForm['ingress_host_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<div ng-repeat="item in ic.Hosts track by $index" style="margin-top: 2px;">
|
||||
<div class="form-inline">
|
||||
<div class="col-sm-10 input-group input-group-sm" ng-class="{ striked: item.NeedsDeletion }">
|
||||
<span class="input-group-addon">Hostname</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
ng-model="item.Host"
|
||||
ng-change="ctrl.onChangeIngressHostname()"
|
||||
placeholder="foo"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
|
||||
<button ng-if="!item.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="item.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreHostname(item)">
|
||||
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning"
|
||||
style="margin-top: 5px;"
|
||||
ng-show="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid || item.Duplicate"
|
||||
>
|
||||
<ng-messages for="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
|
||||
</ng-messages>
|
||||
<p ng-if="item.Duplicate">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This hostname is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
This host is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue