mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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
|
@ -1,6 +1,7 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { IngressClassDatatable } from '@/react/kubernetes/cluster/ingressClass/IngressClassDatatable';
|
||||
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
||||
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||
|
@ -8,6 +9,16 @@ import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces
|
|||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.kubernetes.react.components', [])
|
||||
.component(
|
||||
'ingressClassDatatable',
|
||||
r2a(IngressClassDatatable, [
|
||||
'onChangeAvailability',
|
||||
'description',
|
||||
'ingressControllers',
|
||||
'noIngressControllerLabel',
|
||||
'view',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'namespacesSelector',
|
||||
r2a(NamespacesSelector, [
|
||||
|
|
|
@ -158,10 +158,12 @@ export function CreateIngressView() {
|
|||
);
|
||||
const ingressClassOptions: Option<string>[] = [
|
||||
{ label: 'Select an ingress class', value: '' },
|
||||
...(ingressControllersResults.data?.map((cls) => ({
|
||||
label: cls.ClassName,
|
||||
value: cls.ClassName,
|
||||
})) || []),
|
||||
...(ingressControllersResults.data
|
||||
?.filter((cls) => cls.Availability)
|
||||
.map((cls) => ({
|
||||
label: cls.ClassName,
|
||||
value: cls.ClassName,
|
||||
})) || []),
|
||||
];
|
||||
|
||||
if (!existingIngressClass && ingressRule.IngressClassName) {
|
||||
|
|
|
@ -65,14 +65,12 @@ export function useIngresses(
|
|||
'ingress',
|
||||
],
|
||||
async () => {
|
||||
const ingresses: Ingress[] = [];
|
||||
for (let i = 0; i < namespaces.length; i += 1) {
|
||||
const ings = await getIngresses(environmentId, namespaces[i]);
|
||||
if (ings) {
|
||||
ingresses.push(...ings);
|
||||
}
|
||||
}
|
||||
return ingresses;
|
||||
const ingresses = await Promise.all(
|
||||
namespaces.map((namespace) => getIngresses(environmentId, namespace))
|
||||
);
|
||||
// flatten the array and remove empty ingresses
|
||||
const filteredIngresses = ingresses.flat().filter((ing) => ing);
|
||||
return filteredIngresses;
|
||||
},
|
||||
{
|
||||
enabled: namespaces.length > 0,
|
||||
|
|
|
@ -35,124 +35,35 @@
|
|||
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left col-sm-5 col-lg-4 px-0"> Allow users to use external load balancer </label>
|
||||
<label class="switch mb-0 col-sm-8">
|
||||
<label class="switch col-sm-8 mb-0">
|
||||
<input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><span class="slider round" data-cy="kubeSetup-loadBalancerToggle"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 text-muted small mt-4">
|
||||
<p> Configuring ingress controllers will allow users to expose application they deploy over a HTTP route. </p>
|
||||
<p class="mt-1 vertical-center">
|
||||
<pr-icon icon="'alert-circle'" mode="'warning'" feather="true"></pr-icon>
|
||||
Ingress classes must be manually specified for each controller you want to use in the cluster. Make sure that each controller is running inside your cluster.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ingress-class-datatable
|
||||
on-change-availability="(ctrl.onChangeAvailability)"
|
||||
ingress-controllers="ctrl.originalIngressControllers"
|
||||
description="'Enabling ingress controllers in your cluster allows them to be available in the Portainer UI for users to publish applications over HTTP/HTTPS. A controller must have a class name for it to be included here.'"
|
||||
no-ingress-controller-label="'No supported ingress controllers found.'"
|
||||
view="'cluster'"
|
||||
></ingress-class-datatable>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Ingress controller</label>
|
||||
<span class="label label-default interactive vertical-center" style="margin-left: 10px" ng-click="ctrl.addIngressClass()" data-cy="kubeSetup-congifIngressButton">
|
||||
<pr-icon icon="'plus'" feather="true" size="'sm'" class="vertical-center"></pr-icon> configure ingress controller
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
||||
<div ng-repeat-start="ingressClass in ctrl.formValues.IngressClasses" style="margin-top: 2px">
|
||||
<div class="col-sm-7 input-group input-group-sm" ng-class="{ striked: ingressClass.NeedsDeletion }">
|
||||
<span class="input-group-addon">Ingress class</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="ingress_class_name_{{ $index }}"
|
||||
ng-model="ingressClass.Name"
|
||||
placeholder="nginx"
|
||||
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
|
||||
ng-change="ctrl.onChangeIngressClassName($index)"
|
||||
required
|
||||
data-cy="kubeSetup-ingressClassName"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: ingressClass.NeedsDeletion }">
|
||||
<span class="input-group-addon">Type</span>
|
||||
<select
|
||||
class="form-control"
|
||||
name="ingress_class_type_{{ $index }}"
|
||||
ng-model="ingressClass.Type"
|
||||
ng-options="value as value for (key, value) in ctrl.IngressClassTypes"
|
||||
required
|
||||
data-cy="kubeSetup-ingressType"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a type</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm">
|
||||
<button
|
||||
ng-if="!ingressClass.NeedsDeletion"
|
||||
class="btn btn-dangerlight btn-only-icon"
|
||||
type="button"
|
||||
ng-click="ctrl.removeIngressClass($index)"
|
||||
data-cy="kubeSetup-deleteIngress"
|
||||
>
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
||||
</button>
|
||||
<button ng-if="ingressClass.NeedsDeletion" class="btn btn-only-icon btn-light" type="button" ng-click="ctrl.restoreIngressClass($index)">
|
||||
<pr-icon icon="'rotate-ccw'" feather="true"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ng-repeat-end
|
||||
ng-show="
|
||||
kubernetesClusterSetupForm['ingress_class_name_' + $index].$invalid ||
|
||||
kubernetesClusterSetupForm['ingress_class_type_' + $index].$invalid ||
|
||||
ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined
|
||||
"
|
||||
<por-switch-field
|
||||
checked="ctrl.formValues.IngressAvailabilityPerNamespace"
|
||||
name="'ingressAvailabilityPerNamespace'"
|
||||
label="'Configure ingress controller availability per namespace'"
|
||||
tooltip="'This allows an administrator to configure, in each namespace, which ingress controllers will be available for users to select when setting up ingresses for applications.'"
|
||||
on-change="(ctrl.onToggleIngressAvailabilityPerNamespace)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
<div class="col-sm-7 input-group">
|
||||
<div
|
||||
class="small"
|
||||
style="margin-top: 5px"
|
||||
ng-if="kubernetesClusterSetupForm['ingress_class_name_' + $index].$invalid || ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined"
|
||||
>
|
||||
<div ng-messages="kubernetesClusterSetupForm['ingress_class_name_'+$index].$error">
|
||||
<p ng-message="required" class="vertical-center text-warning"
|
||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Ingress class name is required.</p
|
||||
>
|
||||
<p ng-message="pattern" class="vertical-center text-warning"
|
||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This field must consist of lower case alphanumeric characters or '-', start
|
||||
with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').</p
|
||||
>
|
||||
</div>
|
||||
<p ng-if="ctrl.state.duplicates.ingressClasses.refs[$index] !== undefined" class="vertical-center text-warning">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This ingress class is already defined.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 input-group">
|
||||
<div class="small" style="margin-top: 5px" ng-if="kubernetesClusterSetupForm['ingress_class_type_' + $index].$invalid">
|
||||
<div ng-messages="kubernetesClusterSetupForm['ingress_class_type_'+$index].$error">
|
||||
<p ng-message="required" class="vertical-center text-warning"
|
||||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Ingress class type is required.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.hasTraefikIngress()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
<p>
|
||||
<i class="fa fa-flask blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
Traefik support is experimental.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- auto update window -->
|
||||
<div class="col-sm-12 form-section-title"> Change Window Settings </div>
|
||||
|
||||
|
@ -163,7 +74,7 @@
|
|||
name="'disableSysctlSettingForRegularUsers'"
|
||||
label="'Enable Change Window'"
|
||||
feature-id="ctrl.limitedFeatureAutoWindow"
|
||||
tooltip="'Specify a timeframe during which automatic updates can occur in this environment.'"
|
||||
tooltip="'Automatic updates to stacks or applications outside the defined change window will not occur.'"
|
||||
on-change="(ctrl.onToggleAutoUpdate)"
|
||||
label-class="'col-sm-5 col-lg-4 px-0 !m-0'"
|
||||
switch-class="'col-sm-8 text-muted'"
|
||||
|
|
|
@ -2,12 +2,12 @@ import _ from 'lodash-es';
|
|||
import angular from 'angular';
|
||||
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
|
||||
import { getIngressControllerClassMap, updateIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
|
||||
class KubernetesConfigureController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
||||
|
@ -41,10 +41,15 @@ class KubernetesConfigureController {
|
|||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.configureAsync = this.configureAsync.bind(this);
|
||||
this.areControllersChanged = this.areControllersChanged.bind(this);
|
||||
this.areFormValuesChanged = this.areFormValuesChanged.bind(this);
|
||||
this.onBeforeOnload = this.onBeforeOnload.bind(this);
|
||||
this.limitedFeature = FeatureId.K8S_SETUP_DEFAULT;
|
||||
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||
this.onChangeAvailability = this.onChangeAvailability.bind(this);
|
||||
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
||||
this.onToggleIngressAvailabilityPerNamespace = this.onToggleIngressAvailabilityPerNamespace.bind(this);
|
||||
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
@ -66,48 +71,23 @@ class KubernetesConfigureController {
|
|||
/* #endregion */
|
||||
|
||||
/* #region INGRESS CLASSES UI MANAGEMENT */
|
||||
addIngressClass() {
|
||||
this.formValues.IngressClasses.push(new KubernetesIngressClass());
|
||||
this.onChangeIngressClass();
|
||||
}
|
||||
|
||||
restoreIngressClass(index) {
|
||||
this.formValues.IngressClasses[index].NeedsDeletion = false;
|
||||
this.onChangeIngressClass();
|
||||
}
|
||||
|
||||
removeIngressClass(index) {
|
||||
if (!this.formValues.IngressClasses[index].IsNew) {
|
||||
this.formValues.IngressClasses[index].NeedsDeletion = true;
|
||||
} else {
|
||||
this.formValues.IngressClasses.splice(index, 1);
|
||||
}
|
||||
this.onChangeIngressClass();
|
||||
}
|
||||
|
||||
onChangeIngressClass() {
|
||||
const state = this.state.duplicates.ingressClasses;
|
||||
const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
onChangeIngressClassName(index) {
|
||||
const fv = this.formValues.IngressClasses[index];
|
||||
if (_.includes(fv.Name, KubernetesIngressClassTypes.NGINX)) {
|
||||
fv.Type = KubernetesIngressClassTypes.NGINX;
|
||||
} else if (_.includes(fv.Name, KubernetesIngressClassTypes.TRAEFIK)) {
|
||||
fv.Type = KubernetesIngressClassTypes.TRAEFIK;
|
||||
}
|
||||
this.onChangeIngressClass();
|
||||
onChangeAvailability(controllerClassMap) {
|
||||
this.ingressControllers = controllerClassMap;
|
||||
}
|
||||
|
||||
hasTraefikIngress() {
|
||||
return _.find(this.formValues.IngressClasses, { Type: this.IngressClassTypes.TRAEFIK });
|
||||
}
|
||||
|
||||
onToggleIngressAvailabilityPerNamespace() {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.IngressAvailabilityPerNamespace = !this.formValues.IngressAvailabilityPerNamespace;
|
||||
});
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region RESOURCES AND METRICS */
|
||||
|
||||
onChangeEnableResourceOverCommit(enabled) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.EnableResourceOverCommit = enabled;
|
||||
|
@ -117,13 +97,19 @@ class KubernetesConfigureController {
|
|||
});
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region CONFIGURE */
|
||||
assignFormValuesToEndpoint(endpoint, storageClasses, ingressClasses) {
|
||||
endpoint.Kubernetes.Configuration.StorageClasses = storageClasses;
|
||||
endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
endpoint.Kubernetes.Configuration.EnableResourceOverCommit = this.formValues.EnableResourceOverCommit;
|
||||
endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage = this.formValues.ResourceOverCommitPercentage;
|
||||
endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses;
|
||||
endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace;
|
||||
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = this.formValues.IngressAvailabilityPerNamespace;
|
||||
endpoint.ChangeWindow = this.state.autoUpdateSettings;
|
||||
}
|
||||
|
||||
transformFormValues() {
|
||||
|
@ -150,11 +136,9 @@ class KubernetesConfigureController {
|
|||
|
||||
async removeIngressesAcrossNamespaces() {
|
||||
const ingressesToDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||
|
||||
if (!ingressesToDel.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
const oldEndpointID = this.EndpointProvider.endpointID();
|
||||
this.EndpointProvider.setEndpointID(this.endpoint.Id);
|
||||
|
@ -213,7 +197,9 @@ class KubernetesConfigureController {
|
|||
|
||||
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||
|
||||
// updateIngressControllerClassMap must be done after updateEndpoint, as a hacky workaround. A better solution: saving ingresscontrollers somewhere else, is being discussed
|
||||
await updateIngressControllerClassMap(this.state.endpointId, this.ingressControllers);
|
||||
this.state.isSaving = true;
|
||||
const storagePromises = _.map(storageClasses, (storageClass) => {
|
||||
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||
if (oldStorageClass) {
|
||||
|
@ -291,19 +277,31 @@ class KubernetesConfigureController {
|
|||
isServerRunning: false,
|
||||
userClick: false,
|
||||
},
|
||||
timeZone: '',
|
||||
isSaving: false,
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
EnableResourceOverCommit: true,
|
||||
ResourceOverCommitPercentage: 20,
|
||||
IngressClasses: [],
|
||||
RestrictDefaultNamespace: false,
|
||||
enableAutoUpdateTimeWindow: false,
|
||||
IngressAvailabilityPerNamespace: false,
|
||||
};
|
||||
|
||||
try {
|
||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
|
||||
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]);
|
||||
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.state.endpointId });
|
||||
this.originalIngressControllers = structuredClone(this.ingressControllers);
|
||||
|
||||
this.state.autoUpdateSettings = this.endpoint.ChangeWindow;
|
||||
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
||||
if (storage) {
|
||||
|
@ -316,12 +314,15 @@ class KubernetesConfigureController {
|
|||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
this.formValues.EnableResourceOverCommit = this.endpoint.Kubernetes.Configuration.EnableResourceOverCommit;
|
||||
this.formValues.ResourceOverCommitPercentage = this.endpoint.Kubernetes.Configuration.ResourceOverCommitPercentage;
|
||||
this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace;
|
||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||
ic.IsNew = false;
|
||||
ic.NeedsDeletion = false;
|
||||
return ic;
|
||||
});
|
||||
this.formValues.IngressAvailabilityPerNamespace = this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace;
|
||||
|
||||
this.oldFormValues = Object.assign({}, this.formValues);
|
||||
} catch (err) {
|
||||
|
@ -329,12 +330,48 @@ class KubernetesConfigureController {
|
|||
} finally {
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.onBeforeOnload);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
$onDestroy() {
|
||||
window.removeEventListener('beforeunload', this.onBeforeOnload);
|
||||
}
|
||||
|
||||
areControllersChanged() {
|
||||
return !_.isEqual(this.ingressControllers, this.originalIngressControllers);
|
||||
}
|
||||
|
||||
areFormValuesChanged() {
|
||||
return !_.isEqual(this.formValues, this.oldFormValues);
|
||||
}
|
||||
|
||||
onBeforeOnload(event) {
|
||||
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged())) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
}
|
||||
|
||||
uiCanExit() {
|
||||
if (!this.state.isSaving && (this.areControllersChanged() || this.areFormValuesChanged())) {
|
||||
return this.ModalService.confirmAsync({
|
||||
title: 'Are you sure?',
|
||||
message: 'You currently have unsaved changes in the cluster setup view. Are you sure you want to leave?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Yes',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesConfigureController;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -156,193 +156,20 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title"> Ingresses </div>
|
||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable && 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="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>
|
||||
|
||||
<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="text-muted col-sm-12" style="width: 100%">
|
||||
<pr-icon icon="'svg-route'" mode="'primary'"></pr-icon>
|
||||
{{ ic.IngressClass.Name }}
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group">
|
||||
<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-9 pt-2">
|
||||
<label class="switch">
|
||||
<input type="checkbox" ng-model="ic.Selected" />
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
</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="vertical-center label label-default interactive" style="margin-left: 10px" ng-click="ctrl.addHostname(ic)">
|
||||
<pr-icon icon="'plus'" feather="true"></pr-icon> 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 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
|
||||
/>
|
||||
</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-dangerlight" type="button" ng-click="ctrl.removeHostname(ic, $index)">
|
||||
<pr-icon icon="'trash-2'" feather="true"></pr-icon>
|
||||
</button>
|
||||
<button ng-if="item.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreHostname(item)">
|
||||
<pr-icon icon="'svg-restore'"></pr-icon>
|
||||
</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" class="vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" feather="true" mode="'warning'"></pr-icon>
|
||||
Hostname is required.
|
||||
</p>
|
||||
<p ng-message="pattern" class="vertical-center">
|
||||
<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="item.Duplicate">
|
||||
<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" style="margin-bottom: 20px">
|
||||
<div class="col-sm-12 small text-muted" style="margin-top: 5px">
|
||||
<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>
|
||||
<span class="vertical-center label label-default interactive" style="margin-left: 10px" ng-click="ctrl.addAnnotation(ic)">
|
||||
<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>
|
||||
<span
|
||||
class="vertical-center label label-default interactive"
|
||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
||||
style="margin-left: 10px"
|
||||
ng-click="ctrl.addRewriteAnnotation(ic)"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="vertical-center label label-default interactive"
|
||||
ng-if="ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX"
|
||||
style="margin-left: 10px"
|
||||
ng-click="ctrl.addUseregexAnnotation(ic)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px">
|
||||
<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="{{
|
||||
ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX
|
||||
? 'e.g. nginx.ingress.kubernetes.io/enable-rewrite-log'
|
||||
: 'e.g. traefik.ingress.kubernetes.io/router.priority'
|
||||
}}"
|
||||
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="{{ ic.IngressClass.Type === ctrl.IngressClassTypes.NGINX ? 'e.g. true or false' : 'e.g. 42' }}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-1 input-group input-group-sm">
|
||||
<button class="btn btn-md btn-dangerlight btn-only-icon" type="button" ng-click="ctrl.removeAnnotation(ic, $index)">
|
||||
<pr-icon icon="'trash-2'" feather="true" size="'md'"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
|
||||
<!-- #region REGISTRIES -->
|
||||
<div>
|
||||
<div class="col-sm-12 form-section-title"> Registries </div>
|
||||
|
|
|
@ -5,21 +5,15 @@ import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quot
|
|||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||
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 { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { updateIngressControllerClassMap, getIngressControllerClassMap } from '@/react/kubernetes/cluster/ingressClass/utils';
|
||||
|
||||
class KubernetesResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
|
@ -73,35 +67,11 @@ class KubernetesResourcePoolController {
|
|||
this.getEvents = this.getEvents.bind(this);
|
||||
this.onToggleLoadBalancersQuota = this.onToggleLoadBalancersQuota.bind(this);
|
||||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||
this.onChangeIngressControllerAvailability = this.onChangeIngressControllerAvailability.bind(this);
|
||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #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);
|
||||
this.onChangeIngressHostname();
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
onRegistriesChange(registries) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Registries = registries;
|
||||
|
@ -120,55 +90,10 @@ class KubernetesResourcePoolController {
|
|||
});
|
||||
}
|
||||
|
||||
/* #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;
|
||||
}
|
||||
});
|
||||
});
|
||||
onChangeIngressControllerAvailability(controllerClassMap) {
|
||||
this.ingressControllers = controllerClassMap;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -219,6 +144,7 @@ class KubernetesResourcePoolController {
|
|||
try {
|
||||
this.checkDefaults();
|
||||
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
|
||||
await updateIngressControllerClassMap(this.endpoint.Id, this.ingressControllers, this.formValues.Name);
|
||||
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
|
@ -426,11 +352,12 @@ class KubernetesResourcePoolController {
|
|||
ingressesLoading: true,
|
||||
viewReady: false,
|
||||
eventWarningCount: 0,
|
||||
canUseIngress: this.endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
canUseIngress: false,
|
||||
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValidationReferences(),
|
||||
},
|
||||
ingressAvailabilityPerNamespace: this.endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
||||
|
@ -439,6 +366,11 @@ class KubernetesResourcePoolController {
|
|||
|
||||
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
|
||||
|
||||
this.ingressControllers = [];
|
||||
if (this.state.ingressAvailabilityPerNamespace) {
|
||||
this.ingressControllers = await getIngressControllerClassMap({ environmentId: this.endpoint.Id, namespace: name });
|
||||
}
|
||||
|
||||
this.pool = _.find(pools, { Namespace: { Name: name } });
|
||||
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
||||
this.formValues.Name = this.pool.Namespace.Name;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue