1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

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 detection
This commit is contained in:
Alice Groux 2021-04-27 19:51:13 +02:00 committed by GitHub
parent ca849e31a1
commit befccacc27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 308 additions and 106 deletions

View file

@ -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) ||

View file

@ -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;

View file

@ -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">

View file

@ -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 {

View file

@ -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>

View file

@ -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) {