mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
feat(ingress): autodetect ingress controllers EE-673 (#7712)
This commit is contained in:
parent
c96551e410
commit
89eda13eb3
48 changed files with 1252 additions and 1047 deletions
|
@ -180,214 +180,17 @@
|
|||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region STORAGES -->
|
||||
<div class="col-sm-12 form-section-title"> Storage </div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
||||
prevent the usage of a specific storage option inside this namespace.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title vertical-center">
|
||||
<pr-icon icon="'svg-route'"></pr-icon>
|
||||
standard
|
||||
</div>
|
||||
|
||||
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
||||
|
||||
<!-- #endregion -->
|
||||
|
||||
<div ng-if="$ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title"> Ingresses </div>
|
||||
<div ng-if="$ctrl.state.ingressAvailabilityPerNamespace">
|
||||
<!-- #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="kubernetes.cluster.setup">environment configuration view</a> to be able to register ingresses inside this namespace.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length > 0">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
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 track by ic.IngressClass.Name">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<span class="text-muted vertical-center"><pr-icon icon="'svg-route'"></pr-icon> {{ ic.IngressClass.Name }}</span>
|
||||
<hr class="mt-2 mb-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 col-lg-2">
|
||||
<label class="control-label text-left"> Allow users to use this ingress </label>
|
||||
</div>
|
||||
<div class="col-sm-8 pt-2">
|
||||
<label class="switch">
|
||||
<input type="checkbox" ng-model="ic.Selected" /><span class="slider round" data-cy="namespaceCreate-ingressToggle{{ ic.IngressClass.Name }}"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ic.Selected">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Hostnames
|
||||
<portainer-tooltip
|
||||
message="'Hostnames associated to the ingress inside this namespace. 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" ng-click="$ctrl.addHostname(ic)" data-cy="namespaceCreate-addHostButton{{ ic.IngressClass.Name }}">
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add hostname
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 pt-4">
|
||||
<div ng-repeat="item in ic.Hosts track by $index">
|
||||
<div class="form-inline">
|
||||
<div class="col-sm-8 input-group input-group-sm pt-2">
|
||||
<span class="input-group-addon required">Hostname</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="hostname_{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
ng-model="item.Host"
|
||||
ng-change="$ctrl.onChangeIngressHostname()"
|
||||
placeholder="foo"
|
||||
pattern="[\*a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
|
||||
required
|
||||
data-cy="namespaceCreate-hostnameInput{{ ic.IngressClass.Name }}_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm !pt-2" ng-if="$index > 0">
|
||||
<button class="btn btn-md btn-dangerlight btn-only-icon" type="button" ng-click="$ctrl.removeHostname(ic, $index)">
|
||||
<pr-icon icon="'trash-2'" size="'md'" feather="true"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="small text-warning pt-1"
|
||||
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 class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> Hostname is required.</p>
|
||||
<p class="vertical-center" ng-message="pattern">
|
||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
||||
This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com').
|
||||
</p>
|
||||
</ng-messages>
|
||||
<p class="vertical-center" ng-if="$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
|
||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon> This hostname is already used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-repeat-end class="form-group" ng-if="ic.Selected">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
<p class="vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
You can specify a list of annotations that will be associated to the ingress.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Annotations </label>
|
||||
<label class="control-label text-left">
|
||||
<span class="label label-default interactive" ng-click="$ctrl.addAnnotation(ic)" data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}">
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add annotation
|
||||
</span>
|
||||
<portainer-tooltip
|
||||
message="'Use annotations to configure options for an ingress. Review Nginx or Traefik documentation to find the annotations supported by your choice of ingress type.'"
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<label class="control-label text-left">
|
||||
<span
|
||||
class="label label-default interactive"
|
||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
||||
ng-click="$ctrl.addRewriteAnnotation(ic)"
|
||||
data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}"
|
||||
>
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add rewrite annotation
|
||||
</span>
|
||||
<portainer-tooltip
|
||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
||||
message="'When the exposed URLs for your applications differ from the specified paths in the ingress, use the rewrite target annotation to denote the path to redirect to.'"
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
<label class="control-label text-left">
|
||||
<span
|
||||
class="label label-default interactive"
|
||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
||||
ng-click="$ctrl.addUseregexAnnotation(ic)"
|
||||
data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}"
|
||||
>
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon> add regular expression annotation
|
||||
</span>
|
||||
<portainer-tooltip
|
||||
ng-if="ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX"
|
||||
message="'Enable use of regular expressions in ingress paths (set in the ingress details of an application). Use this along with rewrite-target to specify the regex capturing group to be replaced, e.g. path regex of ^/foo/(,*) and rewrite-target of /bar/$1 rewrites example.com/foo/account to example.com/bar/account.'"
|
||||
>
|
||||
</portainer-tooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline pt-4">
|
||||
<div class="pt-2" ng-repeat="annotation in ic.Annotations track by $index">
|
||||
<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="{{
|
||||
ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX
|
||||
? 'e.g. nginx.ingress.kubernetes.io/enable-rewrite-log'
|
||||
: 'e.g. traefik.ingress.kubernetes.io/router.priority'
|
||||
}}"
|
||||
required
|
||||
data-cy="namespaceCreate-annotationKey{{ ic.IngressClass.Name }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group input-group-sm col-sm-5">
|
||||
<span class="input-group-addon">Value</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="annotation.Value"
|
||||
placeholder="{{ ic.IngressClass.Type === $ctrl.IngressClassTypes.NGINX ? 'e.g. true or false' : 'e.g. 42' }}"
|
||||
required
|
||||
data-cy="namespaceCreate-annotationValue{{ ic.IngressClass.Name }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group input-group-sm col-sm-1">
|
||||
<button
|
||||
class="btn btn-md btn-dangerlight btn-only-icon"
|
||||
type="button"
|
||||
ng-click="$ctrl.removeAnnotation(ic, $index)"
|
||||
data-cy="namespaceCreate-deleteAnnotationButton{{ ic.IngressClass.Name }}"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" size="'md'" feather="true"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title"> Networking </div>
|
||||
<ingress-class-datatable
|
||||
ng-if="$ctrl.state.ingressAvailabilityPerNamespace"
|
||||
on-change-availability="($ctrl.onChangeIngressControllerAvailability)"
|
||||
ingress-controllers="$ctrl.ingressControllers"
|
||||
description="'Enable the ingress controllers that users can select when publishing applications in this namespace.'"
|
||||
no-ingress-controller-label="'No ingress controllers found in the cluster. Go to the cluster setup view to configure and allow the use of ingress controllers in the cluster.'"
|
||||
view="'namespace'"
|
||||
></ingress-class-datatable>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
|
||||
|
@ -422,6 +225,25 @@
|
|||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region STORAGES -->
|
||||
<div class="col-sm-12 form-section-title"> Storage </div>
|
||||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" mode="'primary'" feather="true"></pr-icon>
|
||||
Quotas can be set on each storage option to prevent users from exceeding a specific threshold when deploying applications. You can set a quota to 0 to effectively
|
||||
prevent the usage of a specific storage option inside this namespace.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title vertical-center">
|
||||
<pr-icon icon="'svg-route'"></pr-icon>
|
||||
standard
|
||||
</div>
|
||||
|
||||
<storage-class-switch value="sc.Selected" name="sc.Name" on-change="(ctrl.onToggleStorageQuota)" authorization="K8sResourcePoolDetailsW"></storage-class-switch>
|
||||
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- summary -->
|
||||
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !$ctrl.isCreateButtonDisabled()" form-values="$ctrl.formValues"></kubernetes-summary-view>
|
||||
<!-- !summary -->
|
||||
|
|
|
@ -2,20 +2,12 @@ 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,
|
||||
KubernetesResourcePoolIngressClassHostFormValue,
|
||||
KubernetesResourcePoolNginxRewriteAnnotationFormValue,
|
||||
KubernetesResourcePoolNginxUseregexAnnotationFormValue,
|
||||
KubernetesResourcePoolTraefikRewriteAnnotationFormValue,
|
||||
} from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolFormValues, 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';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
|
||||
class KubernetesCreateResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
@ -39,10 +31,17 @@ class KubernetesCreateResourcePoolController {
|
|||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||
this.onToggleLoadBalancerQuota = this.onToggleLoadBalancerQuota.bind(this);
|
||||
this.onToggleResourceQuota = this.onToggleResourceQuota.bind(this);
|
||||
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
|
||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
onRegistriesChange(registries) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Registries = registries;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleStorageQuota(storageClassName, enabled) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.StorageClasses = this.formValues.StorageClasses.map((sClass) => (sClass.Name !== storageClassName ? sClass : { ...sClass, Selected: enabled }));
|
||||
|
@ -61,74 +60,9 @@ class KubernetesCreateResourcePoolController {
|
|||
});
|
||||
}
|
||||
|
||||
onChangeIngressHostname() {
|
||||
const state = this.state.duplicates.ingressHosts;
|
||||
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
|
||||
const hostnames = _.compact(hosts.map((h) => h.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 = false;
|
||||
_.forIn(duplicates, (value) => {
|
||||
if (Object.keys(value).length > 0) {
|
||||
state.hasRefs = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onRegistriesChange(registries) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Registries = registries;
|
||||
});
|
||||
}
|
||||
|
||||
addHostname(ingressClass) {
|
||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
|
||||
removeHostname(ingressClass, index) {
|
||||
ingressClass.Hosts.splice(index, 1);
|
||||
this.onChangeIngressHostname();
|
||||
}
|
||||
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
addAnnotation(ingressClass) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
||||
}
|
||||
|
||||
addRewriteAnnotation(ingressClass) {
|
||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.NGINX) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxRewriteAnnotationFormValue());
|
||||
}
|
||||
|
||||
if (ingressClass.IngressClass.Type === this.IngressClassTypes.TRAEFIK) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolTraefikRewriteAnnotationFormValue());
|
||||
}
|
||||
}
|
||||
|
||||
addUseregexAnnotation(ingressClass) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolNginxUseregexAnnotationFormValue());
|
||||
}
|
||||
|
||||
removeAnnotation(ingressClass, index) {
|
||||
ingressClass.Annotations.splice(index, 1);
|
||||
/* #region INGRESS MANAGEMENT */
|
||||
onChangeIngressControllerAvailability(controllerClassMap) {
|
||||
this.ingressControllers = controllerClassMap;
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
@ -175,6 +109,7 @@ class KubernetesCreateResourcePoolController {
|
|||
this.checkDefaults();
|
||||
this.formValues.Owner = this.Authentication.getUserDetails().username;
|
||||
await this.KubernetesResourcePoolService.create(this.formValues);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||
this.Notifications.success('Namespace successfully created', this.formValues.Name);
|
||||
this.$state.go('kubernetes.resourcePools');
|
||||
} catch (err) {
|
||||
|
@ -239,15 +174,21 @@ class KubernetesCreateResourcePoolController {
|
|||
viewReady: false,
|
||||
isAlreadyExist: false,
|
||||
hasPrefixKube: false,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
canUseIngress: false,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
isAdmin: this.Authentication.isAdmin(),
|
||||
ingressAvailabilityPerNamespace: endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace,
|
||||
};
|
||||
|
||||
const nodes = await this.KubernetesNodeService.get();
|
||||
|
||||
this.ingressControllers = [];
|
||||
if (this.state.ingressAvailabilityPerNamespace) {
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, allowedOnly: true });
|
||||
}
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
this.state.sliderMaxMemory += filesizeParser(item.Memory);
|
||||
this.state.sliderMaxCpu += item.CPU;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue