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>pull/4247/head
parent
68851aada4
commit
d850e18ff0
|
@ -5,9 +5,8 @@ func KubernetesDefault() KubernetesData {
|
|||
Configuration: KubernetesConfiguration{
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
UseIngress: false,
|
||||
StorageClasses: []KubernetesStorageClassConfig{},
|
||||
IngressClasses: []string{},
|
||||
IngressClasses: []KubernetesIngressClassConfig{},
|
||||
},
|
||||
Snapshots: []KubernetesSnapshot{},
|
||||
}
|
||||
|
|
|
@ -332,9 +332,8 @@ type (
|
|||
KubernetesConfiguration struct {
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
UseIngress bool `json:"UseIngress"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []string `json:"IngressClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
}
|
||||
|
||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||
|
@ -345,6 +344,12 @@ type (
|
|||
AllowVolumeExpansion bool `json:"AllowVolumeExpansion"`
|
||||
}
|
||||
|
||||
// KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration
|
||||
KubernetesIngressClassConfig struct {
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
}
|
||||
|
||||
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
|
||||
LDAPGroupSearchSettings struct {
|
||||
GroupBaseDN string `json:"GroupBaseDN"`
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
KubernetesApplicationDataAccessPolicies,
|
||||
KubernetesApplicationDeploymentTypes,
|
||||
KubernetesApplicationPersistedFolder,
|
||||
KubernetesApplicationPort,
|
||||
KubernetesApplicationPublishingTypes,
|
||||
KubernetesApplicationTypes,
|
||||
KubernetesPortainerApplicationNameLabel,
|
||||
|
@ -25,7 +26,6 @@ import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet';
|
|||
import KubernetesServiceConverter from 'Kubernetes/converters/service';
|
||||
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
|
||||
import PortainerError from 'Portainer/error';
|
||||
import { KubernetesApplicationPort } from 'Kubernetes/models/application/models';
|
||||
import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper';
|
||||
|
||||
function _apiPortsToPublishedPorts(pList, pRefs) {
|
||||
|
@ -270,9 +270,9 @@ class KubernetesApplicationConverter {
|
|||
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;
|
||||
if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
||||
res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
||||
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && !isIngress) {
|
||||
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) {
|
||||
res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER;
|
||||
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && isIngress) {
|
||||
} else if (app.ServiceType === KubernetesServiceTypes.CLUSTER_IP && isIngress) {
|
||||
res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS;
|
||||
} else {
|
||||
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
||||
|
|
|
@ -3,12 +3,12 @@ import * as JsonPatch from 'fast-json-patch';
|
|||
|
||||
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
||||
import {
|
||||
KubernetesPortainerApplicationStackNameLabel,
|
||||
KubernetesApplicationPublishingTypes,
|
||||
KubernetesPortainerApplicationNameLabel,
|
||||
KubernetesPortainerApplicationOwnerLabel,
|
||||
KubernetesPortainerApplicationStackNameLabel,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models';
|
||||
import { KubernetesService, KubernetesServiceHeadlessClusterIP, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
||||
|
||||
function _publishedPortToServicePort(formValues, publishedPort, type) {
|
||||
|
@ -42,7 +42,7 @@ class KubernetesServiceConverter {
|
|||
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
||||
res.ApplicationOwner = formValues.ApplicationOwner;
|
||||
res.ApplicationName = formValues.Name;
|
||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER || formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
|
||||
res.Type = KubernetesServiceTypes.NODE_PORT;
|
||||
} else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
export const KubernetesIngressClassAnnotation = 'kubernetes.io/ingress.class';
|
||||
export const KubernetesIngressClassMandatoryAnnotations = Object.freeze({
|
||||
nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/$1' },
|
||||
|
||||
// keys must match KubernetesIngressClassTypes values to map them quickly using the ingress type
|
||||
// KubernetesIngressClassRewriteTargetAnnotations[KubernetesIngressClassTypes.NGINX] for example
|
||||
export const KubernetesIngressClassRewriteTargetAnnotations = Object.freeze({
|
||||
nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/' },
|
||||
traefik: { 'traefik.ingress.kubernetes.io/rewrite-target': '/' },
|
||||
});
|
||||
|
||||
export const KubernetesIngressClassTypes = Object.freeze({
|
||||
NGINX: 'nginx',
|
||||
TRAEFIK: 'traefik',
|
||||
});
|
||||
|
|
|
@ -2,9 +2,10 @@ import * as _ from 'lodash-es';
|
|||
import * as JsonPatch from 'fast-json-patch';
|
||||
|
||||
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||
import { KubernetesIngressRule, KubernetesIngress } from './models';
|
||||
import { KubernetesResourcePoolIngressClassAnnotationFormValue, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngress, KubernetesIngressRule } from './models';
|
||||
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||
import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants';
|
||||
import { KubernetesIngressClassAnnotation, KubernetesIngressClassRewriteTargetAnnotations } from './constants';
|
||||
|
||||
export class KubernetesIngressConverter {
|
||||
// TODO: refactor @LP
|
||||
|
@ -64,17 +65,68 @@ export class KubernetesIngressConverter {
|
|||
return ingresses;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {KubernetesResourcePoolIngressClassFormValue} formValues
|
||||
*/
|
||||
static resourcePoolIngressClassFormValueToIngress(formValues) {
|
||||
const res = new KubernetesIngress();
|
||||
res.Name = formValues.IngressClass.Name;
|
||||
res.Namespace = formValues.Namespace;
|
||||
const pairs = _.map(formValues.Annotations, (a) => [a.Key, a.Value]);
|
||||
res.Annotations = _.fromPairs(pairs);
|
||||
if (formValues.RewriteTarget) {
|
||||
_.extend(res.Annotations, KubernetesIngressClassRewriteTargetAnnotations[formValues.IngressClass.Type]);
|
||||
}
|
||||
res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name;
|
||||
res.Host = formValues.Host;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {KubernetesIngressClass} ics Ingress classes (saved in Portainer DB)
|
||||
* @param {KubernetesIngress} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and passed for RP EDIT VIEW
|
||||
*/
|
||||
static ingressClassesToFormValues(ics, ingresses) {
|
||||
const res = _.map(ics, (ic) => {
|
||||
const fv = new KubernetesResourcePoolIngressClassFormValue();
|
||||
fv.IngressClass = ic;
|
||||
const ingress = _.find(ingresses, { Name: ic.Name });
|
||||
if (ingress) {
|
||||
fv.Selected = true;
|
||||
fv.WasSelected = true;
|
||||
fv.Host = ingress.Host;
|
||||
const [[rewriteKey]] = _.toPairs(KubernetesIngressClassRewriteTargetAnnotations[ic.Type]);
|
||||
const annotations = _.map(_.toPairs(ingress.Annotations), ([key, value]) => {
|
||||
if (key === rewriteKey) {
|
||||
fv.RewriteTarget = true;
|
||||
} else if (key !== KubernetesIngressClassAnnotation) {
|
||||
const annotation = new KubernetesResourcePoolIngressClassAnnotationFormValue();
|
||||
annotation.Key = key;
|
||||
annotation.Value = value;
|
||||
return annotation;
|
||||
}
|
||||
});
|
||||
fv.Annotations = _.without(annotations, undefined);
|
||||
fv.AdvancedConfig = fv.Annotations.length > 0;
|
||||
}
|
||||
return fv;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
static createPayload(data) {
|
||||
const res = new KubernetesIngressCreatePayload();
|
||||
res.metadata.name = data.Name;
|
||||
res.metadata.namespace = data.Namespace;
|
||||
res.metadata.annotations = data.Annotations || {};
|
||||
res.metadata.annotations[KubernetesIngressClassAnnotation] = data.IngressClassName;
|
||||
const annotations = KubernetesIngressClassMandatoryAnnotations[data.Name];
|
||||
if (annotations) {
|
||||
_.extend(res.metadata.annotations, annotations);
|
||||
}
|
||||
res.metadata.annotations = data.Annotations;
|
||||
if (data.Paths && data.Paths.length) {
|
||||
_.forEach(data.Paths, (p) => {
|
||||
if (p.Host === 'undefined' || p.Host === undefined) {
|
||||
p.Host = '';
|
||||
}
|
||||
});
|
||||
const groups = _.groupBy(data.Paths, 'Host');
|
||||
const rules = _.map(groups, (paths, host) => {
|
||||
const rule = new KubernetesIngressRuleCreatePayload();
|
||||
|
|
|
@ -24,3 +24,12 @@ export function KubernetesIngressRule() {
|
|||
Path: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function KubernetesIngressClass() {
|
||||
return {
|
||||
Name: '',
|
||||
Type: undefined,
|
||||
NeedsDeletion: false,
|
||||
IsNew: true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -55,10 +55,10 @@ class KubernetesIngressService {
|
|||
/**
|
||||
* CREATE
|
||||
*/
|
||||
async createAsync(formValues) {
|
||||
async createAsync(ingress) {
|
||||
try {
|
||||
const params = {};
|
||||
const payload = KubernetesIngressConverter.createPayload(formValues);
|
||||
const payload = KubernetesIngressConverter.createPayload(ingress);
|
||||
const namespace = payload.metadata.namespace;
|
||||
const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise;
|
||||
return data;
|
||||
|
@ -67,8 +67,8 @@ class KubernetesIngressService {
|
|||
}
|
||||
}
|
||||
|
||||
create(formValues) {
|
||||
return this.$async(this.createAsync, formValues);
|
||||
create(ingress) {
|
||||
return this.$async(this.createAsync, ingress);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,7 +100,7 @@ class KubernetesIngressService {
|
|||
async deleteAsync(ingress) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = ingress.Name;
|
||||
params.id = ingress.IngressClass.Name;
|
||||
const namespace = ingress.Namespace;
|
||||
await this.KubernetesIngresses(namespace).delete(params).$promise;
|
||||
} catch (err) {
|
||||
|
|
|
@ -151,7 +151,7 @@ export class KubernetesApplicationAutoScalerFormValue {
|
|||
}
|
||||
}
|
||||
|
||||
export function KubernetesApplicationFormValidationDuplicate() {
|
||||
export function KubernetesFormValueDuplicate() {
|
||||
return {
|
||||
refs: {},
|
||||
hasDuplicates: false,
|
||||
|
|
|
@ -8,15 +8,24 @@ export function KubernetesResourcePoolFormValues(defaults) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} ingressClassName
|
||||
* @param {KubernetesIngressClass} ingressClass
|
||||
*/
|
||||
export function KubernetesResourcePoolIngressClassFormValue(ingressClassName) {
|
||||
export function KubernetesResourcePoolIngressClassFormValue(ingressClass) {
|
||||
return {
|
||||
Name: ingressClassName,
|
||||
IngressClassName: ingressClassName,
|
||||
Namespace: undefined, // will be filled inside ResourcePoolService.create
|
||||
IngressClass: ingressClass,
|
||||
RewriteTarget: false,
|
||||
Annotations: [], // KubernetesResourcePoolIngressClassAnnotationFormValue
|
||||
Host: undefined,
|
||||
Selected: false,
|
||||
WasSelected: false,
|
||||
Namespace: undefined, // will be filled inside ResourcePoolService.create
|
||||
AdvancedConfig: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function KubernetesResourcePoolIngressClassAnnotationFormValue() {
|
||||
return {
|
||||
Key: '',
|
||||
Value: '',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'
|
|||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
|
||||
class KubernetesResourcePoolService {
|
||||
/* @ngInject */
|
||||
|
@ -89,7 +90,8 @@ class KubernetesResourcePoolService {
|
|||
const ingressPromises = _.map(formValues.IngressClasses, (c) => {
|
||||
if (c.Selected) {
|
||||
c.Namespace = namespace.Name;
|
||||
return this.KubernetesIngressService.create(c);
|
||||
const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
|
||||
return this.KubernetesIngressService.create(ingress);
|
||||
}
|
||||
});
|
||||
await Promise.all(ingressPromises);
|
||||
|
|
|
@ -1329,10 +1329,10 @@
|
|||
class="form-control"
|
||||
name="ingress_route_{{ $index }}"
|
||||
ng-model="publishedPort.IngressRoute"
|
||||
placeholder="foo"
|
||||
placeholder="route"
|
||||
ng-required="!publishedPort.NeedsDeletion"
|
||||
ng-change="ctrl.onChangePortMappingIngressRoute()"
|
||||
ng-pattern="/^\/?([a-zA-Z0-9]+[a-zA-Z0-9-/_]*[a-zA-Z0-9]|[a-zA-Z0-9]+)$/"
|
||||
ng-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
|
||||
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
KubernetesApplicationPersistedFolderFormValue,
|
||||
KubernetesApplicationPublishedPortFormValue,
|
||||
KubernetesApplicationPlacementFormValue,
|
||||
KubernetesApplicationFormValidationDuplicate,
|
||||
KubernetesFormValueDuplicate,
|
||||
} from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||
|
@ -367,14 +367,16 @@ class KubernetesCreateApplicationController {
|
|||
const publishedPort = this.formValues.PublishedPorts[index];
|
||||
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||
publishedPort.IngressHost = ingress.Host;
|
||||
this.onChangePublishedPorts();
|
||||
}
|
||||
|
||||
onChangePortMappingIngressRoute() {
|
||||
const state = this.state.duplicates.publishedPorts.ingressRoutes;
|
||||
|
||||
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew ? p.IngressRoute : undefined));
|
||||
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? p.IngressRoute : undefined));
|
||||
const allRoutes = _.flatMapDeep(this.ingresses, (c) => _.map(c.Paths, 'Path'));
|
||||
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
||||
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
|
||||
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path));
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
|
||||
_.forEach(newRoutes, (route, idx) => {
|
||||
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
|
||||
|
@ -814,7 +816,6 @@ class KubernetesCreateApplicationController {
|
|||
actionInProgress: false,
|
||||
useLoadBalancer: false,
|
||||
useServerMetrics: false,
|
||||
canUseIngress: false,
|
||||
sliders: {
|
||||
cpu: {
|
||||
min: 0,
|
||||
|
@ -834,17 +835,17 @@ class KubernetesCreateApplicationController {
|
|||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||
alreadyExists: false,
|
||||
duplicates: {
|
||||
environmentVariables: new KubernetesApplicationFormValidationDuplicate(),
|
||||
persistedFolders: new KubernetesApplicationFormValidationDuplicate(),
|
||||
configurationPaths: new KubernetesApplicationFormValidationDuplicate(),
|
||||
existingVolumes: new KubernetesApplicationFormValidationDuplicate(),
|
||||
environmentVariables: new KubernetesFormValueDuplicate(),
|
||||
persistedFolders: new KubernetesFormValueDuplicate(),
|
||||
configurationPaths: new KubernetesFormValueDuplicate(),
|
||||
existingVolumes: new KubernetesFormValueDuplicate(),
|
||||
publishedPorts: {
|
||||
containerPorts: new KubernetesApplicationFormValidationDuplicate(),
|
||||
nodePorts: new KubernetesApplicationFormValidationDuplicate(),
|
||||
ingressRoutes: new KubernetesApplicationFormValidationDuplicate(),
|
||||
loadBalancerPorts: new KubernetesApplicationFormValidationDuplicate(),
|
||||
containerPorts: new KubernetesFormValueDuplicate(),
|
||||
nodePorts: new KubernetesFormValueDuplicate(),
|
||||
ingressRoutes: new KubernetesFormValueDuplicate(),
|
||||
loadBalancerPorts: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
placements: new KubernetesApplicationFormValidationDuplicate(),
|
||||
placements: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
isEdit: false,
|
||||
params: {
|
||||
|
|
|
@ -247,7 +247,7 @@
|
|||
</div>
|
||||
|
||||
<!-- internal notice -->
|
||||
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP">
|
||||
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && !ctrl.state.useIngress">
|
||||
<div class="small text-muted">
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is only available for internal usage inside the cluster via the application name <code>{{ ctrl.application.ServiceName }}</code>
|
||||
|
@ -261,6 +261,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ingress notice -->
|
||||
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && ctrl.state.useIngress">
|
||||
<div class="small text-muted">
|
||||
<p>
|
||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This application is available for internal usage inside the cluster via the application name <code>{{ ctrl.application.ServiceName }}</code>
|
||||
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
|
||||
><i class="fa fa-check" aria-hidden="true"></i> copied</span
|
||||
>
|
||||
</p>
|
||||
<p>It can also be accessed via specific HTTP route(s).</p>
|
||||
</div>
|
||||
<div class="small text-muted" style="margin-top: 2px;">
|
||||
<p>Refer to the below port configuration to access the application.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- table -->
|
||||
<div style="margin-top: 15px;">
|
||||
<table class="table">
|
||||
|
|
|
@ -295,6 +295,10 @@ class KubernetesApplicationController {
|
|||
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
|
||||
}
|
||||
|
||||
this.state.useIngress = _.find(application.PublishedPorts, (p) => {
|
||||
return this.portHasIngressRules(p);
|
||||
});
|
||||
|
||||
this.placements = computePlacements(nodes, this.application);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
|
||||
|
@ -322,6 +326,7 @@ class KubernetesApplicationController {
|
|||
},
|
||||
eventWarningCount: 0,
|
||||
expandedNote: false,
|
||||
useIngress: false,
|
||||
};
|
||||
|
||||
this.state.activeTab = this.LocalStorage.getActiveTab('application');
|
||||
|
|
|
@ -33,42 +33,91 @@
|
|||
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Enabling the ingress feature will allow users to expose application they deploy over a HTTP route.<br />
|
||||
Adding ingress controllers will allow users to expose application they deploy over a HTTP route.<br />
|
||||
<p style="margin-top: 2px;">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Ingress classes (controllers) must be manually specified for each one you want to use in the cluster. Make sure that each controller is running inside your
|
||||
cluster.
|
||||
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>
|
||||
</span>
|
||||
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">
|
||||
Allow users to use ingress
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseIngress" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.formValues.UseIngress">
|
||||
<label for="ingress_classes" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Ingress classes
|
||||
<portainer-tooltip position="bottom" message="Provide a comma separated list of all the ingress classes available in your cluster."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="ingress_classes"
|
||||
id="ingress_classes"
|
||||
ng-model="ctrl.formValues.IngressClasses"
|
||||
placeholder="nginx,gce,traefik"
|
||||
required
|
||||
/>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Ingress controller</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addIngressClass()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add ingress controller
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 small text-warning" ng-show="kubernetesClusterSetupForm.ingress_classes.$invalid">
|
||||
<div ng-messages="kubernetesClusterSetupForm.ingress_classes.$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 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
|
||||
/>
|
||||
</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
|
||||
>
|
||||
<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-sm btn-danger" type="button" ng-click="ctrl.removeIngressClass($index)">
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button ng-if="ingressClass.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreIngressClass($index)">
|
||||
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||
</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
|
||||
"
|
||||
>
|
||||
<div class="col-sm-7 input-group">
|
||||
<div
|
||||
class="small text-warning"
|
||||
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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress class name is required.</p>
|
||||
<p ng-message="pattern"
|
||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> 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">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This ingress class is already defined.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 input-group">
|
||||
<div class="small text-warning" 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"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress class type is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import _ from 'lodash-es';
|
||||
import * as _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import { KubernetesStorageClassAccessPolicies, KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
|
||||
|
||||
class KubernetesConfigureController {
|
||||
/* #region CONSTRUCTOR */
|
||||
/* @ngInject */
|
||||
constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider) {
|
||||
constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider, ModalService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$stateParams = $stateParams;
|
||||
|
@ -12,11 +17,16 @@ class KubernetesConfigureController {
|
|||
this.KubernetesStorageService = KubernetesStorageService;
|
||||
this.EndpointService = EndpointService;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.ModalService = ModalService;
|
||||
|
||||
this.IngressClassTypes = KubernetesIngressClassTypes;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.configureAsync = this.configureAsync.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region STORAGE CLASSES UI MANAGEMENT */
|
||||
storageClassAvailable() {
|
||||
return this.StorageClasses && this.StorageClasses.length > 0;
|
||||
}
|
||||
|
@ -28,55 +38,99 @@ class KubernetesConfigureController {
|
|||
valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return valid;
|
||||
}
|
||||
/* #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.hasDuplicates = 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();
|
||||
}
|
||||
/* #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.IngressClasses = ingressClasses;
|
||||
}
|
||||
|
||||
transformFormValues() {
|
||||
const storageClasses = _.map(this.StorageClasses, (item) => {
|
||||
if (item.selected) {
|
||||
const res = new KubernetesStorageClass();
|
||||
res.Name = item.Name;
|
||||
res.AccessModes = _.map(item.AccessModes, 'Name');
|
||||
res.Provisioner = item.Provisioner;
|
||||
res.AllowVolumeExpansion = item.AllowVolumeExpansion;
|
||||
return res;
|
||||
}
|
||||
});
|
||||
_.pull(storageClasses, undefined);
|
||||
|
||||
const ingressClasses = _.without(
|
||||
_.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic)),
|
||||
undefined
|
||||
);
|
||||
_.pull(ingressClasses, undefined);
|
||||
|
||||
return [storageClasses, ingressClasses];
|
||||
}
|
||||
|
||||
async configureAsync() {
|
||||
try {
|
||||
this.state.actionInProgress = true;
|
||||
const classes = _.without(
|
||||
_.map(this.StorageClasses, (item) => {
|
||||
if (item.selected) {
|
||||
const res = new KubernetesStorageClass();
|
||||
res.Name = item.Name;
|
||||
res.AccessModes = _.map(item.AccessModes, 'Name');
|
||||
res.Provisioner = item.Provisioner;
|
||||
res.AllowVolumeExpansion = item.AllowVolumeExpansion;
|
||||
return res;
|
||||
}
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
const [storageClasses, ingressClasses] = this.transformFormValues();
|
||||
|
||||
this.endpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
this.endpoint.Kubernetes.Configuration.UseIngress = this.formValues.UseIngress;
|
||||
if (this.formValues.UseIngress) {
|
||||
this.endpoint.Kubernetes.Configuration.IngressClasses = _.split(this.formValues.IngressClasses, ',');
|
||||
}
|
||||
this.assignFormValuesToEndpoint(this.endpoint, storageClasses, ingressClasses);
|
||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||
|
||||
const storagePromises = _.map(classes, (storageClass) => {
|
||||
const storagePromises = _.map(storageClasses, (storageClass) => {
|
||||
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||
if (oldStorageClass) {
|
||||
return this.KubernetesStorageService.patch(this.state.endpointId, oldStorageClass, storageClass);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(storagePromises);
|
||||
|
||||
const endpoints = this.EndpointProvider.endpoints();
|
||||
const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id);
|
||||
if (modifiedEndpoint) {
|
||||
modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||
modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||
modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||
modifiedEndpoint.Kubernetes.Configuration.UseIngress = this.formValues.UseIngress;
|
||||
if (this.formValues.UseIngress) {
|
||||
modifiedEndpoint.Kubernetes.Configuration.IngressClasses = _.split(this.formValues.IngressClasses, ',');
|
||||
}
|
||||
this.assignFormValuesToEndpoint(modifiedEndpoint, storageClasses, ingressClasses);
|
||||
this.EndpointProvider.setEndpoints(endpoints);
|
||||
}
|
||||
this.Notifications.success('Configuration successfully applied');
|
||||
|
@ -89,22 +143,38 @@ class KubernetesConfigureController {
|
|||
}
|
||||
|
||||
configure() {
|
||||
return this.$async(this.configureAsync);
|
||||
const toDel = _.filter(this.formValues.IngressClasses, { NeedsDeletion: true });
|
||||
if (toDel.length) {
|
||||
this.ModalService.confirmUpdate(
|
||||
`Removing ingress controllers will make them unavailable for future use.<br/>Existing resources linked to these ingress controllers will continue to live in cluster but you will not be able to remove them from Portainer.<br/><br/>Do you wish to continue?`,
|
||||
(confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.$async(this.configureAsync);
|
||||
}
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
displayConfigureClassPanel: {},
|
||||
viewReady: false,
|
||||
endpointId: this.$stateParams.id,
|
||||
duplicates: {
|
||||
ingressClasses: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
};
|
||||
|
||||
this.formValues = {
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
UseIngress: false,
|
||||
IngressClasses: '',
|
||||
IngressClasses: [],
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -127,8 +197,11 @@ class KubernetesConfigureController {
|
|||
|
||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||
this.formValues.UseIngress = this.endpoint.Kubernetes.Configuration.UseIngress;
|
||||
this.formValues.IngressClasses = _.join(this.endpoint.Kubernetes.Configuration.IngressClasses);
|
||||
this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => {
|
||||
ic.IsNew = false;
|
||||
ic.NeedsDeletion = false;
|
||||
return ic;
|
||||
});
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
||||
} finally {
|
||||
|
@ -139,6 +212,7 @@ class KubernetesConfigureController {
|
|||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
/* #endregion */
|
||||
}
|
||||
|
||||
export default KubernetesConfigureController;
|
||||
|
|
|
@ -129,54 +129,126 @@
|
|||
</div>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
</div>
|
||||
<!-- #region INGRESSES -->
|
||||
<div class="form-group" ng-if="!ctrl.state.canUseIngress">
|
||||
<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 ng-if="ctrl.state.canUseIngress">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ingresses
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.canUseIngress">
|
||||
<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>
|
||||
You can enable one or multiple ingresses to be used when deploying an application inside this resource pool.
|
||||
</p>
|
||||
<!-- #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="col-sm-12">
|
||||
<table class="table" style="table-layout: fixed;">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%;">Ingress controller</td>
|
||||
<td style="width: 66%;">
|
||||
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>
|
||||
<span>{{ class.Name }}</span>
|
||||
</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>
|
||||
<!-- #endregion -->
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
@ -187,7 +259,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!resourcePoolCreationForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.HasQuota && !ctrl.isQuotaValid()) || !ctrl.isValid()"
|
||||
ng-disabled="!resourcePoolCreationForm.$valid || ctrl.isCreateButtonDisabled()"
|
||||
ng-click="ctrl.createResourcePool()"
|
||||
button-spinner="ctrl.state.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -3,11 +3,15 @@ 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, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
|
||||
|
||||
class KubernetesCreateResourcePoolController {
|
||||
/* #region CONSTRUCTOR */
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, Authentication, EndpointProvider) {
|
||||
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, KubernetesIngressService, Authentication, EndpointProvider) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
|
@ -16,14 +20,42 @@ class KubernetesCreateResourcePoolController {
|
|||
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
|
||||
this.onInit = this.onInit.bind(this);
|
||||
this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this);
|
||||
this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this);
|
||||
this.getIngressesAsync = this.getIngressesAsync.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
state.refs = duplicates;
|
||||
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return !this.state.isAlreadyExist;
|
||||
/* #region ANNOTATIONS MANAGEMENT */
|
||||
addAnnotation(ingressClass) {
|
||||
ingressClass.Annotations.push(new KubernetesResourcePoolIngressClassAnnotationFormValue());
|
||||
}
|
||||
|
||||
removeAnnotation(ingressClass, index) {
|
||||
ingressClass.Annotations.splice(index, 1);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
isCreateButtonDisabled() {
|
||||
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasDuplicates;
|
||||
}
|
||||
|
||||
onChangeName() {
|
||||
|
@ -50,6 +82,7 @@ class KubernetesCreateResourcePoolController {
|
|||
}
|
||||
}
|
||||
|
||||
/* #region CREATE RESOURCE POOL */
|
||||
async createResourcePoolAsync() {
|
||||
this.state.actionInProgress = true;
|
||||
try {
|
||||
|
@ -69,7 +102,26 @@ class KubernetesCreateResourcePoolController {
|
|||
createResourcePool() {
|
||||
return this.$async(this.createResourcePoolAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET INGRESSES */
|
||||
async getIngressesAsync() {
|
||||
this.state.ingressesLoading = true;
|
||||
try {
|
||||
this.allIngresses = await this.KubernetesIngressService.get();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
|
||||
} finally {
|
||||
this.state.ingressesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
getIngresses() {
|
||||
return this.$async(this.getIngressesAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region GET RESOURCE POOLS */
|
||||
async getResourcePoolsAsync() {
|
||||
try {
|
||||
this.resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
|
@ -81,7 +133,9 @@ class KubernetesCreateResourcePoolController {
|
|||
getResourcePools() {
|
||||
return this.$async(this.getResourcePoolsAsync);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
try {
|
||||
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||
|
@ -95,7 +149,10 @@ class KubernetesCreateResourcePoolController {
|
|||
sliderMaxCpu: 0,
|
||||
viewReady: false,
|
||||
isAlreadyExist: false,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.UseIngress,
|
||||
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
|
||||
duplicates: {
|
||||
ingressHosts: new KubernetesFormValueDuplicate(),
|
||||
},
|
||||
};
|
||||
|
||||
const nodes = await this.KubernetesNodeService.get();
|
||||
|
@ -107,8 +164,9 @@ class KubernetesCreateResourcePoolController {
|
|||
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
||||
await this.getResourcePools();
|
||||
if (this.state.canUseIngress) {
|
||||
await this.getIngresses();
|
||||
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||
this.formValues.IngressClasses = _.map(ingressClasses, (item) => new KubernetesResourcePoolIngressClassFormValue(item));
|
||||
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses);
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||
|
@ -120,6 +178,7 @@ class KubernetesCreateResourcePoolController {
|
|||
$onInit() {
|
||||
return this.$async(this.onInit);
|
||||
}
|
||||
/* #endregion */
|
||||
}
|
||||
|
||||
export default KubernetesCreateResourcePoolController;
|
||||
|
|
|
@ -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…
Reference in New Issue