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;
|
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 groupped = _.groupBy(names);
|
||||||
const res = {};
|
const res = {};
|
||||||
_.forEach(names, (name, index) => {
|
_.forEach(names, (name, index) => {
|
||||||
if (groupped[name].length > 1 && name) {
|
if (name && groupped[name].length > 1) {
|
||||||
res[index] = name;
|
res[index] = name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,22 +2,18 @@ import _ from 'lodash-es';
|
||||||
import * as JsonPatch from 'fast-json-patch';
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
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 { KubernetesIngress, KubernetesIngressRule } from './models';
|
||||||
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||||
import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants';
|
import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants';
|
||||||
|
|
||||||
export class KubernetesIngressConverter {
|
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) {
|
static apiToModel(data) {
|
||||||
let host = undefined;
|
|
||||||
const paths = _.flatMap(data.spec.rules, (rule) => {
|
const paths = _.flatMap(data.spec.rules, (rule) => {
|
||||||
host = host || rule.host; // TODO: refactor @LP - read above
|
|
||||||
return !rule.http
|
return !rule.http
|
||||||
? []
|
? []
|
||||||
: _.map(rule.http.paths, (path) => {
|
: _.map(rule.http.paths, (path) => {
|
||||||
|
@ -41,7 +37,11 @@ export class KubernetesIngressConverter {
|
||||||
? data.metadata.annotations[KubernetesIngressClassAnnotation]
|
? data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||||
: data.spec.ingressClassName;
|
: data.spec.ingressClassName;
|
||||||
res.Paths = paths;
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ export class KubernetesIngressConverter {
|
||||||
_.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]);
|
_.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]);
|
||||||
}
|
}
|
||||||
res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name;
|
res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name;
|
||||||
res.Host = formValues.Host;
|
res.Hosts = formValues.Hosts;
|
||||||
res.Paths = formValues.Paths;
|
res.Paths = formValues.Paths;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,13 @@ export class KubernetesIngressConverter {
|
||||||
if (ingress) {
|
if (ingress) {
|
||||||
fv.Selected = true;
|
fv.Selected = true;
|
||||||
fv.WasSelected = 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 [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]);
|
||||||
const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => {
|
const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => {
|
||||||
if (key === rewriteKey) {
|
if (key === rewriteKey) {
|
||||||
|
@ -128,16 +134,17 @@ export class KubernetesIngressConverter {
|
||||||
p.Host = '';
|
p.Host = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const hostsWithRules = [];
|
||||||
const groups = _.groupBy(data.Paths, 'Host');
|
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();
|
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);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host);
|
||||||
rule.http.paths = _.map(paths, (p) => {
|
rule.http.paths = _.map(paths, (p) => {
|
||||||
const path = new KubernetesIngressRulePathCreatePayload();
|
const path = new KubernetesIngressRulePathCreatePayload();
|
||||||
|
@ -146,11 +153,27 @@ export class KubernetesIngressConverter {
|
||||||
path.backend.servicePort = p.Port;
|
path.backend.servicePort = p.Port;
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
hostsWithRules.push(host);
|
||||||
return rule;
|
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);
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules);
|
||||||
} else if (data.Host) {
|
} else if (data.Hosts) {
|
||||||
res.spec.rules = [{ host: data.Host }];
|
res.spec.rules = [];
|
||||||
|
_.forEach(data.Hosts, (host) => {
|
||||||
|
if (!host.NeedsDeletion) {
|
||||||
|
res.spec.rules.push({ host: host.Host });
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
delete res.spec.rules;
|
delete res.spec.rules;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ export function KubernetesIngress() {
|
||||||
Name: '',
|
Name: '',
|
||||||
Namespace: '',
|
Namespace: '',
|
||||||
Annotations: {},
|
Annotations: {},
|
||||||
Host: undefined,
|
// Host: undefined,
|
||||||
PreviousHost: undefined, // only use for RP ingress host edit
|
Hosts: [],
|
||||||
|
// PreviousHost: undefined, // only use for RP ingress host edit
|
||||||
Paths: [],
|
Paths: [],
|
||||||
IngressClassName: '',
|
IngressClassName: '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ export function KubernetesResourcePoolIngressClassFormValue(ingressClass) {
|
||||||
IngressClass: ingressClass,
|
IngressClass: ingressClass,
|
||||||
RewriteTarget: false,
|
RewriteTarget: false,
|
||||||
Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue
|
Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue
|
||||||
Host: undefined,
|
Hosts: [],
|
||||||
Selected: false,
|
Selected: false,
|
||||||
WasSelected: false,
|
WasSelected: false,
|
||||||
AdvancedConfig: false,
|
AdvancedConfig: false,
|
||||||
|
@ -31,3 +31,12 @@ export function KubernetesResourcePoolIngressClassAnnotationFormValue() {
|
||||||
Value: '',
|
Value: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function KubernetesResourcePoolIngressClassHostFormValue() {
|
||||||
|
return {
|
||||||
|
Host: '',
|
||||||
|
PreviousHost: '',
|
||||||
|
NeedsDeletion: false,
|
||||||
|
IsNew: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1264,7 +1264,14 @@
|
||||||
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
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-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>
|
<span class="input-group-addon">container port</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -1327,7 +1334,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
ng-if="
|
ng-if="
|
||||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||||
|
@ -1349,7 +1356,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
ng-if="
|
ng-if="
|
||||||
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||||
|
|
|
@ -320,7 +320,7 @@ class KubernetesCreateApplicationController {
|
||||||
const p = new KubernetesApplicationPublishedPortFormValue();
|
const p = new KubernetesApplicationPublishedPortFormValue();
|
||||||
const ingresses = this.filteredIngresses;
|
const ingresses = this.filteredIngresses;
|
||||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
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) {
|
if (this.formValues.PublishedPorts.length) {
|
||||||
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
p.Protocol = this.formValues.PublishedPorts[0].Protocol;
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,7 @@ class KubernetesCreateApplicationController {
|
||||||
const ingresses = this.filteredIngresses;
|
const ingresses = this.filteredIngresses;
|
||||||
_.forEach(this.formValues.PublishedPorts, (p) => {
|
_.forEach(this.formValues.PublishedPorts, (p) => {
|
||||||
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
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) {
|
onChangePortMappingIngress(index) {
|
||||||
const publishedPort = this.formValues.PublishedPorts[index];
|
const publishedPort = this.formValues.PublishedPorts[index];
|
||||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
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();
|
this.onChangePublishedPorts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -780,6 +781,7 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
refreshIngresses(namespace) {
|
refreshIngresses(namespace) {
|
||||||
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||||
|
this.ingressHostnames = this.filteredIngresses.length ? this.filteredIngresses[0].Hosts : [];
|
||||||
if (!this.publishViaIngressEnabled()) {
|
if (!this.publishViaIngressEnabled()) {
|
||||||
if (this.savedFormValues) {
|
if (this.savedFormValues) {
|
||||||
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
this.formValues.PublishingType = this.savedFormValues.PublishingType;
|
||||||
|
|
|
@ -208,7 +208,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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 class="text-muted col-sm-12" style="width: 100%;">
|
||||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
<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 }}
|
<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 ng-if="ic.Selected">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label text-left col-sm-2">
|
<div class="col-sm-12">
|
||||||
Hostname
|
<label class="control-label text-left">
|
||||||
|
Hostnames
|
||||||
<portainer-tooltip
|
<portainer-tooltip
|
||||||
position="bottom"
|
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."
|
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>
|
</portainer-tooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addHostname(ic)">
|
||||||
<input class="form-control" name="ingress_host_{{ $index }}" ng-model="ic.Host" placeholder="host.com" ng-change="ctrl.onChangeIngressHostname()" required />
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="form-group" ng-show="resourcePoolCreationForm['ingress_host_' + $index].$invalid || ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
<div
|
||||||
<div class="col-sm-12 small text-warning">
|
class="small text-warning"
|
||||||
<div ng-messages="resourcePoolCreationForm['ingress_host_' + $index].$error">
|
style="margin-top: 5px;"
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
ng-show="
|
||||||
</div>
|
resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$invalid ||
|
||||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
"
|
||||||
This host is already used.
|
>
|
||||||
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">
|
<label class="control-label text-left">
|
||||||
|
|
|
@ -3,7 +3,11 @@ import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
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 { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
|
@ -34,17 +38,43 @@ class KubernetesCreateResourcePoolController {
|
||||||
|
|
||||||
onChangeIngressHostname() {
|
onChangeIngressHostname() {
|
||||||
const state = this.state.duplicates.ingressHosts;
|
const state = this.state.duplicates.ingressHosts;
|
||||||
|
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||||
const hosts = _.map(this.formValues.IngressClasses, 'Host');
|
const hostnames = _.map(hosts, 'Host');
|
||||||
const allHosts = _.map(this.allIngresses, 'Host');
|
const hostnamesWithoutRemoved = _.filter(hostnames, (h) => !h.NeedsDeletion);
|
||||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(hosts);
|
const allHosts = _.flatMap(this.allIngresses, 'Hosts');
|
||||||
_.forEach(hosts, (host, idx) => {
|
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnamesWithoutRemoved);
|
||||||
if (_.includes(allHosts, host) && host !== undefined) {
|
_.forEach(hostnames, (host, idx) => {
|
||||||
duplicates[idx] = host;
|
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.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 */
|
/* #region ANNOTATIONS MANAGEMENT */
|
||||||
|
@ -168,6 +198,11 @@ class KubernetesCreateResourcePoolController {
|
||||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
|
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
|
||||||
}
|
}
|
||||||
|
_.forEach(this.formValues.IngressClasses, (ic) => {
|
||||||
|
if (ic.Hosts.length === 0) {
|
||||||
|
ic.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -167,7 +167,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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 class="text-muted col-sm-12" style="width: 100%;">
|
||||||
<div style="border-bottom: 1px solid #cdcdcd; padding-bottom: 5px;">
|
<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 }}
|
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i> {{ ic.IngressClass.Name }}
|
||||||
|
@ -184,36 +184,59 @@
|
||||||
|
|
||||||
<div ng-if="ic.Selected">
|
<div ng-if="ic.Selected">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label text-left col-sm-2">
|
<div class="col-sm-12">
|
||||||
Hostname
|
<label class="control-label text-left">
|
||||||
|
Hostnames
|
||||||
<portainer-tooltip
|
<portainer-tooltip
|
||||||
position="bottom"
|
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."
|
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>
|
</portainer-tooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<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 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
|
<input
|
||||||
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="ingress_host_{{ $index }}"
|
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||||
ng-model="ic.Host"
|
ng-model="item.Host"
|
||||||
placeholder="host.com"
|
|
||||||
ng-change="ctrl.onChangeIngressHostname()"
|
ng-change="ctrl.onChangeIngressHostname()"
|
||||||
|
placeholder="foo"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="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>
|
</div>
|
||||||
<p ng-if="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
<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>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
This host is already used.
|
This hostname is already used.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
<div class="form-group" ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -4,7 +4,11 @@ import filesizeParser from 'filesize-parser';
|
||||||
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
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 { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
|
@ -62,22 +66,6 @@ class KubernetesResourcePoolController {
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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 */
|
/* #region ANNOTATIONS MANAGEMENT */
|
||||||
addAnnotation(ingressClass) {
|
addAnnotation(ingressClass) {
|
||||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
||||||
|
@ -85,9 +73,59 @@ class KubernetesResourcePoolController {
|
||||||
|
|
||||||
removeAnnotation(ingressClass, index) {
|
removeAnnotation(ingressClass, index) {
|
||||||
ingressClass.Annotations.splice(index, 1);
|
ingressClass.Annotations.splice(index, 1);
|
||||||
|
this.onChangeIngressHostname();
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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) {
|
selectTab(index) {
|
||||||
this.LocalStorage.storeActiveTab('resourcePool', index);
|
this.LocalStorage.storeActiveTab('resourcePool', index);
|
||||||
}
|
}
|
||||||
|
@ -312,6 +350,11 @@ class KubernetesResourcePoolController {
|
||||||
await this.getIngresses();
|
await this.getIngresses();
|
||||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
|
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);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
Loading…
Reference in New Issue