mirror of
https://github.com/portainer/portainer.git
synced 2025-07-21 22:39:41 +02:00
feat(k8s/applications): expose applications via ingress (#4136)
* feat(k8s/endpoint): expose ingress controllers on endpoints * feat(k8s/applications): add ability to expose applications over ingress - missing RP and app edits * feat(k8s/application): add validation for ingress routes * feat(k8s/resource-pools): edit available ingress classes * fix(k8s/ingress): var name refactor was partially applied * feat(kubernetes): double validation on RP edit * feat(k8s/application): app edit ingress update + formvalidation + UI rework * feat(k8s/ingress): dictionary for default annotations on ingress creation * fix(k8s/application): temporary fix + TODO dev notice * feat(k8s/application): select default ingress of selected resource pool * feat(k8s/ingress): revert ingressClassName removal * feat(k8s/ingress): admins can now add an host to ingress in a resource pool * feat(k8s/resource-pool): list applications using RP ingresses * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * feat(k8s/configure): minor UI update * fix(k8s/ingresses): remove host if undefined * feat(k8s/resource-pool): remove the activate ingresses switch * fix(k8s/resource-pool): edditing an ingress host was deleting all the routes of the ingress * feat(k8s/application): prevent app deploy if no ports to publish and publishing type not internal * feat(k8s/ingress): minor UI update * fix(k8s/ingress): allow routes without prepending / * feat(k8s/application): add form validation on ingress route Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
parent
201c3ac143
commit
f91d3f1ca3
31 changed files with 1595 additions and 443 deletions
|
@ -5,7 +5,9 @@ func KubernetesDefault() KubernetesData {
|
||||||
Configuration: KubernetesConfiguration{
|
Configuration: KubernetesConfiguration{
|
||||||
UseLoadBalancer: false,
|
UseLoadBalancer: false,
|
||||||
UseServerMetrics: false,
|
UseServerMetrics: false,
|
||||||
|
UseIngress: false,
|
||||||
StorageClasses: []KubernetesStorageClassConfig{},
|
StorageClasses: []KubernetesStorageClassConfig{},
|
||||||
|
IngressClasses: []string{},
|
||||||
},
|
},
|
||||||
Snapshots: []KubernetesSnapshot{},
|
Snapshots: []KubernetesSnapshot{},
|
||||||
}
|
}
|
||||||
|
|
|
@ -332,7 +332,9 @@ type (
|
||||||
KubernetesConfiguration struct {
|
KubernetesConfiguration struct {
|
||||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||||
|
UseIngress bool `json:"UseIngress"`
|
||||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||||
|
IngressClasses []string `json:"IngressClasses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
<div class="col-sm-1"></div>
|
<div class="col-sm-1"></div>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
<button type="button" class="btn btn-sm btn-danger" style="margin-left: 0;" ng-click="$ctrl.removeEntry(index)">
|
<button type="button" class="btn btn-sm btn-danger" style="margin-left: 0;" ng-click="$ctrl.removeEntry(index)">
|
||||||
<i class="fa fa-trash" aria-hidden="true"></i> Remove entry
|
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove entry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,7 +49,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class KubernetesApplicationConverter {
|
class KubernetesApplicationConverter {
|
||||||
static applicationCommon(res, data, service, ingressRules) {
|
static applicationCommon(res, data, service, ingresses) {
|
||||||
res.Id = data.metadata.uid;
|
res.Id = data.metadata.uid;
|
||||||
res.Name = data.metadata.name;
|
res.Name = data.metadata.name;
|
||||||
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
|
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-';
|
||||||
|
@ -111,7 +111,7 @@ class KubernetesApplicationConverter {
|
||||||
|
|
||||||
const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports));
|
const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports));
|
||||||
const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs);
|
const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs);
|
||||||
const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingressRules, service);
|
const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingresses, service.metadata.name);
|
||||||
_.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port)));
|
_.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port)));
|
||||||
res.PublishedPorts = ports;
|
res.PublishedPorts = ports;
|
||||||
}
|
}
|
||||||
|
@ -210,9 +210,9 @@ class KubernetesApplicationConverter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiDeploymentToApplication(data, service, ingressRules) {
|
static apiDeploymentToApplication(data, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
|
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
|
res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
||||||
|
@ -221,9 +221,9 @@ class KubernetesApplicationConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiDaemonSetToApplication(data, service, ingressRules) {
|
static apiDaemonSetToApplication(data, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
|
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
|
res.ApplicationType = KubernetesApplicationTypes.DAEMONSET;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED;
|
||||||
|
@ -232,9 +232,9 @@ class KubernetesApplicationConverter {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static apiStatefulSetToapplication(data, service, ingressRules) {
|
static apiStatefulSetToapplication(data, service, ingresses) {
|
||||||
const res = new KubernetesApplication();
|
const res = new KubernetesApplication();
|
||||||
KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules);
|
KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses);
|
||||||
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
|
res.ApplicationType = KubernetesApplicationTypes.STATEFULSET;
|
||||||
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED;
|
||||||
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
|
||||||
|
@ -261,15 +261,18 @@ class KubernetesApplicationConverter {
|
||||||
res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
|
res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders
|
||||||
res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
|
res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations);
|
||||||
res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount);
|
res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount);
|
||||||
|
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts);
|
||||||
|
|
||||||
|
const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length;
|
||||||
if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER;
|
||||||
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) {
|
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && !isIngress) {
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER;
|
res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER;
|
||||||
|
} else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && isIngress) {
|
||||||
|
res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS;
|
||||||
} else {
|
} else {
|
||||||
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
||||||
}
|
}
|
||||||
res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +316,7 @@ class KubernetesApplicationConverter {
|
||||||
if (!service.Ports.length) {
|
if (!service.Ports.length) {
|
||||||
service = undefined;
|
service = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [app, headlessService, service, claims];
|
return [app, headlessService, service, claims];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import * as JsonPatch from 'fast-json-patch';
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads';
|
||||||
|
@ -11,8 +11,9 @@ import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServic
|
||||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models';
|
||||||
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
||||||
|
|
||||||
class KubernetesServiceConverter {
|
function _publishedPortToServicePort(formValues, publishedPort, type) {
|
||||||
static publishedPortToServicePort(name, publishedPort, type) {
|
if (publishedPort.IsNew || !publishedPort.NeedsDeletion) {
|
||||||
|
const name = formValues.Name;
|
||||||
const res = new KubernetesServicePort();
|
const res = new KubernetesServicePort();
|
||||||
res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol);
|
res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol);
|
||||||
res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort;
|
res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort;
|
||||||
|
@ -27,7 +28,9 @@ class KubernetesServiceConverter {
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KubernetesServiceConverter {
|
||||||
/**
|
/**
|
||||||
* Generate KubernetesService from KubernetesApplicationFormValues
|
* Generate KubernetesService from KubernetesApplicationFormValues
|
||||||
* @param {KubernetesApplicationFormValues} formValues
|
* @param {KubernetesApplicationFormValues} formValues
|
||||||
|
@ -39,12 +42,13 @@ class KubernetesServiceConverter {
|
||||||
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
|
||||||
res.ApplicationOwner = formValues.ApplicationOwner;
|
res.ApplicationOwner = formValues.ApplicationOwner;
|
||||||
res.ApplicationName = formValues.Name;
|
res.ApplicationName = formValues.Name;
|
||||||
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
|
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER || formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
res.Type = KubernetesServiceTypes.NODE_PORT;
|
res.Type = KubernetesServiceTypes.NODE_PORT;
|
||||||
} else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
} else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||||
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
|
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
|
||||||
}
|
}
|
||||||
res.Ports = _.map(formValues.PublishedPorts, (item) => KubernetesServiceConverter.publishedPortToServicePort(formValues.Name, item, res.Type));
|
const ports = _.map(formValues.PublishedPorts, (item) => _publishedPortToServicePort(formValues, item, res.Type));
|
||||||
|
res.Ports = _.uniqBy(_.without(ports, undefined), (p) => p.targetPort + p.protocol);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -249,8 +249,14 @@ class KubernetesApplicationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) {
|
||||||
const finalRes = _.map(publishedPorts, (port) => {
|
const generatePort = (port, rule) => {
|
||||||
const res = new KubernetesApplicationPublishedPortFormValue();
|
const res = new KubernetesApplicationPublishedPortFormValue();
|
||||||
|
res.IsNew = false;
|
||||||
|
if (rule) {
|
||||||
|
res.IngressName = rule.IngressName;
|
||||||
|
res.IngressRoute = rule.Path;
|
||||||
|
res.IngressHost = rule.Host;
|
||||||
|
}
|
||||||
res.Protocol = port.Protocol;
|
res.Protocol = port.Protocol;
|
||||||
res.ContainerPort = port.TargetPort;
|
res.ContainerPort = port.TargetPort;
|
||||||
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
|
||||||
|
@ -260,6 +266,13 @@ class KubernetesApplicationHelper {
|
||||||
res.NodePort = port.NodePort;
|
res.NodePort = port.NodePort;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalRes = _.flatMap(publishedPorts, (port) => {
|
||||||
|
if (port.IngressRules.length) {
|
||||||
|
return _.map(port.IngressRules, (rule) => generatePort(port, rule));
|
||||||
|
}
|
||||||
|
return generatePort(port);
|
||||||
});
|
});
|
||||||
return finalRes;
|
return finalRes;
|
||||||
}
|
}
|
||||||
|
|
4
app/kubernetes/ingress/constants.js
Normal file
4
app/kubernetes/ingress/constants.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const KubernetesIngressClassAnnotation = 'kubernetes.io/ingress.class';
|
||||||
|
export const KubernetesIngressClassMandatoryAnnotations = Object.freeze({
|
||||||
|
nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/$1' },
|
||||||
|
});
|
|
@ -1,19 +1,113 @@
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { KubernetesIngressRule } from './models';
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
|
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
|
||||||
|
import { KubernetesIngressRule, KubernetesIngress } from './models';
|
||||||
|
import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads';
|
||||||
|
import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants';
|
||||||
|
|
||||||
export class KubernetesIngressConverter {
|
export class KubernetesIngressConverter {
|
||||||
|
// TODO: refactor @LP
|
||||||
|
// currently only allows the first non-empty host to be used as the "configured" host.
|
||||||
|
// As we currently only allow a single host to be used for a Portianer-managed ingress
|
||||||
|
// it's working as the only non-empty host will be the one defined by the admin
|
||||||
|
// but it will take a random existing host for non Portainer ingresses (CLI deployed)
|
||||||
|
// Also won't support multiple hosts if we make it available in the future
|
||||||
static apiToModel(data) {
|
static apiToModel(data) {
|
||||||
const rules = _.flatMap(data.spec.rules, (rule) => {
|
let host = undefined;
|
||||||
return _.map(rule.http.paths, (path) => {
|
const paths = _.flatMap(data.spec.rules, (rule) => {
|
||||||
|
host = host || rule.host; // TODO: refactor @LP - read above
|
||||||
|
return !rule.http
|
||||||
|
? []
|
||||||
|
: _.map(rule.http.paths, (path) => {
|
||||||
const ingRule = new KubernetesIngressRule();
|
const ingRule = new KubernetesIngressRule();
|
||||||
|
ingRule.IngressName = data.metadata.name;
|
||||||
ingRule.ServiceName = path.backend.serviceName;
|
ingRule.ServiceName = path.backend.serviceName;
|
||||||
ingRule.Host = rule.host;
|
ingRule.Host = rule.host || '';
|
||||||
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
|
ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined;
|
||||||
ingRule.Port = path.backend.servicePort;
|
ingRule.Port = path.backend.servicePort;
|
||||||
ingRule.Path = path.path;
|
ingRule.Path = path.path;
|
||||||
return ingRule;
|
return ingRule;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return rules;
|
|
||||||
|
const res = new KubernetesIngress();
|
||||||
|
res.Name = data.metadata.name;
|
||||||
|
res.Namespace = data.metadata.namespace;
|
||||||
|
res.Annotations = data.metadata.annotations || {};
|
||||||
|
res.IngressClassName =
|
||||||
|
data.metadata.annotations && data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||||
|
? data.metadata.annotations[KubernetesIngressClassAnnotation]
|
||||||
|
: data.spec.ingressClassName;
|
||||||
|
res.Paths = paths;
|
||||||
|
res.Host = host;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static applicationFormValuesToIngresses(formValues, serviceName) {
|
||||||
|
const ingresses = angular.copy(formValues.OriginalIngresses);
|
||||||
|
_.forEach(formValues.PublishedPorts, (p) => {
|
||||||
|
const ingress = _.find(ingresses, { Name: p.IngressName });
|
||||||
|
if (ingress && p.NeedsDeletion) {
|
||||||
|
const path = _.find(ingress.Paths, { Port: p.ContainerPort, ServiceName: serviceName, Path: p.IngressRoute });
|
||||||
|
_.remove(ingress.Paths, path);
|
||||||
|
} else if (ingress && p.IsNew) {
|
||||||
|
const rule = new KubernetesIngressRule();
|
||||||
|
rule.IngressName = ingress.Name;
|
||||||
|
rule.ServiceName = serviceName;
|
||||||
|
rule.Port = p.ContainerPort;
|
||||||
|
rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute;
|
||||||
|
rule.Host = p.IngressHost;
|
||||||
|
ingress.Paths.push(rule);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ingresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (data.Paths && data.Paths.length) {
|
||||||
|
const groups = _.groupBy(data.Paths, 'Host');
|
||||||
|
const rules = _.map(groups, (paths, host) => {
|
||||||
|
const rule = new KubernetesIngressRuleCreatePayload();
|
||||||
|
|
||||||
|
if (host === 'undefined' || _.isEmpty(host)) {
|
||||||
|
host = data.Host;
|
||||||
|
}
|
||||||
|
if (host === data.PreviousHost && host !== data.Host) {
|
||||||
|
host = data.Host;
|
||||||
|
}
|
||||||
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host);
|
||||||
|
rule.http.paths = _.map(paths, (p) => {
|
||||||
|
const path = new KubernetesIngressRulePathCreatePayload();
|
||||||
|
path.path = p.Path;
|
||||||
|
path.backend.serviceName = p.ServiceName;
|
||||||
|
path.backend.servicePort = p.Port;
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
return rule;
|
||||||
|
});
|
||||||
|
KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules);
|
||||||
|
} else if (data.Host) {
|
||||||
|
res.spec.rules = [{ host: data.Host }];
|
||||||
|
} else {
|
||||||
|
delete res.spec.rules;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static patchPayload(oldData, newData) {
|
||||||
|
const oldPayload = KubernetesIngressConverter.createPayload(oldData);
|
||||||
|
const newPayload = KubernetesIngressConverter.createPayload(newData);
|
||||||
|
const payload = JsonPatch.compare(oldPayload, newPayload);
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import * as _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
|
|
||||||
export class KubernetesIngressHelper {
|
export class KubernetesIngressHelper {
|
||||||
static findSBoundServiceIngressesRules(ingressRules, service) {
|
static findSBoundServiceIngressesRules(ingresses, serviceName) {
|
||||||
return _.filter(ingressRules, (r) => r.ServiceName === service.metadata.name);
|
const rules = _.flatMap(ingresses, 'Paths');
|
||||||
|
return _.filter(rules, { ServiceName: serviceName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
/**
|
export function KubernetesIngress() {
|
||||||
* KubernetesIngressRule Model
|
return {
|
||||||
*/
|
Name: '',
|
||||||
const _KubernetesIngressRule = Object.freeze({
|
Namespace: '',
|
||||||
|
Annotations: {},
|
||||||
|
Host: undefined,
|
||||||
|
PreviousHost: undefined, // only use for RP ingress host edit
|
||||||
|
Paths: [],
|
||||||
|
IngressClassName: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor @LP
|
||||||
|
// rename this model to KubernetesIngressPath (and all it's references)
|
||||||
|
// as it's conceptually not an ingress rule (element of ingress.spec.rules)
|
||||||
|
// but a path (element of ingress.spec.rules[].paths)
|
||||||
|
export function KubernetesIngressRule() {
|
||||||
|
return {
|
||||||
|
IngressName: '',
|
||||||
ServiceName: '',
|
ServiceName: '',
|
||||||
Host: '',
|
Host: '',
|
||||||
IP: '',
|
IP: '',
|
||||||
Port: '',
|
Port: '',
|
||||||
Path: '',
|
Path: '',
|
||||||
});
|
};
|
||||||
|
|
||||||
export class KubernetesIngressRule {
|
|
||||||
constructor() {
|
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressRule)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
33
app/kubernetes/ingress/payloads.js
Normal file
33
app/kubernetes/ingress/payloads.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
|
||||||
|
|
||||||
|
export function KubernetesIngressCreatePayload() {
|
||||||
|
return {
|
||||||
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
|
spec: {
|
||||||
|
backend: {
|
||||||
|
serviceName: 'portainer-empty-default-backend',
|
||||||
|
servicePort: 1,
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KubernetesIngressRuleCreatePayload() {
|
||||||
|
return {
|
||||||
|
host: '',
|
||||||
|
http: {
|
||||||
|
paths: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KubernetesIngressRulePathCreatePayload() {
|
||||||
|
return {
|
||||||
|
backend: {
|
||||||
|
serviceName: '',
|
||||||
|
servicePort: 0,
|
||||||
|
},
|
||||||
|
path: '',
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
import { rawResponse } from 'Kubernetes/rest/response/transform';
|
import { rawResponse } from 'Kubernetes/rest/response/transform';
|
||||||
|
|
||||||
angular.module('portainer.kubernetes').factory('KubernetesIngresses', [
|
angular.module('portainer.kubernetes').factory('KubernetesIngresses', factory);
|
||||||
'$resource',
|
|
||||||
'API_ENDPOINT_ENDPOINTS',
|
function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
'EndpointProvider',
|
|
||||||
function KubernetesIngressesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (namespace) {
|
return function (namespace) {
|
||||||
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1' + (namespace ? '/namespaces/:namespace' : '') + '/ingresses/:id/:action';
|
const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`;
|
||||||
return $resource(
|
return $resource(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
|
@ -46,5 +44,4 @@ angular.module('portainer.kubernetes').factory('KubernetesIngresses', [
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
]);
|
|
||||||
|
|
|
@ -12,6 +12,9 @@ class KubernetesIngressService {
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
this.getAllAsync = this.getAllAsync.bind(this);
|
this.getAllAsync = this.getAllAsync.bind(this);
|
||||||
|
this.createAsync = this.createAsync.bind(this);
|
||||||
|
this.patchAsync = this.patchAsync.bind(this);
|
||||||
|
this.deleteAsync = this.deleteAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,6 +51,66 @@ class KubernetesIngressService {
|
||||||
}
|
}
|
||||||
return this.$async(this.getAllAsync, namespace);
|
return this.$async(this.getAllAsync, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CREATE
|
||||||
|
*/
|
||||||
|
async createAsync(formValues) {
|
||||||
|
try {
|
||||||
|
const params = {};
|
||||||
|
const payload = KubernetesIngressConverter.createPayload(formValues);
|
||||||
|
const namespace = payload.metadata.namespace;
|
||||||
|
const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw new PortainerError('Unable to create ingress', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create(formValues) {
|
||||||
|
return this.$async(this.createAsync, formValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH
|
||||||
|
*/
|
||||||
|
async patchAsync(oldIngress, newIngress) {
|
||||||
|
try {
|
||||||
|
const params = new KubernetesCommonParams();
|
||||||
|
params.id = newIngress.Name;
|
||||||
|
const namespace = newIngress.Namespace;
|
||||||
|
const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress);
|
||||||
|
if (!payload.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await this.KubernetesIngresses(namespace).patch(params, payload).$promise;
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw new PortainerError('Unable to patch ingress', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(oldIngress, newIngress) {
|
||||||
|
return this.$async(this.patchAsync, oldIngress, newIngress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE
|
||||||
|
*/
|
||||||
|
async deleteAsync(ingress) {
|
||||||
|
try {
|
||||||
|
const params = new KubernetesCommonParams();
|
||||||
|
params.id = ingress.Name;
|
||||||
|
const namespace = ingress.Namespace;
|
||||||
|
await this.KubernetesIngresses(namespace).delete(params).$promise;
|
||||||
|
} catch (err) {
|
||||||
|
throw new PortainerError('Unable to delete ingress', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(ingress) {
|
||||||
|
return this.$async(this.deleteAsync, ingress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesIngressService;
|
export default KubernetesIngressService;
|
||||||
|
|
|
@ -22,6 +22,7 @@ const _KubernetesApplicationFormValues = Object.freeze({
|
||||||
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
|
DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED,
|
||||||
Configurations: [], // KubernetesApplicationConfigurationFormValue list
|
Configurations: [], // KubernetesApplicationConfigurationFormValue list
|
||||||
AutoScaler: {},
|
AutoScaler: {},
|
||||||
|
OriginalIngresses: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationFormValues {
|
export class KubernetesApplicationFormValues {
|
||||||
|
@ -106,18 +107,19 @@ export class KubernetesApplicationPersistedFolderFormValue {
|
||||||
/**
|
/**
|
||||||
* KubernetesApplicationPublishedPortFormValue Model
|
* KubernetesApplicationPublishedPortFormValue Model
|
||||||
*/
|
*/
|
||||||
const _KubernetesApplicationPublishedPortFormValue = Object.freeze({
|
export function KubernetesApplicationPublishedPortFormValue() {
|
||||||
|
return {
|
||||||
|
NeedsDeletion: false,
|
||||||
|
IsNew: true,
|
||||||
ContainerPort: '',
|
ContainerPort: '',
|
||||||
NodePort: '',
|
NodePort: '',
|
||||||
LoadBalancerPort: '',
|
LoadBalancerPort: '',
|
||||||
LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort
|
LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort
|
||||||
Protocol: 'TCP',
|
Protocol: 'TCP',
|
||||||
});
|
IngressName: undefined,
|
||||||
|
IngressRoute: undefined,
|
||||||
export class KubernetesApplicationPublishedPortFormValue {
|
IngressHost: undefined,
|
||||||
constructor() {
|
};
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const KubernetesApplicationPublishingTypes = Object.freeze({
|
||||||
INTERNAL: 1,
|
INTERNAL: 1,
|
||||||
CLUSTER: 2,
|
CLUSTER: 2,
|
||||||
LOAD_BALANCER: 3,
|
LOAD_BALANCER: 3,
|
||||||
|
INGRESS: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const KubernetesApplicationQuotaDefaults = {
|
export const KubernetesApplicationQuotaDefaults = {
|
||||||
|
|
22
app/kubernetes/models/resource-pool/formValues.js
Normal file
22
app/kubernetes/models/resource-pool/formValues.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export function KubernetesResourcePoolFormValues(defaults) {
|
||||||
|
return {
|
||||||
|
MemoryLimit: defaults.MemoryLimit,
|
||||||
|
CpuLimit: defaults.CpuLimit,
|
||||||
|
HasQuota: true,
|
||||||
|
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} ingressClassName
|
||||||
|
*/
|
||||||
|
export function KubernetesResourcePoolIngressClassFormValue(ingressClassName) {
|
||||||
|
return {
|
||||||
|
Name: ingressClassName,
|
||||||
|
IngressClassName: ingressClassName,
|
||||||
|
Host: undefined,
|
||||||
|
Selected: false,
|
||||||
|
WasSelected: false,
|
||||||
|
Namespace: undefined, // will be filled inside ResourcePoolService.create
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,18 +3,13 @@ export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes
|
||||||
export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner';
|
export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubernetesResourcePool Model (Composite)
|
* KubernetesResourcePool Model
|
||||||
* ResourcePool is a composite model that includes
|
|
||||||
* A Namespace and a Quota
|
|
||||||
*/
|
*/
|
||||||
const _KubernetesResourcePool = Object.freeze({
|
export function KubernetesResourcePool() {
|
||||||
|
return {
|
||||||
Namespace: {}, // KubernetesNamespace
|
Namespace: {}, // KubernetesNamespace
|
||||||
Quota: undefined, // KubernetesResourceQuota
|
Quota: undefined, // KubernetesResourceQuota,
|
||||||
|
Ingresses: [], // KubernetesIngress[]
|
||||||
Yaml: '',
|
Yaml: '',
|
||||||
});
|
};
|
||||||
|
|
||||||
export class KubernetesResourcePool {
|
|
||||||
constructor() {
|
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourcePool)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
|
|
||||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
import { KubernetesApplicationTypes, KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||||
import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback';
|
import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback';
|
||||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
|
@ -13,8 +13,10 @@ import { KubernetesApplication } from 'Kubernetes/models/application/models';
|
||||||
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
||||||
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
||||||
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
||||||
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
|
|
||||||
class KubernetesApplicationService {
|
class KubernetesApplicationService {
|
||||||
|
/* #region CONSTRUCTOR */
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
$async,
|
$async,
|
||||||
|
@ -53,10 +55,9 @@ class KubernetesApplicationService {
|
||||||
this.rollbackAsync = this.rollbackAsync.bind(this);
|
this.rollbackAsync = this.rollbackAsync.bind(this);
|
||||||
this.deleteAsync = this.deleteAsync.bind(this);
|
this.deleteAsync = this.deleteAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region UTILS */
|
||||||
* UTILS
|
|
||||||
*/
|
|
||||||
_getApplicationApiService(app) {
|
_getApplicationApiService(app) {
|
||||||
let apiService;
|
let apiService;
|
||||||
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
|
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
|
||||||
|
@ -71,9 +72,15 @@ class KubernetesApplicationService {
|
||||||
return apiService;
|
return apiService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_generateIngressPatchPromises(oldIngresses, newIngresses) {
|
||||||
* GET
|
return _.map(newIngresses, (newIng) => {
|
||||||
*/
|
const oldIng = _.find(oldIngresses, { Name: newIng.Name });
|
||||||
|
return this.KubernetesIngressService.patch(oldIng, newIng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region GET */
|
||||||
async getAsync(namespace, name) {
|
async getAsync(namespace, name) {
|
||||||
try {
|
try {
|
||||||
const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([
|
const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([
|
||||||
|
@ -121,7 +128,7 @@ class KubernetesApplicationService {
|
||||||
if (scaler && scaler.Yaml) {
|
if (scaler && scaler.Yaml) {
|
||||||
application.Yaml += '---\n' + scaler.Yaml;
|
application.Yaml += '---\n' + scaler.Yaml;
|
||||||
}
|
}
|
||||||
// TODO: refactor
|
// TODO: refactor @LP
|
||||||
// append ingress yaml ?
|
// append ingress yaml ?
|
||||||
return application;
|
return application;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -185,10 +192,9 @@ class KubernetesApplicationService {
|
||||||
}
|
}
|
||||||
return this.$async(this.getAllAsync, namespace);
|
return this.$async(this.getAllAsync, namespace);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region CREATE */
|
||||||
* CREATE
|
|
||||||
*/
|
|
||||||
// TODO: review
|
// TODO: review
|
||||||
// resource creation flow
|
// resource creation flow
|
||||||
// should we keep formValues > Resource_1 || Resource_2
|
// should we keep formValues > Resource_1 || Resource_2
|
||||||
|
@ -199,6 +205,10 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
if (service) {
|
if (service) {
|
||||||
await this.KubernetesServiceService.create(service);
|
await this.KubernetesServiceService.create(service);
|
||||||
|
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name);
|
||||||
|
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiService = this._getApplicationApiService(app);
|
const apiService = this._getApplicationApiService(app);
|
||||||
|
@ -231,10 +241,9 @@ class KubernetesApplicationService {
|
||||||
create(formValues) {
|
create(formValues) {
|
||||||
return this.$async(this.createAsync, formValues);
|
return this.$async(this.createAsync, formValues);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region PATCH */
|
||||||
* PATCH
|
|
||||||
*/
|
|
||||||
// this function accepts KubernetesApplicationFormValues as parameters
|
// this function accepts KubernetesApplicationFormValues as parameters
|
||||||
async patchAsync(oldFormValues, newFormValues) {
|
async patchAsync(oldFormValues, newFormValues) {
|
||||||
try {
|
try {
|
||||||
|
@ -269,10 +278,23 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
if (oldService && newService) {
|
if (oldService && newService) {
|
||||||
await this.KubernetesServiceService.patch(oldService, newService);
|
await this.KubernetesServiceService.patch(oldService, newService);
|
||||||
|
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
|
||||||
|
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||||
|
await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses));
|
||||||
|
}
|
||||||
} else if (!oldService && newService) {
|
} else if (!oldService && newService) {
|
||||||
await this.KubernetesServiceService.create(newService);
|
await this.KubernetesServiceService.create(newService);
|
||||||
|
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||||
|
await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
} else if (oldService && !newService) {
|
} else if (oldService && !newService) {
|
||||||
await this.KubernetesServiceService.delete(oldService);
|
await this.KubernetesServiceService.delete(oldService);
|
||||||
|
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
|
||||||
|
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
||||||
|
@ -327,10 +349,9 @@ class KubernetesApplicationService {
|
||||||
}
|
}
|
||||||
return this.$async(this.patchAsync, oldValues, newValues);
|
return this.$async(this.patchAsync, oldValues, newValues);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region DELETE */
|
||||||
* DELETE
|
|
||||||
*/
|
|
||||||
async deleteAsync(application) {
|
async deleteAsync(application) {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
@ -351,8 +372,18 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
if (application.ServiceType) {
|
if (application.ServiceType) {
|
||||||
await this.KubernetesServiceService.delete(servicePayload);
|
await this.KubernetesServiceService.delete(servicePayload);
|
||||||
|
const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length;
|
||||||
|
if (isIngress) {
|
||||||
|
const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace);
|
||||||
|
const formValues = {
|
||||||
|
OriginalIngresses: originalIngresses,
|
||||||
|
PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts),
|
||||||
|
};
|
||||||
|
_.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true));
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name);
|
||||||
|
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(application.AutoScaler)) {
|
if (!_.isEmpty(application.AutoScaler)) {
|
||||||
await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler);
|
await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler);
|
||||||
}
|
}
|
||||||
|
@ -364,10 +395,9 @@ class KubernetesApplicationService {
|
||||||
delete(application) {
|
delete(application) {
|
||||||
return this.$async(this.deleteAsync, application);
|
return this.$async(this.deleteAsync, application);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region ROLLBACK */
|
||||||
* ROLLBACK
|
|
||||||
*/
|
|
||||||
async rollbackAsync(application, targetRevision) {
|
async rollbackAsync(application, targetRevision) {
|
||||||
try {
|
try {
|
||||||
const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision);
|
const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision);
|
||||||
|
@ -381,6 +411,7 @@ class KubernetesApplicationService {
|
||||||
rollback(application, targetRevision) {
|
rollback(application, targetRevision) {
|
||||||
return this.$async(this.rollbackAsync, application, targetRevision);
|
return this.$async(this.rollbackAsync, application, targetRevision);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesApplicationService;
|
export default KubernetesApplicationService;
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models';
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool';
|
import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool';
|
||||||
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
|
||||||
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
|
||||||
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
|
|
||||||
class KubernetesResourcePoolService {
|
class KubernetesResourcePoolService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService) {
|
constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||||
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
||||||
|
this.KubernetesIngressService = KubernetesIngressService;
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
this.getAllAsync = this.getAllAsync.bind(this);
|
this.getAllAsync = this.getAllAsync.bind(this);
|
||||||
|
@ -67,30 +69,37 @@ class KubernetesResourcePoolService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CREATE
|
* CREATE
|
||||||
|
* @param {KubernetesResourcePoolFormValues} formValues
|
||||||
*/
|
*/
|
||||||
// TODO: review LimitRange future
|
async createAsync(formValues) {
|
||||||
async createAsync(name, owner, hasQuota, cpuLimit, memoryLimit) {
|
|
||||||
try {
|
try {
|
||||||
const namespace = new KubernetesNamespace();
|
const namespace = new KubernetesNamespace();
|
||||||
namespace.Name = name;
|
namespace.Name = formValues.Name;
|
||||||
namespace.ResourcePoolName = name;
|
namespace.ResourcePoolName = formValues.Name;
|
||||||
namespace.ResourcePoolOwner = owner;
|
namespace.ResourcePoolOwner = formValues.Owner;
|
||||||
await this.KubernetesNamespaceService.create(namespace);
|
await this.KubernetesNamespaceService.create(namespace);
|
||||||
if (hasQuota) {
|
if (formValues.HasQuota) {
|
||||||
const quota = new KubernetesResourceQuota(name);
|
const quota = new KubernetesResourceQuota(formValues.Name);
|
||||||
quota.CpuLimit = cpuLimit;
|
quota.CpuLimit = formValues.CpuLimit;
|
||||||
quota.MemoryLimit = memoryLimit;
|
quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit);
|
||||||
quota.ResourcePoolName = name;
|
quota.ResourcePoolName = formValues.Name;
|
||||||
quota.ResourcePoolOwner = owner;
|
quota.ResourcePoolOwner = formValues.Owner;
|
||||||
await this.KubernetesResourceQuotaService.create(quota);
|
await this.KubernetesResourceQuotaService.create(quota);
|
||||||
}
|
}
|
||||||
|
const ingressPromises = _.map(formValues.IngressClasses, (c) => {
|
||||||
|
if (c.Selected) {
|
||||||
|
c.Namespace = namespace.Name;
|
||||||
|
return this.KubernetesIngressService.create(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(ingressPromises);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
create(name, owner, hasQuota, cpuLimit, memoryLimit) {
|
create(formValues) {
|
||||||
return this.$async(this.createAsync, name, owner, hasQuota, cpuLimit, memoryLimit);
|
return this.$async(this.createAsync, formValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
|
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
|
||||||
<!-- name -->
|
<!-- #region NAME FIELD -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="application_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="application_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
@ -48,9 +48,9 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- image -->
|
<!-- #region IMAGE FIELD -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
@ -64,13 +64,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !image -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Resource pool
|
Resource pool
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region RESOURCE POOL -->
|
||||||
<!-- resource-pool -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
|
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
@ -91,12 +90,12 @@
|
||||||
resource pool.
|
resource pool.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !resource-pool -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Stack
|
Stack
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region STACK -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -105,7 +104,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- stack -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="stack_name" class="col-sm-1 control-label text-left">Stack</label>
|
<label for="stack_name" class="col-sm-1 control-label text-left">Stack</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
@ -121,13 +119,12 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !stack -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Environment
|
Environment
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region ENVIRONMENT VARIABLES -->
|
||||||
<!-- environment-variables -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">Environment variables</label>
|
<label class="control-label text-left">Environment variables</label>
|
||||||
|
@ -146,7 +143,7 @@
|
||||||
name="environment_variable_name_{{ $index }}"
|
name="environment_variable_name_{{ $index }}"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
ng-model="envVar.Name"
|
ng-model="envVar.Name"
|
||||||
ng-change="ctrl.onChangeEnvironmentName($index)"
|
ng-change="ctrl.onChangeEnvironmentName()"
|
||||||
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
|
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
|
||||||
placeholder="foo"
|
placeholder="foo"
|
||||||
required
|
required
|
||||||
|
@ -155,7 +152,9 @@
|
||||||
<div
|
<div
|
||||||
class="small text-warning"
|
class="small text-warning"
|
||||||
style="margin-top: 5px;"
|
style="margin-top: 5px;"
|
||||||
ng-show="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
|
ng-show="
|
||||||
|
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
|
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
|
||||||
|
@ -164,7 +163,7 @@
|
||||||
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
|
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
|
||||||
>
|
>
|
||||||
</ng-messages>
|
</ng-messages>
|
||||||
<p ng-if="ctrl.state.duplicateEnvironmentVariables[$index] !== undefined"
|
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
|
||||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,22 +176,21 @@
|
||||||
|
|
||||||
<div class="input-group col-sm-2 input-group-sm">
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable($index)">
|
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable($index)">
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable($index)">
|
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable($index)">
|
||||||
Restore
|
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !environment-variables -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Configurations
|
Configurations
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region CONFIGURATIONS -->
|
||||||
<!-- configurations -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">Configurations</label>
|
<label class="control-label text-left">Configurations</label>
|
||||||
|
@ -231,7 +229,7 @@
|
||||||
<button class="btn btn-sm btn-primary" type="button" ng-if="config.Overriden" ng-click="ctrl.resetConfiguration(index)">
|
<button class="btn btn-sm btn-primary" type="button" ng-if="config.Overriden" ng-click="ctrl.resetConfiguration(index)">
|
||||||
<i class="fa fa-undo" aria-hidden="true"></i> Auto
|
<i class="fa fa-undo" aria-hidden="true"></i> Auto
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)"> <i class="fa fa-trash" aria-hidden="true"></i> Remove </button>
|
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)"> <i class="fa fa-trash-alt" aria-hidden="true"></i> Remove </button>
|
||||||
</div>
|
</div>
|
||||||
<!-- no-override -->
|
<!-- no-override -->
|
||||||
<div class="col-sm-12" style="margin-top: 10px;" ng-if="config.SelectedConfiguration && !config.Overriden">
|
<div class="col-sm-12" style="margin-top: 10px;" ng-if="config.SelectedConfiguration && !config.Overriden">
|
||||||
|
@ -277,13 +275,13 @@
|
||||||
style="margin-top: 5px;"
|
style="margin-top: 5px;"
|
||||||
ng-show="
|
ng-show="
|
||||||
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
|
||||||
ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined
|
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||||
</ng-messages>
|
</ng-messages>
|
||||||
<p ng-if="ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined"
|
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
|
||||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -302,12 +300,12 @@
|
||||||
<!-- !has-override -->
|
<!-- !has-override -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !config-element -->
|
<!-- !config-element -->
|
||||||
<!-- !configurations -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Persisting data
|
Persisting data
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region PERSISTED FOLDERS -->
|
||||||
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
<div class="form-group" ng-if="!ctrl.storageClassAvailable()">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -315,7 +313,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- persisted folders -->
|
|
||||||
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
|
||||||
<div class="col-sm-12" style="margin-top: 5px;">
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
<label class="control-label text-left">Persisted folders</label>
|
<label class="control-label text-left">Persisted folders</label>
|
||||||
|
@ -333,7 +330,7 @@
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="persisted_folder_path_{{ $index }}"
|
name="persisted_folder_path_{{ $index }}"
|
||||||
ng-model="persistedFolder.ContainerPath"
|
ng-model="persistedFolder.ContainerPath"
|
||||||
ng-change="ctrl.onChangePersistedFolderPath($index)"
|
ng-change="ctrl.onChangePersistedFolderPath()"
|
||||||
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||||
placeholder="/data"
|
placeholder="/data"
|
||||||
required
|
required
|
||||||
|
@ -360,7 +357,7 @@
|
||||||
uib-btn-radio="false"
|
uib-btn-radio="false"
|
||||||
ng-change="ctrl.useExistingVolume($index)"
|
ng-change="ctrl.useExistingVolume($index)"
|
||||||
ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET"
|
ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET"
|
||||||
>Use an existing volume</label
|
>Existing volume</label
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -422,10 +419,10 @@
|
||||||
<div class="input-group col-sm-1 input-group-sm">
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()" ng-if="!ctrl.state.useExistingVolume[$index]">
|
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()" ng-if="!ctrl.state.useExistingVolume[$index]">
|
||||||
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
|
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
|
||||||
Restore
|
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -434,22 +431,22 @@
|
||||||
<div
|
<div
|
||||||
ng-show="
|
ng-show="
|
||||||
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
||||||
ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined ||
|
ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined ||
|
||||||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
|
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
|
||||||
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
|
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
|
||||||
ctrl.state.duplicateExistingVolumes[$index] !== undefined
|
ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="input-group col-sm-3 input-group-sm">
|
<div class="input-group col-sm-3 input-group-sm">
|
||||||
<div
|
<div
|
||||||
class="small text-warning"
|
class="small text-warning"
|
||||||
style="margin-top: 5px;"
|
style="margin-top: 5px;"
|
||||||
ng-show="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined"
|
ng-show="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
|
||||||
>
|
>
|
||||||
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
|
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
|
||||||
</ng-messages>
|
</ng-messages>
|
||||||
<p ng-if="ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined"
|
<p ng-if="ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
|
||||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already defined.</p
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already defined.</p
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -466,12 +463,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="small text-warning"
|
class="small text-warning"
|
||||||
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicateExistingVolumes[$index] !== undefined"
|
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
|
||||||
>
|
>
|
||||||
<ng-messages for="kubernetesApplicationCreationForm['existing_volumes_' + $index].$error">
|
<ng-messages for="kubernetesApplicationCreationForm['existing_volumes_' + $index].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Volume is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Volume is required.</p>
|
||||||
</ng-messages>
|
</ng-messages>
|
||||||
<p ng-if="ctrl.state.duplicateExistingVolumes[$index] !== undefined"
|
<p ng-if="ctrl.state.duplicates.existingVolumes.refs[$index] !== undefined"
|
||||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This volume is already used.</p
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This volume is already used.</p
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -483,8 +480,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !persisted folders -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region DATA ACCESS POLICY -->
|
||||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
<div ng-if="ctrl.showDataAccessPolicySection()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
@ -579,11 +577,12 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !access policy options -->
|
<!-- !access policy options -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Resource reservations
|
Resource reservations
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region RESOURCE RESERVATIONS -->
|
||||||
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota">
|
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -668,11 +667,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !cpu-limit-input -->
|
<!-- !cpu-limit-input -->
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Deployment
|
Deployment
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region DEPLOYMENT -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
Select how you want to deploy your application inside the cluster.
|
Select how you want to deploy your application inside the cluster.
|
||||||
|
@ -775,8 +775,9 @@
|
||||||
>. You will not be able to scale that application.
|
>. You will not be able to scale that application.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- auto scaling -->
|
<!-- #region AUTO SCALING -->
|
||||||
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL">
|
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL">
|
||||||
Auto-scaling
|
Auto-scaling
|
||||||
</div>
|
</div>
|
||||||
|
@ -884,12 +885,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !auto scaling -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Publishing the application
|
Publishing the application
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region PUBLISHING OPTIONS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
Select how you want to publish your application.
|
Select how you want to publish your application.
|
||||||
|
@ -899,9 +900,36 @@
|
||||||
<!-- publishing options -->
|
<!-- publishing options -->
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<div class="boxselector_wrapper">
|
<div class="boxselector_wrapper">
|
||||||
<div>
|
<div ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
|
||||||
<input type="radio" id="publishing_internal" ng-value="ctrl.ApplicationPublishingTypes.INTERNAL" ng-model="ctrl.formValues.PublishingType" />
|
<input
|
||||||
<label for="publishing_internal">
|
type="radio"
|
||||||
|
id="publishing_internal"
|
||||||
|
ng-value="ctrl.ApplicationPublishingTypes.INTERNAL"
|
||||||
|
ng-model="ctrl.formValues.PublishingType"
|
||||||
|
ng-change="ctrl.onChangePublishedPorts()"
|
||||||
|
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="publishing_internal"
|
||||||
|
ng-if="
|
||||||
|
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INTERNAL)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Internal
|
||||||
|
</div>
|
||||||
|
<p>Internal communications inside the cluster only</p>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="publishing_internal"
|
||||||
|
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INTERNAL"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
|
||||||
|
style="cursor: pointer; border-color: #767676;"
|
||||||
|
>
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-list-alt" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Internal
|
Internal
|
||||||
|
@ -909,9 +937,37 @@
|
||||||
<p>Internal communications inside the cluster only</p>
|
<p>Internal communications inside the cluster only</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<input type="radio" id="publishing_cluster" ng-value="ctrl.ApplicationPublishingTypes.CLUSTER" ng-model="ctrl.formValues.PublishingType" />
|
<div ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
|
||||||
<label for="publishing_cluster">
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="publishing_cluster"
|
||||||
|
ng-value="ctrl.ApplicationPublishingTypes.CLUSTER"
|
||||||
|
ng-model="ctrl.formValues.PublishingType"
|
||||||
|
ng-change="ctrl.onChangePublishedPorts()"
|
||||||
|
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="publishing_cluster"
|
||||||
|
ng-if="
|
||||||
|
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Cluster
|
||||||
|
</div>
|
||||||
|
<p>Publish this application via a port on all nodes of the cluster</p>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="publishing_cluster"
|
||||||
|
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.CLUSTER"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
|
||||||
|
style="cursor: pointer; border-color: #767676;"
|
||||||
|
>
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Cluster
|
Cluster
|
||||||
|
@ -919,9 +975,74 @@
|
||||||
<p>Publish this application via a port on all nodes of the cluster</p>
|
<p>Publish this application via a port on all nodes of the cluster</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="ctrl.publishViaLoadBalancerEnabled()">
|
<div ng-if="ctrl.publishViaIngressEnabled()" ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
|
||||||
<input type="radio" id="publishing_loadbalancer" ng-value="ctrl.ApplicationPublishingTypes.LOAD_BALANCER" ng-model="ctrl.formValues.PublishingType" />
|
<input
|
||||||
<label for="publishing_loadbalancer">
|
type="radio"
|
||||||
|
id="publishing_ingress"
|
||||||
|
ng-value="ctrl.ApplicationPublishingTypes.INGRESS"
|
||||||
|
ng-model="ctrl.formValues.PublishingType"
|
||||||
|
ng-change="ctrl.onChangePublishedPorts()"
|
||||||
|
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="publishing_ingress"
|
||||||
|
ng-if="
|
||||||
|
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Ingress
|
||||||
|
</div>
|
||||||
|
<p>Publish this application via a HTTP route</p>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="publishing_ingress"
|
||||||
|
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.INGRESS"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
|
||||||
|
style="cursor: pointer; border-color: #767676;"
|
||||||
|
>
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Ingress
|
||||||
|
</div>
|
||||||
|
<p>Publish this application via a HTTP route</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="ctrl.publishViaLoadBalancerEnabled()" ng-style="{ color: ctrl.isPublishingTypeEditDisabled() ? '#767676' : '' }">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="publishing_loadbalancer"
|
||||||
|
ng-value="ctrl.ApplicationPublishingTypes.LOAD_BALANCER"
|
||||||
|
ng-model="ctrl.formValues.PublishingType"
|
||||||
|
ng-change="ctrl.onChangePublishedPorts()"
|
||||||
|
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="publishing_loadbalancer"
|
||||||
|
ng-if="
|
||||||
|
!ctrl.isPublishingTypeEditDisabled() ||
|
||||||
|
(ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Load balancer
|
||||||
|
</div>
|
||||||
|
<p>Publish this application via a load balancer</p>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="publishing_loadbalancer"
|
||||||
|
ng-if="ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType !== ctrl.ApplicationPublishingTypes.LOAD_BALANCER"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
tooltip-placement="bottom"
|
||||||
|
tooltip-class="portainer-tooltip"
|
||||||
|
uib-tooltip="Changing the publishing mode is not allowed until you delete all previously existing ports"
|
||||||
|
style="cursor: pointer; border-color: #767676;"
|
||||||
|
>
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-project-diagram" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Load balancer
|
Load balancer
|
||||||
|
@ -931,9 +1052,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !publishing options -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<!-- published ports -->
|
<!-- #region PUBLISHED PORTS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12" style="margin-top: 5px;">
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
<label class="control-label text-left">Published ports</label>
|
<label class="control-label text-left">Published ports</label>
|
||||||
|
@ -951,8 +1072,12 @@
|
||||||
When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port
|
When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port
|
||||||
number inside the default range <code>30000-32767</code>.
|
number inside the default range <code>30000-32767</code>.
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="ctrl.isNotInternalAndHasNoPublishedPorts()" class="col-sm-12 small text-muted text-warning" style="margin-top: 12px;">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> At least one published port must be defined.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||||
|
<!-- #region INPUTS -->
|
||||||
<div
|
<div
|
||||||
ng-repeat-start="publishedPort in ctrl.formValues.PublishedPorts"
|
ng-repeat-start="publishedPort in ctrl.formValues.PublishedPorts"
|
||||||
style="margin-top: 2px;"
|
style="margin-top: 2px;"
|
||||||
|
@ -960,9 +1085,9 @@
|
||||||
tooltip-placement="bottom"
|
tooltip-placement="bottom"
|
||||||
tooltip-class="portainer-tooltip"
|
tooltip-class="portainer-tooltip"
|
||||||
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
tooltip-enable="ctrl.disableLoadBalancerEdit()"
|
||||||
uib-tooltip="Edition is not allowed while the Load Balancer is in Pending state"
|
uib-tooltip="Edition is not allowed while the Load Balancer is in 'Pending' state"
|
||||||
>
|
>
|
||||||
<div class="col-sm-4 input-group input-group-sm">
|
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
|
||||||
<span class="input-group-addon">container port</span>
|
<span class="input-group-addon">container port</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -972,12 +1097,20 @@
|
||||||
placeholder="80"
|
placeholder="80"
|
||||||
ng-min="1"
|
ng-min="1"
|
||||||
ng-max="65535"
|
ng-max="65535"
|
||||||
required
|
ng-required="!publishedPort.NeedsDeletion"
|
||||||
ng-disabled="ctrl.disableLoadBalancerEdit()"
|
ng-change="ctrl.onChangePortMappingContainerPort()"
|
||||||
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
|
<div
|
||||||
|
class="col-sm-3 input-group input-group-sm"
|
||||||
|
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
|
ng-if="
|
||||||
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER) ||
|
||||||
|
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
|
||||||
|
"
|
||||||
|
>
|
||||||
<span class="input-group-addon">node port</span>
|
<span class="input-group-addon">node port</span>
|
||||||
<input
|
<input
|
||||||
name="published_node_port_{{ $index }}"
|
name="published_node_port_{{ $index }}"
|
||||||
|
@ -987,10 +1120,19 @@
|
||||||
placeholder="30080"
|
placeholder="30080"
|
||||||
ng-min="30000"
|
ng-min="30000"
|
||||||
ng-max="32767"
|
ng-max="32767"
|
||||||
|
ng-change="ctrl.onChangePortMappingNodePort()"
|
||||||
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
|
<div
|
||||||
|
class="col-sm-3 input-group input-group-sm"
|
||||||
|
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
|
ng-if="
|
||||||
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER) ||
|
||||||
|
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER)
|
||||||
|
"
|
||||||
|
>
|
||||||
<span class="input-group-addon">load balancer port</span>
|
<span class="input-group-addon">load balancer port</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -1001,69 +1143,206 @@
|
||||||
value="8080"
|
value="8080"
|
||||||
ng-min="1"
|
ng-min="1"
|
||||||
ng-max="65535"
|
ng-max="65535"
|
||||||
required
|
ng-required="!publishedPort.NeedsDeletion"
|
||||||
ng-disabled="ctrl.disableLoadBalancerEdit()"
|
ng-change="ctrl.onChangePortMappingLoadBalancerPort()"
|
||||||
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-3 input-group-sm">
|
<div
|
||||||
<div class="btn-group btn-group-sm">
|
class="col-sm-3 input-group input-group-sm"
|
||||||
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'TCP'" ng-disabled="ctrl.disableLoadBalancerEdit()">TCP</label>
|
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
<label class="btn btn-primary" ng-model="publishedPort.Protocol" uib-btn-radio="'UDP'" ng-disabled="ctrl.disableLoadBalancerEdit()">UDP</label>
|
ng-if="
|
||||||
</div>
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||||
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePublishedPort($index)" ng-if="!ctrl.disableLoadBalancerEdit()">
|
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
"
|
||||||
</button>
|
>
|
||||||
</div>
|
<span class="input-group-addon">ingress</span>
|
||||||
|
<select
|
||||||
|
class="form-control"
|
||||||
|
name="ingress_class_{{ $index }}"
|
||||||
|
ng-model="publishedPort.IngressName"
|
||||||
|
ng-options="ingress.Name as ingress.Name for ingress in ctrl.filteredIngresses"
|
||||||
|
ng-required="!publishedPort.NeedsDeletion"
|
||||||
|
ng-change="ctrl.onChangePortMappingIngress($index)"
|
||||||
|
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
|
>
|
||||||
|
<option selected disabled hidden value="">Select an ingress</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ng-repeat-end
|
class="col-sm-3 input-group input-group-sm"
|
||||||
|
ng-class="{ striked: publishedPort.NeedsDeletion }"
|
||||||
ng-if="
|
ng-if="
|
||||||
kubernetesApplicationCreationForm['container_port_' + $index].$invalid ||
|
(publishedPort.IsNew && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS) ||
|
||||||
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid ||
|
(!publishedPort.IsNew && ctrl.savedFormValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
|
||||||
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid
|
"
|
||||||
|
>
|
||||||
|
<span class="input-group-addon">route</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
name="ingress_route_{{ $index }}"
|
||||||
|
ng-model="publishedPort.IngressRoute"
|
||||||
|
placeholder="foo"
|
||||||
|
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-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
|
<div class="btn-group btn-group-sm" ng-class="{ striked: publishedPort.NeedsDeletion }">
|
||||||
|
<label
|
||||||
|
class="btn btn-primary"
|
||||||
|
ng-model="publishedPort.Protocol"
|
||||||
|
uib-btn-radio="'TCP'"
|
||||||
|
ng-change="ctrl.onChangePortMappingContainerPort()"
|
||||||
|
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
|
||||||
|
>TCP</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-primary"
|
||||||
|
ng-model="publishedPort.Protocol"
|
||||||
|
uib-btn-radio="'UDP'"
|
||||||
|
ng-change="ctrl.onChangePortMappingContainerPort()"
|
||||||
|
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
|
||||||
|
>UDP</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
ng-if="!ctrl.disableLoadBalancerEdit() && !publishedPort.NeedsDeletion"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
type="button"
|
||||||
|
ng-click="ctrl.removePublishedPort($index)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
ng-if="publishedPort.NeedsDeletion && ctrl.formValues.PublishingType === ctrl.savedFormValues.PublishingType"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
type="button"
|
||||||
|
ng-click="ctrl.restorePublishedPort($index)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-trash-restore" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region VALIDATION -->
|
||||||
|
<div
|
||||||
|
ng-repeat-end
|
||||||
|
ng-show="
|
||||||
|
kubernetesApplicationCreationForm['container_port_' + $index].$invalid ||
|
||||||
|
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid ||
|
||||||
|
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid ||
|
||||||
|
kubernetesApplicationCreationForm['ingress_class_' + $index].$invalid ||
|
||||||
|
kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid ||
|
||||||
|
ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined ||
|
||||||
|
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined) ||
|
||||||
|
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS && ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined) ||
|
||||||
|
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER &&
|
||||||
|
ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="col-sm-3 input-group input-group-sm">
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-if="
|
||||||
|
kubernetesApplicationCreationForm['container_port_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="col-sm-4 input-group input-group-sm">
|
|
||||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['container_port_' + $index].$invalid">
|
|
||||||
<div ng-messages="kubernetesApplicationCreationForm['container_port_'+$index].$error">
|
<div ng-messages="kubernetesApplicationCreationForm['container_port_'+$index].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
|
||||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p ng-if="ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This port is already used.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group input-group-sm col-sm-4" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
|
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER">
|
||||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid">
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-if="
|
||||||
|
kubernetesApplicationCreationForm['published_node_port_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined
|
||||||
|
"
|
||||||
|
>
|
||||||
<div ng-messages="kubernetesApplicationCreationForm['published_node_port_'+$index].$error">
|
<div ng-messages="kubernetesApplicationCreationForm['published_node_port_'+$index].$error">
|
||||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
||||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p ng-if="ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This port is already used.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
|
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS">
|
||||||
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid">
|
<div class="small text-warning" style="margin-top: 5px;" ng-if="kubernetesApplicationCreationForm['ingress_class_' + $index].$invalid">
|
||||||
|
<div ng-messages="kubernetesApplicationCreationForm['ingress_class_'+$index].$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress selection is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS">
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-if="kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined"
|
||||||
|
>
|
||||||
|
<div ng-messages="kubernetesApplicationCreationForm['ingress_route_'+$index].$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
|
||||||
|
<p ng-message="pattern"
|
||||||
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_' or
|
||||||
|
'/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p ng-if="ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This route is already used.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-3 input-group input-group-sm" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER">
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-if="
|
||||||
|
kubernetesApplicationCreationForm['load_balancer_port_' + $index].$invalid ||
|
||||||
|
ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined
|
||||||
|
"
|
||||||
|
>
|
||||||
<div ng-messages="kubernetesApplicationCreationForm['load_balancer_port_'+$index].$error">
|
<div ng-messages="kubernetesApplicationCreationForm['load_balancer_port_'+$index].$error">
|
||||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number is required.</p>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number is required.</p>
|
||||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
||||||
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Load balancer port number must be inside the range 1-65535.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p ng-if="ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
This port is already used.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-1 input-group-sm"> </div>
|
<div class="input-group col-sm-1 input-group-sm"> </div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !published ports -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- #region ACTIONS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
|
@ -1083,10 +1362,12 @@
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-default"
|
class="btn btn-sm btn-default"
|
||||||
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
|
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
|
||||||
>Cancel</button
|
|
||||||
>
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
</form>
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import * as JsonPatch from 'fast-json-patch';
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
|
|
||||||
class KubernetesCreateApplicationController {
|
class KubernetesCreateApplicationController {
|
||||||
|
/* #region CONSTRUCTOR */
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
$async,
|
$async,
|
||||||
|
@ -40,6 +42,7 @@ class KubernetesCreateApplicationController {
|
||||||
KubernetesStackService,
|
KubernetesStackService,
|
||||||
KubernetesConfigurationService,
|
KubernetesConfigurationService,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
|
KubernetesIngressService,
|
||||||
KubernetesPersistentVolumeClaimService,
|
KubernetesPersistentVolumeClaimService,
|
||||||
KubernetesNamespaceHelper,
|
KubernetesNamespaceHelper,
|
||||||
KubernetesVolumeService
|
KubernetesVolumeService
|
||||||
|
@ -56,6 +59,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||||
this.KubernetesNodeService = KubernetesNodeService;
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||||
|
this.KubernetesIngressService = KubernetesIngressService;
|
||||||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
|
|
||||||
|
@ -73,38 +77,25 @@ class KubernetesCreateApplicationController {
|
||||||
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
|
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
|
||||||
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
|
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
|
||||||
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
|
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
|
||||||
this.refreshStacksConfigsAppsAsync = this.refreshStacksConfigsAppsAsync.bind(this);
|
this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this);
|
||||||
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
this.getApplicationAsync = this.getApplicationAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
isValid() {
|
|
||||||
return (
|
|
||||||
!this.state.alreadyExists &&
|
|
||||||
!this.state.hasDuplicateEnvironmentVariables &&
|
|
||||||
!this.state.hasDuplicatePersistedFolderPaths &&
|
|
||||||
!this.state.hasDuplicateConfigurationPaths &&
|
|
||||||
!this.state.hasDuplicateExistingVolumes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeName() {
|
onChangeName() {
|
||||||
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
|
||||||
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/* #region AUTO SCLAER UI MANAGEMENT */
|
||||||
* AUTO SCALER UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
|
|
||||||
unselectAutoScaler() {
|
unselectAutoScaler() {
|
||||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
|
||||||
this.formValues.AutoScaler.IsUsed = false;
|
this.formValues.AutoScaler.IsUsed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
/**
|
/* #region CONFIGURATION UI MANAGEMENT */
|
||||||
* CONFIGURATION UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
addConfiguration() {
|
addConfiguration() {
|
||||||
let config = new KubernetesApplicationConfigurationFormValue();
|
let config = new KubernetesApplicationConfigurationFormValue();
|
||||||
config.SelectedConfiguration = this.configurations[0];
|
config.SelectedConfiguration = this.configurations[0];
|
||||||
|
@ -133,8 +124,12 @@ class KubernetesCreateApplicationController {
|
||||||
this.onChangeConfigurationPath();
|
this.onChangeConfigurationPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearConfigurations() {
|
||||||
|
this.formValues.Configurations = [];
|
||||||
|
}
|
||||||
|
|
||||||
onChangeConfigurationPath() {
|
onChangeConfigurationPath() {
|
||||||
this.state.duplicateConfigurationPaths = [];
|
this.state.duplicates.configurationPaths.refs = [];
|
||||||
|
|
||||||
const paths = _.reduce(
|
const paths = _.reduce(
|
||||||
this.formValues.Configurations,
|
this.formValues.Configurations,
|
||||||
|
@ -151,33 +146,20 @@ class KubernetesCreateApplicationController {
|
||||||
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
|
_.forEach(config.OverridenKeys, (overridenKey, keyIndex) => {
|
||||||
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
|
const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path);
|
||||||
if (findPath) {
|
if (findPath) {
|
||||||
this.state.duplicateConfigurationPaths[index + '_' + keyIndex] = findPath;
|
this.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] = findPath;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state.hasDuplicateConfigurationPaths = Object.keys(this.state.duplicateConfigurationPaths).length > 0;
|
this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
|
||||||
}
|
}
|
||||||
/**
|
/* #endregion */
|
||||||
* !CONFIGURATION UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/* #region ENVIRONMENT UI MANAGEMENT */
|
||||||
* ENVIRONMENT UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
addEnvironmentVariable() {
|
addEnvironmentVariable() {
|
||||||
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
|
this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
hasEnvironmentVariables() {
|
|
||||||
return this.formValues.EnvironmentVariables.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeEnvironmentName() {
|
|
||||||
this.state.duplicateEnvironmentVariables = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
|
|
||||||
this.state.hasDuplicateEnvironmentVariables = Object.keys(this.state.duplicateEnvironmentVariables).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreEnvironmentVariable(index) {
|
restoreEnvironmentVariable(index) {
|
||||||
this.formValues.EnvironmentVariables[index].NeedsDeletion = false;
|
this.formValues.EnvironmentVariables[index].NeedsDeletion = false;
|
||||||
}
|
}
|
||||||
|
@ -190,13 +172,14 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
this.onChangeEnvironmentName();
|
this.onChangeEnvironmentName();
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* !ENVIRONMENT UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
onChangeEnvironmentName() {
|
||||||
* PERSISTENT FOLDERS UI MANAGEMENT
|
this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
|
||||||
*/
|
this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region PERSISTENT FOLDERS UI MANAGEMENT */
|
||||||
addPersistedFolder() {
|
addPersistedFolder() {
|
||||||
let storageClass = {};
|
let storageClass = {};
|
||||||
if (this.storageClasses.length > 0) {
|
if (this.storageClasses.length > 0) {
|
||||||
|
@ -207,22 +190,17 @@ class KubernetesCreateApplicationController {
|
||||||
this.resetDeploymentType();
|
this.resetDeploymentType();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangePersistedFolderPath() {
|
|
||||||
this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(
|
|
||||||
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
||||||
if (persistedFolder.NeedsDeletion) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return persistedFolder.ContainerPath;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
restorePersistedFolder(index) {
|
restorePersistedFolder(index) {
|
||||||
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
this.formValues.PersistedFolders[index].NeedsDeletion = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetPersistedFolders() {
|
||||||
|
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
persistedFolder.ExistingVolume = null;
|
||||||
|
persistedFolder.UseNewVolume = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
removePersistedFolder(index) {
|
removePersistedFolder(index) {
|
||||||
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
|
if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) {
|
||||||
this.formValues.PersistedFolders[index].NeedsDeletion = true;
|
this.formValues.PersistedFolders[index].NeedsDeletion = true;
|
||||||
|
@ -233,10 +211,25 @@ class KubernetesCreateApplicationController {
|
||||||
this.onChangeExistingVolumeSelection();
|
this.onChangeExistingVolumeSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeExistingVolume(index) {
|
onChangePersistedFolderPath() {
|
||||||
if (this.formValues.PersistedFolders[index].UseNewVolume) {
|
this.state.duplicates.persistedFolders.refs = KubernetesFormValidationHelper.getDuplicates(
|
||||||
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
if (persistedFolder.NeedsDeletion) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
return persistedFolder.ContainerPath;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeExistingVolumeSelection() {
|
||||||
|
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
|
||||||
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
useNewVolume(index) {
|
useNewVolume(index) {
|
||||||
|
@ -253,46 +246,129 @@ class KubernetesCreateApplicationController {
|
||||||
this.resetDeploymentType();
|
this.resetDeploymentType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
onChangeExistingVolumeSelection() {
|
/* #region PUBLISHED PORTS UI MANAGEMENT */
|
||||||
this.state.duplicateExistingVolumes = KubernetesFormValidationHelper.getDuplicates(
|
|
||||||
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
||||||
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.state.hasDuplicateExistingVolumes = Object.keys(this.state.duplicateExistingVolumes).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
filterAvailableVolumes() {
|
|
||||||
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
|
||||||
const isSameNamespace = volume.ResourcePool.Namespace.Name === this.formValues.ResourcePool.Namespace.Name;
|
|
||||||
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
|
||||||
const isRWX = _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
|
|
||||||
return isSameNamespace && (isUnused || isRWX);
|
|
||||||
});
|
|
||||||
this.availableVolumes = filteredVolumes;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* !PERSISTENT FOLDERS UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUBLISHED PORTS UI MANAGEMENT
|
|
||||||
*/
|
|
||||||
addPublishedPort() {
|
addPublishedPort() {
|
||||||
this.formValues.PublishedPorts.push(new KubernetesApplicationPublishedPortFormValue());
|
const p = new KubernetesApplicationPublishedPortFormValue();
|
||||||
|
const ingresses = this.filteredIngresses;
|
||||||
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||||
|
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
||||||
|
this.formValues.PublishedPorts.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPublishedPorts() {
|
||||||
|
const ingresses = this.filteredIngresses;
|
||||||
|
_.forEach(this.formValues.PublishedPorts, (p) => {
|
||||||
|
p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined;
|
||||||
|
p.IngressHost = ingresses && ingresses.length ? ingresses[0].Host : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
restorePublishedPort(index) {
|
||||||
|
this.formValues.PublishedPorts[index].NeedsDeletion = false;
|
||||||
|
this.onChangePublishedPorts();
|
||||||
}
|
}
|
||||||
|
|
||||||
removePublishedPort(index) {
|
removePublishedPort(index) {
|
||||||
|
if (this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew) {
|
||||||
|
this.formValues.PublishedPorts[index].NeedsDeletion = true;
|
||||||
|
} else {
|
||||||
this.formValues.PublishedPorts.splice(index, 1);
|
this.formValues.PublishedPorts.splice(index, 1);
|
||||||
}
|
}
|
||||||
/**
|
this.onChangePublishedPorts();
|
||||||
* !PUBLISHED PORTS UI MANAGEMENT
|
}
|
||||||
*/
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region PUBLISHED PORTS ON CHANGE VALIDATION */
|
||||||
|
onChangePublishedPorts() {
|
||||||
|
this.onChangePortMappingContainerPort();
|
||||||
|
this.onChangePortMappingNodePort();
|
||||||
|
this.onChangePortMappingIngressRoute();
|
||||||
|
this.onChangePortMappingLoadBalancer();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangePortMappingContainerPort() {
|
||||||
|
const state = this.state.duplicates.publishedPorts.containerPorts;
|
||||||
|
if (this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol));
|
||||||
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||||
|
state.refs = duplicates;
|
||||||
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||||
|
} else {
|
||||||
|
state.refs = {};
|
||||||
|
state.hasDuplicates = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangePortMappingNodePort() {
|
||||||
|
const state = this.state.duplicates.publishedPorts.nodePorts;
|
||||||
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) {
|
||||||
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort));
|
||||||
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||||
|
state.refs = duplicates;
|
||||||
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||||
|
} else {
|
||||||
|
state.refs = {};
|
||||||
|
state.hasDuplicates = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangePortMappingIngress(index) {
|
||||||
|
const publishedPort = this.formValues.PublishedPorts[index];
|
||||||
|
const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName });
|
||||||
|
publishedPort.IngressHost = ingress.Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
|
||||||
|
_.forEach(newRoutes, (route, idx) => {
|
||||||
|
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
|
||||||
|
duplicates[idx] = route;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.refs = duplicates;
|
||||||
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||||
|
} else {
|
||||||
|
state.refs = {};
|
||||||
|
state.hasDuplicates = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangePortMappingLoadBalancer() {
|
||||||
|
const state = this.state.duplicates.publishedPorts.loadBalancerPorts;
|
||||||
|
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
|
||||||
|
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort));
|
||||||
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||||
|
state.refs = duplicates;
|
||||||
|
state.hasDuplicates = Object.keys(duplicates).length > 0;
|
||||||
|
} else {
|
||||||
|
state.refs = {};
|
||||||
|
state.hasDuplicates = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region STATE VALIDATION FUNCTIONS */
|
||||||
|
isValid() {
|
||||||
|
return (
|
||||||
|
!this.state.alreadyExists &&
|
||||||
|
!this.state.duplicates.environmentVariables.hasDuplicates &&
|
||||||
|
!this.state.duplicates.persistedFolders.hasDuplicates &&
|
||||||
|
!this.state.duplicates.configurationPaths.hasDuplicates &&
|
||||||
|
!this.state.duplicates.existingVolumes.hasDuplicates &&
|
||||||
|
!this.state.duplicates.publishedPorts.containerPorts.hasDuplicates &&
|
||||||
|
!this.state.duplicates.publishedPorts.nodePorts.hasDuplicates &&
|
||||||
|
!this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates &&
|
||||||
|
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* STATE VALIDATION FUNCTIONS
|
|
||||||
*/
|
|
||||||
storageClassAvailable() {
|
storageClassAvailable() {
|
||||||
return this.storageClasses && this.storageClasses.length > 0;
|
return this.storageClasses && this.storageClasses.length > 0;
|
||||||
}
|
}
|
||||||
|
@ -411,6 +487,10 @@ class KubernetesCreateApplicationController {
|
||||||
return this.state.useLoadBalancer;
|
return this.state.useLoadBalancer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publishViaIngressEnabled() {
|
||||||
|
return this.filteredIngresses.length;
|
||||||
|
}
|
||||||
|
|
||||||
isEditAndNoChangesMade() {
|
isEditAndNoChangesMade() {
|
||||||
if (!this.state.isEdit) return false;
|
if (!this.state.isEdit) return false;
|
||||||
const changes = JsonPatch.compare(this.savedFormValues, this.formValues);
|
const changes = JsonPatch.compare(this.savedFormValues, this.formValues);
|
||||||
|
@ -422,6 +502,16 @@ class KubernetesCreateApplicationController {
|
||||||
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isEditAndNotNewPublishedPort(index) {
|
||||||
|
return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNotInternalAndHasNoPublishedPorts() {
|
||||||
|
const toDelPorts = _.filter(this.formValues.PublishedPorts, { NeedsDeletion: true });
|
||||||
|
const toKeepPorts = _.without(this.formValues.PublishedPorts, ...toDelPorts);
|
||||||
|
return this.formValues.PublishingType !== KubernetesApplicationPublishingTypes.INTERNAL && toKeepPorts.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
isNonScalable() {
|
isNonScalable() {
|
||||||
const scalable = this.supportScalableReplicaDeployment();
|
const scalable = this.supportScalableReplicaDeployment();
|
||||||
const global = this.supportGlobalDeployment();
|
const global = this.supportGlobalDeployment();
|
||||||
|
@ -438,8 +528,8 @@ class KubernetesCreateApplicationController {
|
||||||
const invalid = !this.isValid();
|
const invalid = !this.isValid();
|
||||||
const hasNoChanges = this.isEditAndNoChangesMade();
|
const hasNoChanges = this.isEditAndNoChangesMade();
|
||||||
const nonScalable = this.isNonScalable();
|
const nonScalable = this.isNonScalable();
|
||||||
const res = overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
|
const notInternalNoPorts = this.isNotInternalAndHasNoPublishedPorts();
|
||||||
return res;
|
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || notInternalNoPorts;
|
||||||
}
|
}
|
||||||
|
|
||||||
disableLoadBalancerEdit() {
|
disableLoadBalancerEdit() {
|
||||||
|
@ -450,13 +540,19 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER
|
this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* !STATE VALIDATION FUNCTIONS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
isPublishingTypeEditDisabled() {
|
||||||
* DATA AUTO REFRESH
|
const ports = _.filter(this.formValues.PublishedPorts, { IsNew: false, NeedsDeletion: false });
|
||||||
*/
|
return this.state.isEdit && this.formValues.PublishedPorts.length > 0 && ports.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProtocolOptionDisabled(index, protocol) {
|
||||||
|
return this.disableLoadBalancerEdit() || (this.isEditAndNotNewPublishedPort(index) && this.formValues.PublishedPorts[index].Protocol !== protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region DATA AUTO REFRESH */
|
||||||
async updateSlidersAsync() {
|
async updateSlidersAsync() {
|
||||||
try {
|
try {
|
||||||
const quota = this.formValues.ResourcePool.Quota;
|
const quota = this.formValues.ResourcePool.Quota;
|
||||||
|
@ -546,33 +642,54 @@ class KubernetesCreateApplicationController {
|
||||||
return this.$async(this.refreshApplicationsAsync, namespace);
|
return this.$async(this.refreshApplicationsAsync, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshStacksConfigsAppsAsync(namespace) {
|
refreshVolumes(namespace) {
|
||||||
await Promise.all([this.refreshStacks(namespace), this.refreshConfigurations(namespace), this.refreshApplications(namespace)]);
|
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
||||||
|
const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace;
|
||||||
|
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
||||||
|
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
|
||||||
|
return isSameNamespace && (isUnused || isRWX);
|
||||||
|
});
|
||||||
|
this.availableVolumes = filteredVolumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshIngresses(namespace) {
|
||||||
|
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
|
||||||
|
if (!this.publishViaIngressEnabled()) {
|
||||||
|
this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
|
||||||
|
}
|
||||||
|
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshNamespaceDataAsync(namespace) {
|
||||||
|
await Promise.all([
|
||||||
|
this.refreshStacks(namespace),
|
||||||
|
this.refreshConfigurations(namespace),
|
||||||
|
this.refreshApplications(namespace),
|
||||||
|
this.refreshIngresses(namespace),
|
||||||
|
this.refreshVolumes(namespace),
|
||||||
|
]);
|
||||||
this.onChangeName();
|
this.onChangeName();
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshStacksConfigsApps(namespace) {
|
refreshNamespaceData(namespace) {
|
||||||
return this.$async(this.refreshStacksConfigsAppsAsync, namespace);
|
return this.$async(this.refreshNamespaceDataAsync, namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFormValues() {
|
||||||
|
this.clearConfigurations();
|
||||||
|
this.resetPersistedFolders();
|
||||||
|
this.resetPublishedPorts();
|
||||||
}
|
}
|
||||||
|
|
||||||
onResourcePoolSelectionChange() {
|
onResourcePoolSelectionChange() {
|
||||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||||
this.updateSliders();
|
this.updateSliders();
|
||||||
this.refreshStacksConfigsApps(namespace);
|
this.refreshNamespaceData(namespace);
|
||||||
this.filterAvailableVolumes();
|
this.resetFormValues();
|
||||||
this.formValues.Configurations = [];
|
|
||||||
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
|
||||||
persistedFolder.ExistingVolume = null;
|
|
||||||
persistedFolder.UseNewVolume = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
/**
|
/* #endregion */
|
||||||
* !DATA AUTO REFRESH
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/* #region ACTIONS */
|
||||||
* ACTIONS
|
|
||||||
*/
|
|
||||||
async deployApplicationAsync() {
|
async deployApplicationAsync() {
|
||||||
this.state.actionInProgress = true;
|
this.state.actionInProgress = true;
|
||||||
try {
|
try {
|
||||||
|
@ -595,7 +712,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.Notifications.success('Application successfully updated');
|
this.Notifications.success('Application successfully updated');
|
||||||
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
|
this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
|
this.Notifications.error('Failure', err, 'Unable to update application');
|
||||||
} finally {
|
} finally {
|
||||||
this.state.actionInProgress = false;
|
this.state.actionInProgress = false;
|
||||||
}
|
}
|
||||||
|
@ -612,13 +729,9 @@ class KubernetesCreateApplicationController {
|
||||||
return this.$async(this.deployApplicationAsync);
|
return this.$async(this.deployApplicationAsync);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/* #endregion */
|
||||||
* !ACTIONS
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/* #region APPLICATION - used on edit context only */
|
||||||
* APPLICATION - used on edit context only
|
|
||||||
*/
|
|
||||||
async getApplicationAsync() {
|
async getApplicationAsync() {
|
||||||
try {
|
try {
|
||||||
const namespace = this.state.params.namespace;
|
const namespace = this.state.params.namespace;
|
||||||
|
@ -634,16 +747,16 @@ class KubernetesCreateApplicationController {
|
||||||
getApplication() {
|
getApplication() {
|
||||||
return this.$async(this.getApplicationAsync);
|
return this.$async(this.getApplicationAsync);
|
||||||
}
|
}
|
||||||
/**
|
/* #endregion */
|
||||||
* !APPLICATION
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
/* #region ON INIT */
|
||||||
async onInit() {
|
async onInit() {
|
||||||
try {
|
try {
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
useLoadBalancer: false,
|
useLoadBalancer: false,
|
||||||
useServerMetrics: false,
|
useServerMetrics: false,
|
||||||
|
canUseIngress: false,
|
||||||
sliders: {
|
sliders: {
|
||||||
cpu: {
|
cpu: {
|
||||||
min: 0,
|
min: 0,
|
||||||
|
@ -662,14 +775,42 @@ class KubernetesCreateApplicationController {
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
availableSizeUnits: ['MB', 'GB', 'TB'],
|
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||||
alreadyExists: false,
|
alreadyExists: false,
|
||||||
duplicateEnvironmentVariables: {},
|
duplicates: {
|
||||||
hasDuplicateEnvironmentVariables: false,
|
environmentVariables: {
|
||||||
duplicatePersistedFolderPaths: {},
|
refs: {},
|
||||||
hasDuplicatePersistedFolderPaths: false,
|
hasDuplicates: false,
|
||||||
duplicateConfigurationPaths: {},
|
},
|
||||||
hasDuplicateConfigurationPaths: false,
|
persistedFolders: {
|
||||||
duplicateExistingVolumes: {},
|
refs: {},
|
||||||
hasDuplicateExistingVolumes: false,
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
configurationPaths: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
existingVolumes: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
publishedPorts: {
|
||||||
|
containerPorts: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
nodePorts: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
ingressRoutes: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
loadBalancerPorts: {
|
||||||
|
refs: {},
|
||||||
|
hasDuplicates: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
params: {
|
params: {
|
||||||
namespace: this.$transition$.params().namespace,
|
namespace: this.$transition$.params().namespace,
|
||||||
|
@ -694,20 +835,27 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
this.formValues = new KubernetesApplicationFormValues();
|
this.formValues = new KubernetesApplicationFormValues();
|
||||||
|
|
||||||
const [resourcePools, nodes, volumes, applications] = await Promise.all([
|
const [resourcePools, nodes, ingresses] = await Promise.all([
|
||||||
this.KubernetesResourcePoolService.get(),
|
this.KubernetesResourcePoolService.get(),
|
||||||
this.KubernetesNodeService.get(),
|
this.KubernetesNodeService.get(),
|
||||||
this.KubernetesVolumeService.get(),
|
this.KubernetesIngressService.get(),
|
||||||
this.KubernetesApplicationService.get(),
|
|
||||||
]);
|
]);
|
||||||
|
this.ingresses = ingresses;
|
||||||
|
|
||||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||||
this.formValues.ResourcePool = this.resourcePools[0];
|
this.formValues.ResourcePool = this.resourcePools[0];
|
||||||
|
|
||||||
|
// TODO: refactor @Max
|
||||||
|
// Don't pull all volumes and applications across all namespaces
|
||||||
|
// Use refreshNamespaceData flow (triggered on Init + on Namespace change)
|
||||||
|
// and query only accross the selected namespace
|
||||||
|
if (this.storageClassAvailable()) {
|
||||||
|
const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]);
|
||||||
this.volumes = volumes;
|
this.volumes = volumes;
|
||||||
_.forEach(this.volumes, (volume) => {
|
_.forEach(this.volumes, (volume) => {
|
||||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||||
});
|
});
|
||||||
this.filterAvailableVolumes();
|
}
|
||||||
|
|
||||||
_.forEach(nodes, (item) => {
|
_.forEach(nodes, (item) => {
|
||||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||||
|
@ -715,11 +863,12 @@ class KubernetesCreateApplicationController {
|
||||||
});
|
});
|
||||||
|
|
||||||
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
|
||||||
await this.refreshStacksConfigsApps(namespace);
|
await this.refreshNamespaceData(namespace);
|
||||||
|
|
||||||
if (this.state.isEdit) {
|
if (this.state.isEdit) {
|
||||||
await this.getApplication();
|
await this.getApplication();
|
||||||
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
|
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
|
||||||
|
this.formValues.OriginalIngresses = this.filteredIngresses;
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
delete this.formValues.ApplicationType;
|
delete this.formValues.ApplicationType;
|
||||||
|
|
||||||
|
@ -734,6 +883,7 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
|
||||||
|
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateSliders();
|
await this.updateSliders();
|
||||||
|
@ -747,6 +897,7 @@ class KubernetesCreateApplicationController {
|
||||||
$onInit() {
|
$onInit() {
|
||||||
return this.$async(this.onInit);
|
return this.$async(this.onInit);
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesCreateApplicationController;
|
export default KubernetesCreateApplicationController;
|
||||||
|
|
|
@ -11,12 +11,12 @@
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Expose applications over external IP addresses
|
Networking
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider.
|
Enabling the load balancer feature will allow users to expose application they deploy over an external IP address assigned by cloud provider.
|
||||||
<p style="margin-top: 2px;">
|
<p style="margin-top: 2px;">
|
||||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.
|
Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.
|
||||||
|
@ -32,6 +32,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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 />
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<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 controllers
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" id="ingress_classes" ng-model="ctrl.formValues.IngressClasses" placeholder="nginx,gce,traefik" />
|
||||||
|
</div>
|
||||||
|
<span class="col-sm-12 text-muted small" style="margin-top: 12px;">
|
||||||
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Once configured, ingresses can be created at the resource pool level.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Metrics
|
Metrics
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +77,6 @@
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label class="control-label text-left">
|
<label class="control-label text-left">
|
||||||
|
|
|
@ -52,6 +52,10 @@ class KubernetesConfigureController {
|
||||||
this.endpoint.Kubernetes.Configuration.StorageClasses = classes;
|
this.endpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||||
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||||
this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
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, ',');
|
||||||
|
}
|
||||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||||
|
|
||||||
const storagePromises = _.map(classes, (storageClass) => {
|
const storagePromises = _.map(classes, (storageClass) => {
|
||||||
|
@ -69,6 +73,10 @@ class KubernetesConfigureController {
|
||||||
modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes;
|
modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes;
|
||||||
modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||||
modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
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.EndpointProvider.setEndpoints(endpoints);
|
this.EndpointProvider.setEndpoints(endpoints);
|
||||||
}
|
}
|
||||||
this.Notifications.success('Configuration successfully applied');
|
this.Notifications.success('Configuration successfully applied');
|
||||||
|
@ -95,6 +103,8 @@ class KubernetesConfigureController {
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
UseLoadBalancer: false,
|
UseLoadBalancer: false,
|
||||||
UseServerMetrics: false,
|
UseServerMetrics: false,
|
||||||
|
UseIngress: false,
|
||||||
|
IngressClasses: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -117,8 +127,10 @@ class KubernetesConfigureController {
|
||||||
|
|
||||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
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);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve storage classes');
|
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint configuration');
|
||||||
} finally {
|
} finally {
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal" autocomplete="off" name="resourcePoolCreationForm">
|
<form class="form-horizontal" autocomplete="off" name="resourcePoolCreationForm">
|
||||||
<!-- name-input -->
|
<!-- #region NAME INPUT -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
|
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-11">
|
||||||
|
@ -39,10 +39,12 @@
|
||||||
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A resource pool with the same name already exists.</p>
|
<p ng-if="ctrl.state.isAlreadyExist"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A resource pool with the same name already exists.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Quota
|
Quota
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region QUOTA -->
|
||||||
<!-- quotas-switch -->
|
<!-- quotas-switch -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 small text-muted">
|
<div class="col-sm-12 small text-muted">
|
||||||
|
@ -56,16 +58,16 @@
|
||||||
<label class="control-label text-left">
|
<label class="control-label text-left">
|
||||||
Resource assignment
|
Resource assignment
|
||||||
</label>
|
</label>
|
||||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.hasQuota" /><i></i> </label>
|
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.HasQuota" /><i></i> </label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="ctrl.formValues.hasQuota && !ctrl.isQuotaValid()">
|
<div class="form-group" ng-if="ctrl.formValues.HasQuota && !ctrl.isQuotaValid()">
|
||||||
<span class="col-sm-12 text-warning small">
|
<span class="col-sm-12 text-warning small">
|
||||||
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- !quotas-switch -->
|
<!-- !quotas-switch -->
|
||||||
<div ng-if="ctrl.formValues.hasQuota">
|
<div ng-if="ctrl.formValues.HasQuota">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Resource limits
|
Resource limits
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,13 +78,8 @@
|
||||||
Memory
|
Memory
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<slider
|
<slider model="ctrl.formValues.MemoryLimit" floor="ctrl.defaults.MemoryLimit" ceil="ctrl.state.sliderMaxMemory" step="128" ng-if="ctrl.state.sliderMaxMemory">
|
||||||
model="ctrl.formValues.MemoryLimit"
|
</slider>
|
||||||
floor="ctrl.defaults.MemoryLimit"
|
|
||||||
ceil="ctrl.state.sliderMaxMemory"
|
|
||||||
step="128"
|
|
||||||
ng-if="ctrl.state.sliderMaxMemory"
|
|
||||||
></slider>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<input
|
<input
|
||||||
|
@ -106,8 +103,8 @@
|
||||||
<div class="col-sm-12 small text-warning">
|
<div class="col-sm-12 small text-warning">
|
||||||
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
|
<div ng-messages="resourcePoolCreationForm.pool_name.$error">
|
||||||
<p
|
<p
|
||||||
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}</p
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}
|
||||||
>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,14 +115,8 @@
|
||||||
CPU
|
CPU
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
<slider
|
<slider model="ctrl.formValues.CpuLimit" floor="ctrl.defaults.CpuLimit" ceil="ctrl.state.sliderMaxCpu" step="0.1" precision="2" ng-if="ctrl.state.sliderMaxCpu">
|
||||||
model="ctrl.formValues.CpuLimit"
|
</slider>
|
||||||
floor="ctrl.defaults.CpuLimit"
|
|
||||||
ceil="ctrl.state.sliderMaxCpu"
|
|
||||||
step="0.1"
|
|
||||||
precision="2"
|
|
||||||
ng-if="ctrl.state.sliderMaxCpu"
|
|
||||||
></slider>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4" style="margin-top: 20px;">
|
<div class="col-sm-4" style="margin-top: 20px;">
|
||||||
<p class="small text-muted">
|
<p class="small text-muted">
|
||||||
|
@ -136,16 +127,67 @@
|
||||||
<!-- !cpu-limit-input -->
|
<!-- !cpu-limit-input -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- actions -->
|
<!-- #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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
|
<!-- #region ACTIONS -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
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.state.actionInProgress || (ctrl.formValues.HasQuota && !ctrl.isQuotaValid()) || !ctrl.isValid()"
|
||||||
ng-click="ctrl.createResourcePool()"
|
ng-click="ctrl.createResourcePool()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
>
|
>
|
||||||
|
@ -154,7 +196,8 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !actions -->
|
|
||||||
|
<!-- #endregion -->
|
||||||
</form>
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
|
@ -3,14 +3,16 @@ import _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
|
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
|
|
||||||
class KubernetesCreateResourcePoolController {
|
class KubernetesCreateResourcePoolController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, Authentication) {
|
constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, Authentication, EndpointProvider) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
|
this.EndpointProvider = EndpointProvider;
|
||||||
|
|
||||||
this.KubernetesNodeService = KubernetesNodeService;
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||||
|
@ -53,13 +55,8 @@ class KubernetesCreateResourcePoolController {
|
||||||
try {
|
try {
|
||||||
this.checkDefaults();
|
this.checkDefaults();
|
||||||
const owner = this.Authentication.getUserDetails().username;
|
const owner = this.Authentication.getUserDetails().username;
|
||||||
await this.KubernetesResourcePoolService.create(
|
this.formValues.Owner = owner;
|
||||||
this.formValues.Name,
|
await this.KubernetesResourcePoolService.create(this.formValues);
|
||||||
owner,
|
|
||||||
this.formValues.hasQuota,
|
|
||||||
this.formValues.CpuLimit,
|
|
||||||
KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit)
|
|
||||||
);
|
|
||||||
this.Notifications.success('Resource pool successfully created', this.formValues.Name);
|
this.Notifications.success('Resource pool successfully created', this.formValues.Name);
|
||||||
this.$state.go('kubernetes.resourcePools');
|
this.$state.go('kubernetes.resourcePools');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -87,13 +84,10 @@ class KubernetesCreateResourcePoolController {
|
||||||
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
try {
|
try {
|
||||||
|
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||||
|
this.endpoint = endpoint;
|
||||||
this.defaults = KubernetesResourceQuotaDefaults;
|
this.defaults = KubernetesResourceQuotaDefaults;
|
||||||
|
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
|
||||||
this.formValues = {
|
|
||||||
MemoryLimit: this.defaults.MemoryLimit,
|
|
||||||
CpuLimit: this.defaults.CpuLimit,
|
|
||||||
hasQuota: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
@ -101,6 +95,7 @@ class KubernetesCreateResourcePoolController {
|
||||||
sliderMaxCpu: 0,
|
sliderMaxCpu: 0,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
isAlreadyExist: false,
|
isAlreadyExist: false,
|
||||||
|
canUseIngress: endpoint.Kubernetes.Configuration.UseIngress,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodes = await this.KubernetesNodeService.get();
|
const nodes = await this.KubernetesNodeService.get();
|
||||||
|
@ -111,6 +106,10 @@ class KubernetesCreateResourcePoolController {
|
||||||
});
|
});
|
||||||
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
|
||||||
await this.getResourcePools();
|
await this.getResourcePools();
|
||||||
|
if (this.state.canUseIngress) {
|
||||||
|
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
|
||||||
|
this.formValues.IngressClasses = _.map(ingressClasses, (item) => new KubernetesResourcePoolIngressClassFormValue(item));
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
|
angular.module('portainer.kubernetes').controller('KubernetesResourcePoolIngressesDatatableController', function ($scope, $controller, DatatableService) {
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||||
|
this.state = Object.assign(this.state, {
|
||||||
|
expandedItems: [],
|
||||||
|
expandAll: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onSettingsRepeaterChange = function () {
|
||||||
|
DatatableService.setDataTableSettings(this.tableKey, this.settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.expandItem = function (item, expanded) {
|
||||||
|
if (!this.itemCanExpand(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Expanded = expanded;
|
||||||
|
if (!expanded) {
|
||||||
|
item.Highlighted = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.itemCanExpand = function (item) {
|
||||||
|
return item.Paths.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hasExpandableItems = function () {
|
||||||
|
return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.expandAll = function () {
|
||||||
|
this.state.expandAll = !this.state.expandAll;
|
||||||
|
_.forEach(this.state.filteredDataSet, (item) => {
|
||||||
|
if (this.itemCanExpand(item)) {
|
||||||
|
this.expandItem(item, this.state.expandAll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onInit = function () {
|
||||||
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
if (storedOrder !== null) {
|
||||||
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular.module('portainer.kubernetes').component('kubernetesResourcePoolIngressesDatatable', {
|
||||||
|
templateUrl: './template.html',
|
||||||
|
controller: 'KubernetesResourcePoolIngressesDatatableController',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@',
|
||||||
|
titleIcon: '@',
|
||||||
|
dataset: '<',
|
||||||
|
tableKey: '@',
|
||||||
|
orderBy: '@',
|
||||||
|
reverseOrder: '<',
|
||||||
|
refreshCallback: '<',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,131 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="toolBar">
|
||||||
|
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="searchInput"
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
placeholder="Search..."
|
||||||
|
auto-focus
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover nowrap-cells">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 10%;">
|
||||||
|
<a ng-click="$ctrl.expandAll()" ng-if="$ctrl.hasExpandableItems()">
|
||||||
|
<i ng-class="{ 'fas fa-angle-down': $ctrl.state.expandAll, 'fas fa-angle-right': !$ctrl.state.expandAll }" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th style="width: 45%;">
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Ingress
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th style="width: 45%;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit: $ctrl.tableKey))"
|
||||||
|
ng-class="{ active: item.Checked }"
|
||||||
|
ng-style="{ background: item.Highlighted ? '#d5e8f3' : '' }"
|
||||||
|
ng-click="$ctrl.expandItem(item, !item.Expanded)"
|
||||||
|
pagination-id="$ctrl.tableKey"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<a ng-if="$ctrl.itemCanExpand(item)">
|
||||||
|
<i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td colspan="3">{{ item.Name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr dir-paginate-end ng-show="item.Expanded" ng-repeat="path in item.Paths" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
|
<td colspan="3">
|
||||||
|
<a style="margin-left: 25px;" ng-href="http://{{ path.Host ? path.Host : path.IP }}{{ path.Path }}" target="_blank">
|
||||||
|
{{ path.Host ? path.Host : path.IP }}{{ path.Path }}
|
||||||
|
</a>
|
||||||
|
<i class="fas fa-long-arrow-alt-right" style="margin: 2px;"></i>
|
||||||
|
<a ui-sref="kubernetes.applications.application({ name: path.ApplicationName, namespace: item.Namespace })">{{ path.ApplicationName }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||||
|
<td colspan="4" class="text-center text-muted">No ingresses available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5" pagination-id="$ctrl.tableKey"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
|
@ -28,16 +28,16 @@
|
||||||
<label class="control-label text-left">
|
<label class="control-label text-left">
|
||||||
Resource assignment
|
Resource assignment
|
||||||
</label>
|
</label>
|
||||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.hasQuota" /><i></i> </label>
|
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.HasQuota" /><i></i> </label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="ctrl.formValues.hasQuota && !ctrl.isQuotaValid()">
|
<div class="form-group" ng-if="ctrl.formValues.HasQuota && ctrl.isAdmin && ctrl.isEditable && !ctrl.isQuotaValid()">
|
||||||
<span class="col-sm-12 text-warning small">
|
<span class="col-sm-12 text-warning small">
|
||||||
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- !quotas-switch -->
|
<!-- !quotas-switch -->
|
||||||
<div ng-if="ctrl.formValues.hasQuota && ctrl.isAdmin && ctrl.isEditable">
|
<div ng-if="ctrl.formValues.HasQuota && ctrl.isAdmin && ctrl.isEditable">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Resource limits
|
Resource limits
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
<!-- !cpu-limit-input -->
|
<!-- !cpu-limit-input -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="ctrl.formValues.hasQuota">
|
<div ng-if="ctrl.formValues.HasQuota">
|
||||||
<kubernetes-resource-reservation
|
<kubernetes-resource-reservation
|
||||||
ng-if="ctrl.pool.Quota"
|
ng-if="ctrl.pool.Quota"
|
||||||
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
|
||||||
|
@ -120,6 +120,46 @@
|
||||||
>
|
>
|
||||||
</kubernetes-resource-reservation>
|
</kubernetes-resource-reservation>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="ctrl.isAdmin && ctrl.isEditable">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Ingresses
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
|
@ -129,7 +169,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="!resourcePoolEditForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.hasQuota && !ctrl.isQuotaValid())"
|
ng-disabled="!resourcePoolEditForm.$valid || ctrl.state.actionInProgress || (ctrl.formValues.HasQuota && !ctrl.isQuotaValid())"
|
||||||
ng-click="ctrl.updateResourcePool()"
|
ng-click="ctrl.updateResourcePool()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
>
|
>
|
||||||
|
@ -186,4 +226,19 @@
|
||||||
</kubernetes-resource-pool-applications-datatable>
|
</kubernetes-resource-pool-applications-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="ctrl.ingresses && ctrl.ingresses.length > 0">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<kubernetes-resource-pool-ingresses-datatable
|
||||||
|
dataset="ctrl.ingresses"
|
||||||
|
table-key="kubernetes.resourcepool.ingresses"
|
||||||
|
order-by="Name"
|
||||||
|
refresh-callback="ctrl.getIngresses"
|
||||||
|
loading="ctrl.state.ingressesLoading"
|
||||||
|
title-text="Ingress routes and applications"
|
||||||
|
title-icon="fa-route"
|
||||||
|
>
|
||||||
|
</kubernetes-resource-pool-ingresses-datatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import * as _ from 'lodash-es';
|
||||||
import filesizeParser from 'filesize-parser';
|
import filesizeParser from 'filesize-parser';
|
||||||
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
|
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassFormValue } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
|
|
||||||
class KubernetesResourcePoolController {
|
class KubernetesResourcePoolController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -13,6 +14,8 @@ class KubernetesResourcePoolController {
|
||||||
Authentication,
|
Authentication,
|
||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
|
EndpointProvider,
|
||||||
|
ModalService,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
KubernetesResourceQuotaService,
|
KubernetesResourceQuotaService,
|
||||||
KubernetesResourcePoolService,
|
KubernetesResourcePoolService,
|
||||||
|
@ -20,13 +23,15 @@ class KubernetesResourcePoolController {
|
||||||
KubernetesPodService,
|
KubernetesPodService,
|
||||||
KubernetesApplicationService,
|
KubernetesApplicationService,
|
||||||
KubernetesNamespaceHelper,
|
KubernetesNamespaceHelper,
|
||||||
ModalService
|
KubernetesIngressService
|
||||||
) {
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
|
this.EndpointProvider = EndpointProvider;
|
||||||
|
this.ModalService = ModalService;
|
||||||
|
|
||||||
this.KubernetesNodeService = KubernetesNodeService;
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
this.KubernetesResourceQuotaService = KubernetesResourceQuotaService;
|
||||||
|
@ -35,14 +40,17 @@ class KubernetesResourcePoolController {
|
||||||
this.KubernetesPodService = KubernetesPodService;
|
this.KubernetesPodService = KubernetesPodService;
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
this.ModalService = ModalService;
|
this.KubernetesIngressService = KubernetesIngressService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
|
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
|
||||||
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
|
||||||
this.getEvents = this.getEvents.bind(this);
|
this.getEvents = this.getEvents.bind(this);
|
||||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||||
|
this.getApplications = this.getApplications.bind(this);
|
||||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||||
|
this.getIngresses = this.getIngresses.bind(this);
|
||||||
|
this.getIngressesAsync = this.getIngressesAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab(index) {
|
selectTab(index) {
|
||||||
|
@ -83,8 +91,8 @@ class KubernetesResourcePoolController {
|
||||||
await this.KubernetesResourceQuotaService.create(quota);
|
await this.KubernetesResourceQuotaService.create(quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasResourceQuotaBeenReduce() {
|
hasResourceQuotaBeenReduced() {
|
||||||
if (this.formValues.hasQuota) {
|
if (this.formValues.HasQuota && this.oldQuota) {
|
||||||
const cpuLimit = this.formValues.CpuLimit;
|
const cpuLimit = this.formValues.CpuLimit;
|
||||||
const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
|
||||||
if (cpuLimit < this.oldQuota.CpuLimit || memoryLimit < this.oldQuota.MemoryLimit) {
|
if (cpuLimit < this.oldQuota.CpuLimit || memoryLimit < this.oldQuota.MemoryLimit) {
|
||||||
|
@ -104,7 +112,7 @@ class KubernetesResourcePoolController {
|
||||||
const owner = this.pool.Namespace.ResourcePoolOwner;
|
const owner = this.pool.Namespace.ResourcePoolOwner;
|
||||||
const quota = this.pool.Quota;
|
const quota = this.pool.Quota;
|
||||||
|
|
||||||
if (this.formValues.hasQuota) {
|
if (this.formValues.HasQuota) {
|
||||||
if (quota) {
|
if (quota) {
|
||||||
quota.CpuLimit = cpuLimit;
|
quota.CpuLimit = cpuLimit;
|
||||||
quota.MemoryLimit = memoryLimit;
|
quota.MemoryLimit = memoryLimit;
|
||||||
|
@ -115,6 +123,25 @@ class KubernetesResourcePoolController {
|
||||||
} else if (quota) {
|
} else if (quota) {
|
||||||
await this.KubernetesResourceQuotaService.delete(quota);
|
await this.KubernetesResourceQuotaService.delete(quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
return this.KubernetesIngressService.patch(oldIngress, newIngress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
|
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
|
||||||
this.$state.reload();
|
this.$state.reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -125,15 +152,25 @@ class KubernetesResourcePoolController {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResourcePool() {
|
updateResourcePool() {
|
||||||
if (this.hasResourceQuotaBeenReduce()) {
|
const willBeDeleted = _.filter(this.formValues.IngressClasses, { WasSelected: true, Selected: false });
|
||||||
this.ModalService.confirmUpdate(
|
const warnings = {
|
||||||
|
quota: this.hasResourceQuotaBeenReduced(),
|
||||||
|
ingress: willBeDeleted.length !== 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (warnings.quota || warnings.ingress) {
|
||||||
|
const messages = {
|
||||||
|
quota:
|
||||||
'Reducing the quota assigned to an "in-use" resource pool may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.',
|
'Reducing the quota assigned to an "in-use" resource pool may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.',
|
||||||
(confirmed) => {
|
ingress: 'Deactivating ingresses may cause applications to be unaccessible. All ingress configurations from affected applications will be removed.',
|
||||||
|
};
|
||||||
|
const displayedMessage = `${warnings.quota ? messages.quota : ''}${warnings.quota && warnings.ingress ? '<br/><br/>' : ''}
|
||||||
|
${warnings.ingress ? messages.ingress : ''}<br/><br/>Do you wish to continue?`;
|
||||||
|
this.ModalService.confirmUpdate(displayedMessage, (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
return this.$async(this.updateResourcePoolAsync);
|
return this.$async(this.updateResourcePoolAsync);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return this.$async(this.updateResourcePoolAsync);
|
return this.$async(this.updateResourcePoolAsync);
|
||||||
}
|
}
|
||||||
|
@ -180,16 +217,36 @@ class KubernetesResourcePoolController {
|
||||||
return this.$async(this.getApplicationsAsync);
|
return this.$async(this.getApplicationsAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getIngressesAsync() {
|
||||||
|
this.state.ingressesLoading = true;
|
||||||
|
try {
|
||||||
|
const namespace = this.pool.Namespace.Name;
|
||||||
|
this.ingresses = await this.KubernetesIngressService.get(namespace);
|
||||||
|
_.forEach(this.ingresses, (ing) => {
|
||||||
|
ing.Namespace = namespace;
|
||||||
|
_.forEach(ing.Paths, (path) => {
|
||||||
|
const application = _.find(this.applications, { ServiceName: path.ServiceName });
|
||||||
|
path.ApplicationName = application && application.Name ? application.Name : '-';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
|
||||||
|
} finally {
|
||||||
|
this.state.ingressesLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIngresses() {
|
||||||
|
return this.$async(this.getIngressesAsync);
|
||||||
|
}
|
||||||
|
|
||||||
async onInit() {
|
async onInit() {
|
||||||
try {
|
try {
|
||||||
|
const endpoint = this.EndpointProvider.currentEndpoint();
|
||||||
|
this.endpoint = endpoint;
|
||||||
this.isAdmin = this.Authentication.isAdmin();
|
this.isAdmin = this.Authentication.isAdmin();
|
||||||
this.defaults = KubernetesResourceQuotaDefaults;
|
this.defaults = KubernetesResourceQuotaDefaults;
|
||||||
|
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
|
||||||
this.formValues = {
|
|
||||||
MemoryLimit: this.defaults.MemoryLimit,
|
|
||||||
CpuLimit: this.defaults.CpuLimit,
|
|
||||||
hasQuota: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
|
@ -204,8 +261,10 @@ class KubernetesResourcePoolController {
|
||||||
showEditorTab: false,
|
showEditorTab: false,
|
||||||
eventsLoading: true,
|
eventsLoading: true,
|
||||||
applicationsLoading: true,
|
applicationsLoading: true,
|
||||||
|
ingressesLoading: true,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
eventWarningCount: 0,
|
eventWarningCount: 0,
|
||||||
|
canUseIngress: endpoint.Kubernetes.Configuration.UseIngress,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool');
|
||||||
|
@ -225,7 +284,7 @@ class KubernetesResourcePoolController {
|
||||||
const quota = pool.Quota;
|
const quota = pool.Quota;
|
||||||
if (quota) {
|
if (quota) {
|
||||||
this.oldQuota = angular.copy(quota);
|
this.oldQuota = angular.copy(quota);
|
||||||
this.formValues.hasQuota = true;
|
this.formValues.HasQuota = true;
|
||||||
this.formValues.CpuLimit = quota.CpuLimit;
|
this.formValues.CpuLimit = quota.CpuLimit;
|
||||||
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
|
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
|
||||||
|
|
||||||
|
@ -240,6 +299,22 @@ class KubernetesResourcePoolController {
|
||||||
|
|
||||||
await this.getEvents();
|
await this.getEvents();
|
||||||
await this.getApplications();
|
await this.getApplications();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to load view data');
|
this.Notifications.error('Failure', err, 'Unable to load view data');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"start:server": "grunt clean:server && grunt start:server",
|
"start:server": "grunt clean:server && grunt start:server",
|
||||||
"start:client": "grunt clean:client && grunt start:client",
|
"start:client": "grunt clean:client && grunt start:client",
|
||||||
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
|
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
|
||||||
|
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client",
|
||||||
"start:toolkit": "grunt start:toolkit",
|
"start:toolkit": "grunt start:toolkit",
|
||||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||||
"clean:all": "grunt clean:all",
|
"clean:all": "grunt clean:all",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue