mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(k8s/ingresses): add more granularity to ingress configuration (#4220)
* feat(k8s/configure): separate ingress class name and ingress class type * feat(k8s/resource-pool): ability to add custom annotations to ingress classes on RP create/edit * feat(k8s/ingresses): remove 'allow users to use ingress' switch * feat(k8s/configure): minor UI update * feat(k8s/resource-pool): minor UI update * feat(k8s/application): update ingress route form validation * refactor(k8s/resource-pool): remove console.log statement * feat(k8s/resource-pool): update ingress annotation placeholders * feat(k8s/configure): add pattern form validation on ingress class * fix(k8s/resource-pool): automatically associate ingress class to ingress * fix(k8s/resource-pool): fix invalid ingress when updating a resource pool * fix(k8s/resource-pool): update ingress rewrite target annotation value * feat(k8s/application): ingress form validation * fix(k8s/application): squash ingress rules with empty host inside a single one * feat(k8s/resource-pool): ingress host validation * fix(k8s/resource-pool): rewrite rewrite option and only display it for ingress of type nginx * feat(k8s/application): do not expose ingress applications over node port * feat(k8s/application): add specific notice for ingress Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
parent
68851aada4
commit
d850e18ff0
21 changed files with 699 additions and 220 deletions
|
@ -120,45 +120,125 @@
|
|||
>
|
||||
</kubernetes-resource-reservation>
|
||||
</div>
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable">
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
</div>
|
||||
<div class="form-group" ng-if="!ctrl.state.canUseIngress">
|
||||
<!-- #region INGRESSES -->
|
||||
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length === 0">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
The ingress feature must be enabled in the
|
||||
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})">endpoint configuration view</a> to be able to register ingresses inside
|
||||
this resource pool.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-sm-12" ng-if="ctrl.state.canUseIngress">
|
||||
<table class="table" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%; border-top: 0px;">Ingress controller</td>
|
||||
<td style="width: 66%; border-top: 0px;">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Optional 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 or via IP address directly if not defined."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="class in ctrl.formValues.IngressClasses">
|
||||
<td style="width: 33%;">
|
||||
<div style="margin: 5px;">
|
||||
<label class="switch" style="margin-right: 10px;"> <input type="checkbox" ng-model="class.Selected" /><i></i> </label>
|
||||
{{ class.Name }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 66%;">
|
||||
<input class="form-control" ng-model="class.Host" placeholder="host.com" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.formValues.IngressClasses.length > 0">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Enable and configure ingresses available to users when deploying applications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-repeat-start="ic in ctrl.formValues.IngressClasses">
|
||||
<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 }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12" style="margin-top: 10px;">
|
||||
<label class="control-label text-left">
|
||||
Allow users to use this ingress
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ic.Selected" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<label class="control-label text-left col-sm-2">
|
||||
Hostname
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Optional 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 or via IP address directly if not defined."
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input class="form-control" ng-model="ic.Host" placeholder="host.com" ng-change="ctrl.onChangeIngressHostname()" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="ctrl.state.duplicates.ingressHosts.refs[$index] !== undefined">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<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 === 'nginx'">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Redirect published routes to / in application
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="Enables the redirection of any route published via ingress to the root path of the application, e.g. /path remaps to /"
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ic.RewriteTarget" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-repeat-end class="form-group" ng-if="ic.Selected" style="margin-bottom: 20px;">
|
||||
<div class="col-sm-12">
|
||||
<p>
|
||||
<a class="small interactive" ng-if="!ic.AdvancedConfig" ng-click="ic.AdvancedConfig = true">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i> Advanced configuration
|
||||
</a>
|
||||
<a class="small interactive" ng-if="ic.AdvancedConfig" ng-click="ic.AdvancedConfig = false">
|
||||
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide configuration
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 small text-muted" ng-if="ic.AdvancedConfig" style="margin-top: 5px;">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You can specify a list of annotations that will be associated to the ingress.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12" ng-if="ic.AdvancedConfig">
|
||||
<label class="control-label text-left">Annotations</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addAnnotation(ic)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add annotation
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="ic.AdvancedConfig">
|
||||
<div ng-repeat="annotation in ic.Annotations track by $index" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">Key</span>
|
||||
<input type="text" class="form-control" ng-model="annotation.Key" placeholder="nginx.ingress.kubernetes.io/rewrite-target" required />
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">Value</span>
|
||||
<input type="text" class="form-control" ng-model="annotation.Value" placeholder="/$1" required />
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm">
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeAnnotation(ic, $index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||
|
@ -169,7 +249,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.HasQuota && !ctrl.isQuotaValid())"
|
||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
|
||||
ng-click="ctrl.updateResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -4,9 +4,13 @@ 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, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
|
||||
class KubernetesResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$async,
|
||||
|
@ -52,11 +56,42 @@ class KubernetesResourcePoolController {
|
|||
this.getIngresses = this.getIngresses.bind(this);
|
||||
this.getIngressesAsync = this.getIngressesAsync.bind(this);
|
||||
}
|
||||
/* #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.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
addAnnotation(ingressClass) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
||||
}
|
||||
|
||||
removeAnnotation(ingressClass, index) {
|
||||
ingressClass.Annotations.splice(index, 1);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
selectTab(index) {
|
||||
this.LocalStorage.storeActiveTab('resourcePool', index);
|
||||
}
|
||||
|
||||
isUpdateButtonDisabled() {
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasDuplicates;
|
||||
}
|
||||
|
||||
isQuotaValid() {
|
||||
if (
|
||||
this.state.sliderMaxCpu < this.formValues.CpuLimit ||
|
||||
|
@ -126,17 +161,16 @@ class KubernetesResourcePoolController {
|
|||
|
||||
const promises = _.map(this.formValues.IngressClasses, (c) => {
|
||||
c.Namespace = namespace;
|
||||
const original = _.find(this.savedIngressClasses, { Name: c.Name });
|
||||
if (c.WasSelected === false && c.Selected === true) {
|
||||
return this.KubernetesIngressService.create(c);
|
||||
const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
return this.KubernetesIngressService.create(ingress);
|
||||
} else if (c.WasSelected === true && c.Selected === false) {
|
||||
return this.KubernetesIngressService.delete(c);
|
||||
} else if (c.Selected === true && original && original.Host !== c.Host) {
|
||||
const oldIngress = _.find(this.ingresses, { Name: c.Name });
|
||||
const newIngress = angular.copy(oldIngress);
|
||||
newIngress.PreviousHost = original.Host;
|
||||
newIngress.Host = c.Host;
|
||||
|
||||
} else if (c.WasSelected === true && c.Selected === true) {
|
||||
const oldIngress = _.find(this.ingresses, { Name: c.IngressClass.Name });
|
||||
const newIngress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
newIngress.Paths = angular.copy(oldIngress.Paths);
|
||||
newIngress.PreviousHost = oldIngress.Host;
|
||||
return this.KubernetesIngressService.patch(oldIngress, newIngress);
|
||||
}
|
||||
});
|
||||
|
@ -180,6 +214,7 @@ class KubernetesResourcePoolController {
|
|||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
/* #region GET EVENTS */
|
||||
async getEventsAsync() {
|
||||
try {
|
||||
this.state.eventsLoading = true;
|
||||
|
@ -195,7 +230,9 @@ class KubernetesResourcePoolController {
|
|||
getEvents() {
|
||||
return this.$async(this.getEventsAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET APPLICATIONS */
|
||||
async getApplicationsAsync() {
|
||||
try {
|
||||
this.state.applicationsLoading = true;
|
||||
|
@ -216,12 +253,15 @@ class KubernetesResourcePoolController {
|
|||
getApplications() {
|
||||
return this.$async(this.getApplicationsAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET INGRESSES */
|
||||
async getIngressesAsync() {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
const namespace = this.pool.Namespace.Name;
|
||||
this.ingresses = await this.KubernetesIngressService.get(namespace);
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
|
||||
_.forEach(this.ingresses, (ing) => {
|
||||
ing.Namespace = namespace;
|
||||
_.forEach(ing.Paths, (path) => {
|
||||
|
@ -239,7 +279,9 @@ class KubernetesResourcePoolController {
|
|||
getIngresses() {
|
||||
return this.$async(this.getIngressesAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
try {
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
|
@ -265,7 +307,10 @@ class KubernetesResourcePoolController {
|
|||
ingressesLoading: true,
|
||||
viewReady: false,
|
||||
eventWarningCount: 0,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.UseIngress,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
||||
|
@ -304,17 +349,7 @@ class KubernetesResourcePoolController {
|
|||
if (this.state.canUseIngress) {
|
||||
await this.getIngresses();
|
||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = _.map(ingressClasses, (item) => {
|
||||
const iClass = new KubernetesResourcePoolIngressClassFormValue(item);
|
||||
const matchingIngress = _.find(this.ingresses, { Name: iClass.Name });
|
||||
if (matchingIngress) {
|
||||
iClass.Selected = true;
|
||||
iClass.WasSelected = true;
|
||||
iClass.Host = matchingIngress.Host;
|
||||
}
|
||||
return iClass;
|
||||
});
|
||||
this.savedIngressClasses = angular.copy(this.formValues.IngressClasses);
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
|
@ -326,6 +361,7 @@ class KubernetesResourcePoolController {
|
|||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
$onDestroy() {
|
||||
if (this.state.currentName !== this.$state.$current.name) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue