From c47e840b37cb20f28e97ad87a6e3e6b81677bc4f Mon Sep 17 00:00:00 2001 From: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Date: Mon, 17 Jan 2022 08:37:46 +1300 Subject: [PATCH] feat(k8s): Allow mix services for k8s app EE-1791 (#6198) allow a mix of services for k8s in ui --- .../applicationsDatatable.html | 12 +- .../kube-services-item.controller.js | 83 + .../kube-services-item.html | 242 ++ .../kube-services-item/kube-services-item.js | 19 + .../kube-services/kube-services.controller.js | 105 + .../kube-services/kube-services.html | 94 + .../components/kube-services/kube-services.js | 12 + app/kubernetes/converters/application.js | 8 +- app/kubernetes/converters/service.js | 65 +- app/kubernetes/helpers/application/index.js | 57 +- app/kubernetes/helpers/serviceHelper.js | 7 + app/kubernetes/ingress/converter.js | 63 + .../models/application/formValues.js | 1 + app/kubernetes/models/service/models.js | 38 + app/kubernetes/services/applicationService.js | 103 +- app/kubernetes/services/serviceService.js | 44 +- .../create/createApplication.html | 2869 +++++++---------- .../create/createApplicationController.js | 10 +- .../views/applications/edit/application.html | 157 +- .../edit/applicationController.js | 10 +- .../ingress-table/ingress-table.controller.js | 29 + .../ingress-table/ingress-table.html | 24 + .../components/ingress-table/ingress-table.js | 11 + .../services-table/services-table.html | 56 + .../services-table/services-table.js | 10 + .../summary/resources/applicationResources.js | 70 +- 26 files changed, 2336 insertions(+), 1863 deletions(-) create mode 100644 app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js create mode 100644 app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html create mode 100644 app/kubernetes/components/kube-services/kube-services-item/kube-services-item.js create mode 100644 app/kubernetes/components/kube-services/kube-services.controller.js create mode 100644 app/kubernetes/components/kube-services/kube-services.html create mode 100644 app/kubernetes/components/kube-services/kube-services.js create mode 100644 app/kubernetes/views/applications/edit/components/ingress-table/ingress-table.controller.js create mode 100644 app/kubernetes/views/applications/edit/components/ingress-table/ingress-table.html create mode 100644 app/kubernetes/views/applications/edit/components/ingress-table/ingress-table.js create mode 100644 app/kubernetes/views/applications/edit/components/services-table/services-table.html create mode 100644 app/kubernetes/views/applications/edit/components/services-table/services-table.js diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index 942fadf75..6603946bb 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -166,7 +166,7 @@ Status - Publishing mode + Published @@ -233,15 +233,9 @@ {{ item.Pods[0].Status }} - - - - - {{ item.ServiceType | kubernetesApplicationServiceTypeText }} - - + + {{ item.Services.length === 0 ? 'No' : 'Yes' }} - - {{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }} diff --git a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js new file mode 100644 index 000000000..9cf03967e --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js @@ -0,0 +1,83 @@ +import _ from 'lodash-es'; +import { KubernetesServicePort, KubernetesIngressServiceRoute } from 'Kubernetes/models/service/models'; +import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues'; +import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; +import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants'; + +export default class KubeServicesItemViewController { + /* @ngInject */ + constructor(EndpointProvider, Authentication) { + this.EndpointProvider = EndpointProvider; + this.Authentication = Authentication; + } + + addPort() { + const p = new KubernetesServicePort(); + p.nodePort = ''; + p.port = ''; + p.targetPort = ''; + p.protocol = 'TCP'; + + if (this.ingressType) { + const r = new KubernetesIngressServiceRoute(); + r.ServiceName = this.serviceName; + p.ingress = r; + p.Ingress = true; + } + this.servicePorts.push(p); + } + + removePort(index) { + this.servicePorts.splice(index, 1); + } + + servicePort(index) { + const targetPort = this.servicePorts[index].targetPort; + this.servicePorts[index].port = targetPort; + } + + isAdmin() { + return this.Authentication.isAdmin(); + } + + onChangeContainerPort() { + const state = this.state.duplicates.targetPort; + const source = _.map(this.servicePorts, (sp) => sp.targetPort); + const duplicates = KubernetesFormValidationHelper.getDuplicates(source); + state.refs = duplicates; + state.hasRefs = Object.keys(duplicates).length > 0; + } + + onChangeServicePort() { + const state = this.state.duplicates.servicePort; + const source = _.map(this.servicePorts, (sp) => sp.port); + const duplicates = KubernetesFormValidationHelper.getDuplicates(source); + state.refs = duplicates; + state.hasRefs = Object.keys(duplicates).length > 0; + } + + onChangeNodePort() { + const state = this.state.duplicates.nodePort; + const source = _.map(this.servicePorts, (sp) => sp.nodePort); + const duplicates = KubernetesFormValidationHelper.getDuplicates(source); + state.refs = duplicates; + state.hasRefs = Object.keys(duplicates).length > 0; + } + + $onInit() { + if (this.servicePorts.length === 0) { + this.addPort(); + } + + this.KubernetesApplicationPublishingTypes = KubernetesApplicationPublishingTypes; + + this.state = { + duplicates: { + targetPort: new KubernetesFormValidationReferences(), + servicePort: new KubernetesFormValidationReferences(), + nodePort: new KubernetesFormValidationReferences(), + }, + endpointId: this.EndpointProvider.endpointID(), + }; + } +} diff --git a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html new file mode 100644 index 000000000..c5b10b36a --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html @@ -0,0 +1,242 @@ +
+
+

+ No Load balancer is available in this cluster, click + here to configure load balancer. +

+
+
+

No Load balancer is available in this cluster, contract your administrator.

+
+ +
+
+ + + publish a new port + +
+
+
+ container port + +
+ +
+ service port + +
+ +
+ nodeport + +
+ +
+ loadbalancer port + +
+ +
+ ingress + +
+ +
+ hostname + +
+ +
+ route + +
+ +
+
+ + +
+ +
+ +
+
+
+

+ This container port is already used. +

+
+
+

Container port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+
+ +
+
+

+ This service port is already used. +

+
+
+
+

Service port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+
+
+ +
+
+
+

Node port number must be inside the range 30000-32767 or blank for system allocated.

+

Node port number must be inside the range 30000-32767 or blank for system allocated.

+
+
+
+ +
+
+
+

Ingress selection is required.

+
+
+
+ +
+
+
+

Host is required.

+
+
+
+
+
+
+

Route is required.

+

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').

+
+
+
+
+
+
+
diff --git a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.js b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.js new file mode 100644 index 000000000..42fb50a96 --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.js @@ -0,0 +1,19 @@ +import angular from 'angular'; +import controller from './kube-services-item.controller'; + +angular.module('portainer.kubernetes').component('kubeServicesItemView', { + templateUrl: './kube-services-item.html', + controller, + bindings: { + serviceType: '<', + servicePorts: '=', + serviceRoutes: '=', + ingressType: '<', + originalIngresses: '<', + isEdit: '<', + serviceName: '<', + multiItemDisable: '<', + serviceIndex: '<', + loadbalancerEnabled: '<', + }, +}); diff --git a/app/kubernetes/components/kube-services/kube-services.controller.js b/app/kubernetes/components/kube-services/kube-services.controller.js new file mode 100644 index 000000000..c35be0585 --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services.controller.js @@ -0,0 +1,105 @@ +import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants'; + +export default class KubeServicesViewController { + /* @ngInject */ + constructor($async, EndpointProvider, Authentication) { + this.$async = $async; + this.EndpointProvider = EndpointProvider; + this.Authentication = Authentication; + } + + addEntry(service) { + const p = new KubernetesService(); + if (service === KubernetesApplicationPublishingTypes.INGRESS) { + p.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP; + p.Ingress = true; + } else { + p.Type = service; + } + + p.Selector = this.formValues.Selector; + + p.Name = this.getUniqName(); + this.state.nameIndex += 1; + this.formValues.Services.push(p); + } + + getUniqName() { + let name = this.formValues.Name + '-' + this.state.nameIndex; + const services = this.formValues.Services; + services.forEach((service) => { + if (service.Name === name) { + this.state.nameIndex += 1; + name = this.formValues.Name + '-' + this.state.nameIndex; + } + }); + const UniqName = this.formValues.Name + '-' + this.state.nameIndex; + return UniqName; + } + + deleteService(index) { + this.formValues.Services.splice(index, 1); + this.state.nameIndex -= 1; + } + + addPort(index) { + const p = new KubernetesServicePort(); + this.formValues.Services[index].Ports.push(p); + } + + serviceType(type) { + switch (type) { + case KubernetesApplicationPublishingTypes.CLUSTER_IP: + return KubernetesServiceTypes.CLUSTER_IP; + case KubernetesApplicationPublishingTypes.NODE_PORT: + return KubernetesServiceTypes.NODE_PORT; + case KubernetesApplicationPublishingTypes.LOAD_BALANCER: + return KubernetesServiceTypes.LOAD_BALANCER; + case KubernetesApplicationPublishingTypes.INGRESS: + return KubernetesServiceTypes.INGRESS; + } + } + + isAdmin() { + return this.Authentication.isAdmin(); + } + + iconStyle(type) { + switch (type) { + case KubernetesApplicationPublishingTypes.CLUSTER_IP: + return 'fa fa-list-alt'; + case KubernetesApplicationPublishingTypes.NODE_PORT: + return 'fa fa-list'; + case KubernetesApplicationPublishingTypes.LOAD_BALANCER: + return 'fa fa-project-diagram'; + case KubernetesApplicationPublishingTypes.INGRESS: + return 'fa fa-route'; + } + } + $onInit() { + this.state = { + serviceType: [ + { + typeName: KubernetesServiceTypes.CLUSTER_IP, + typeValue: KubernetesApplicationPublishingTypes.CLUSTER_IP, + }, + { + typeName: KubernetesServiceTypes.NODE_PORT, + typeValue: KubernetesApplicationPublishingTypes.NODE_PORT, + }, + { + typeName: KubernetesServiceTypes.LOAD_BALANCER, + typeValue: KubernetesApplicationPublishingTypes.LOAD_BALANCER, + }, + { + typeName: KubernetesServiceTypes.INGRESS, + typeValue: KubernetesApplicationPublishingTypes.INGRESS, + }, + ], + selected: KubernetesApplicationPublishingTypes.CLUSTER_IP, + nameIndex: this.formValues.Services.length, + endpointId: this.EndpointProvider.endpointID(), + }; + } +} diff --git a/app/kubernetes/components/kube-services/kube-services.html b/app/kubernetes/components/kube-services/kube-services.html new file mode 100644 index 000000000..35a83c8ca --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services.html @@ -0,0 +1,94 @@ +
+ Publishing the application +
+ +
+
+
+ + +
+
+
+ +
+
+
+
+ + {{ $ctrl.serviceType(service.Type) }} +
+ + +
+ +
+
+ + Ingress +
+
+

+ Ingress is not configured in this namespace, select another namespace or click + here to configure ingress. +

+
+
+

+ Ingress is not configured in this namespace, select another namespace or contact your administrator. +

+
+ +
+ +
+
+ + Ingress +
+ + +
+
+
diff --git a/app/kubernetes/components/kube-services/kube-services.js b/app/kubernetes/components/kube-services/kube-services.js new file mode 100644 index 000000000..22c1bef7c --- /dev/null +++ b/app/kubernetes/components/kube-services/kube-services.js @@ -0,0 +1,12 @@ +import angular from 'angular'; +import controller from './kube-services.controller'; + +angular.module('portainer.kubernetes').component('kubeServicesView', { + templateUrl: './kube-services.html', + controller, + bindings: { + formValues: '=', + isEdit: '<', + loadbalancerEnabled: '<', + }, +}); diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index e153f0934..76dfb1541 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -120,6 +120,8 @@ class KubernetesApplicationConverter { res.ServiceType = serviceType; res.ServiceId = service.metadata.uid; res.ServiceName = service.metadata.name; + res.ClusterIp = service.spec.clusterIP; + res.ExternalIp = service.spec.externalIP; if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) { @@ -279,6 +281,8 @@ class KubernetesApplicationConverter { res.ApplicationType = app.ApplicationType; res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); res.Name = app.Name; + res.Services = KubernetesApplicationHelper.generateServicesFormValuesFromServices(app); + res.Selector = KubernetesApplicationHelper.generateSelectorFromService(app); res.StackName = app.StackName; res.ApplicationOwner = app.ApplicationOwner; res.ImageModel.Image = app.Image; @@ -356,7 +360,9 @@ class KubernetesApplicationConverter { service = undefined; } - return [app, headlessService, service, claims]; + let services = KubernetesServiceConverter.applicationFormValuesToServices(formValues); + + return [app, headlessService, services, service, claims]; } } diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js index e67266e5c..420c6df26 100644 --- a/app/kubernetes/converters/service.js +++ b/app/kubernetes/converters/service.js @@ -52,10 +52,59 @@ class KubernetesServiceConverter { return res; } + static applicationFormValuesToServices(formValues) { + let services = []; + formValues.Services.forEach(function (service) { + const res = new KubernetesService(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = service.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + if (service.Type === KubernetesApplicationPublishingTypes.NODE_PORT) { + res.Type = KubernetesServiceTypes.NODE_PORT; + } else if (service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { + res.Type = KubernetesServiceTypes.LOAD_BALANCER; + } else if (service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP) { + res.Type = KubernetesServiceTypes.CLUSTER_IP; + } + res.Ingress = service.Ingress; + + if (service.Selector !== undefined) { + res.Selector = service.Selector; + } else { + res.Selector = { + app: formValues.Name, + }; + } + + let ports = []; + service.Ports.forEach(function (port, index) { + const res = new KubernetesServicePort(); + res.name = 'port-' + index; + res.port = port.port; + if (port.nodePort) { + res.nodePort = port.nodePort; + } + res.protocol = port.protocol; + res.targetPort = port.targetPort; + res.ingress = port.ingress; + ports.push(res); + }); + res.Ports = ports; + + services.push(res); + }); + return services; + } + static applicationFormValuesToHeadlessService(formValues) { const res = KubernetesServiceConverter.applicationFormValuesToService(formValues); res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name); res.Headless = true; + res.Selector = { + app: formValues.Name, + }; return res; } @@ -70,8 +119,20 @@ class KubernetesServiceConverter { payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName; payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner; - payload.spec.ports = service.Ports; - payload.spec.selector.app = service.ApplicationName; + + const ports = []; + service.Ports.forEach((port) => { + const p = {}; + p.name = port.name; + p.port = port.port; + p.nodePort = port.nodePort; + p.protocol = port.protocol; + p.targetPort = port.targetPort; + ports.push(p); + }); + payload.spec.ports = ports; + + payload.spec.selector = service.Selector; if (service.Headless) { payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP; delete payload.spec.ports; diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index c6d7a1465..8777c0cd4 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models'; -import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; import { KubernetesApplicationAutoScalerFormValue, @@ -276,6 +276,61 @@ class KubernetesApplicationHelper { } /* #endregion */ + /* #region SERVICES -> SERVICES FORM VALUES */ + static generateServicesFormValuesFromServices(app) { + let services = []; + app.Services.forEach(function (service) { + const svc = new KubernetesService(); + svc.Namespace = service.metadata.namespace; + svc.Name = service.metadata.name; + svc.StackName = service.StackName; + svc.ApplicationOwner = app.ApplicationOwner; + svc.ApplicationName = app.ApplicationName; + svc.Type = service.spec.type; + if (service.spec.type === KubernetesServiceTypes.CLUSTER_IP) { + svc.Type = 1; + } else if (service.spec.type === KubernetesServiceTypes.NODE_PORT) { + svc.Type = 2; + } else if (service.spec.type === KubernetesServiceTypes.LOAD_BALANCER) { + svc.Type = 3; + } + + let ports = []; + service.spec.ports.forEach(function (port) { + const svcport = new KubernetesServicePort(); + svcport.name = port.name; + svcport.port = port.port; + svcport.nodePort = port.nodePort; + svcport.protocol = port.protocol; + svcport.targetPort = port.targetPort; + + app.Ingresses.value.forEach((ingress) => { + const ingressMatched = _.find(ingress.Paths, { ServiceName: service.metadata.name }); + if (ingressMatched) { + svcport.ingress = { + IngressName: ingressMatched.IngressName, + Host: ingressMatched.Host, + Path: ingressMatched.Path, + }; + svc.Ingress = true; + } + }); + + ports.push(svcport); + }); + svc.Ports = ports; + svc.Selector = app.Raw.spec.selector.matchLabels; + services.push(svc); + }); + + return services; + } + /* #endregion */ + static generateSelectorFromService(app) { + const selector = app.Raw.spec.selector.matchLabels; + return selector; + } + /* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */ static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) { const generatePort = (port, rule) => { diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js index 247e4a441..987a7fa4e 100644 --- a/app/kubernetes/helpers/serviceHelper.js +++ b/app/kubernetes/helpers/serviceHelper.js @@ -12,5 +12,12 @@ class KubernetesServiceHelper { } return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); } + + static findApplicationBoundServices(services, rawApp) { + if (!rawApp.spec.template) { + return undefined; + } + return _.filter(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); + } } export default KubernetesServiceHelper; diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index ab71f3bd2..fa2e1b546 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -76,6 +76,69 @@ export class KubernetesIngressConverter { return ingresses; } + static applicationFormValuesToDeleteIngresses(formValues, application) { + const ingresses = angular.copy(formValues.OriginalIngresses); + application.Services.forEach((service) => { + ingresses.forEach((ingress) => { + const path = _.find(ingress.Paths, { ServiceName: service.metadata.name }); + if (path) { + _.remove(ingress.Paths, path); + } + }); + }); + return ingresses; + } + + static deleteIngressByServiceName(formValues, service) { + const ingresses = angular.copy(formValues.OriginalIngresses); + ingresses.forEach((ingress) => { + const path = _.find(ingress.Paths, { ServiceName: service.Name }); + if (path) { + _.remove(ingress.Paths, path); + } + }); + return ingresses; + } + + static newApplicationFormValuesToIngresses(formValues, serviceName, servicePorts) { + const ingresses = angular.copy(formValues.OriginalIngresses); + servicePorts.forEach((port) => { + const ingress = _.find(ingresses, { Name: port.ingress.IngressName }); + if (ingress) { + const rule = new KubernetesIngressRule(); + rule.ServiceName = serviceName; + rule.IngressName = port.ingress.IngressName; + rule.Host = port.ingress.Host; + rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path; + rule.Port = port.port; + + ingress.Paths.push(rule); + } + }); + return ingresses; + } + + static editingFormValuesToIngresses(formValues, serviceName, servicePorts) { + const ingresses = angular.copy(formValues.OriginalIngresses); + servicePorts.forEach((port) => { + const ingressMatched = _.find(ingresses, { Name: port.ingress.IngressName }); + if (ingressMatched) { + const pathMatched = _.find(ingressMatched.Paths, { ServiceName: serviceName }); + _.remove(ingressMatched.Paths, pathMatched); + + const rule = new KubernetesIngressRule(); + rule.ServiceName = serviceName; + rule.IngressName = port.ingress.IngressName; + rule.Host = port.ingress.Host; + rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path; + rule.Port = port.port; + + ingressMatched.Paths.push(rule); + } + }); + return ingresses; + } + /** * * @param {KubernetesResourcePoolIngressClassFormValue[]} formValues diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 78c1b055a..3b6a30bac 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -18,6 +18,7 @@ export function KubernetesApplicationFormValues() { this.ReplicaCount = 1; this.AutoScaler = {}; this.Containers = []; + this.Services = []; this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis; this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis; diff --git a/app/kubernetes/models/service/models.js b/app/kubernetes/models/service/models.js index 8dcc36d86..39b110642 100644 --- a/app/kubernetes/models/service/models.js +++ b/app/kubernetes/models/service/models.js @@ -4,6 +4,7 @@ export const KubernetesServiceTypes = Object.freeze({ LOAD_BALANCER: 'LoadBalancer', NODE_PORT: 'NodePort', CLUSTER_IP: 'ClusterIP', + INGRESS: 'Ingress', }); /** @@ -20,6 +21,8 @@ const _KubernetesService = Object.freeze({ ApplicationName: '', ApplicationOwner: '', Note: '', + Ingress: false, + Selector: {}, }); export class KubernetesService { @@ -28,6 +31,40 @@ export class KubernetesService { } } +const _KubernetesIngressService = Object.freeze({ + Headless: false, + Namespace: '', + Name: '', + StackName: '', + Ports: [], + Type: '', + ClusterIP: '', + ApplicationName: '', + ApplicationOwner: '', + Note: '', + Ingress: true, + IngressRoute: [], +}); + +export class KubernetesIngressService { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressService))); + } +} + +const _KubernetesIngressServiceRoute = Object.freeze({ + Host: '', + IngressName: '', + Path: '', + ServiceName: '', +}); + +export class KubernetesIngressServiceRoute { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressServiceRoute))); + } +} + /** * KubernetesServicePort Model */ @@ -37,6 +74,7 @@ const _KubernetesServicePort = Object.freeze({ targetPort: 0, protocol: '', nodePort: 0, + ingress: '', }); export class KubernetesServicePort { diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 0fb529f02..c33d96855 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -120,16 +120,19 @@ class KubernetesApplicationService { const services = await this.KubernetesServiceService.get(namespace); const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; + const boundServices = KubernetesServiceHelper.findApplicationBoundServices(services, rootItem.value.Raw); const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value); application.Yaml = rootItem.value.Yaml; application.Raw = rootItem.value.Raw; application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item)); application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); + application.Services = boundServices; const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; application.AutoScaler = scaler; + application.Ingresses = ingresses; await this.KubernetesHistoryService.get(application); @@ -149,8 +152,10 @@ class KubernetesApplicationService { const convertToApplication = (item, converterFunc, services, pods, ingresses) => { const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const servicesFound = KubernetesServiceHelper.findApplicationBoundServices(services, item); const application = converterFunc(item, pods, service, ingresses); application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); + application.Services = servicesFound; return application; }; @@ -187,6 +192,7 @@ class KubernetesApplicationService { const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; application.AutoScaler = scaler; + application.Ingresses = await this.KubernetesIngressService.get(ns); }) ); return applications; @@ -214,7 +220,18 @@ class KubernetesApplicationService { * also be displayed in the summary output (getCreatedApplicationResources) */ async createAsync(formValues) { - let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + // formValues -> Application + let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + + if (services) { + services.forEach(async (service) => { + this.KubernetesServiceService.create(service); + if (service.Ingress) { + const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(formValues, service.Name, service.Ports); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); + } + }); + } if (service) { await this.KubernetesServiceService.create(service); @@ -261,8 +278,8 @@ class KubernetesApplicationService { * in this method should also be displayed in the summary output (getUpdatedApplicationResources) */ async patchAsync(oldFormValues, newFormValues) { - const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); - const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); + const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); + const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); const oldApiService = this._getApplicationApiService(oldApp); const newApiService = this._getApplicationApiService(newApp); @@ -271,6 +288,9 @@ class KubernetesApplicationService { if (oldService) { await this.KubernetesServiceService.delete(oldService); } + if (newService) { + return ''; + } return await this.create(newFormValues); } @@ -290,25 +310,54 @@ class KubernetesApplicationService { await newApiService.patch(oldApp, newApp); - if (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) { - 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) { - 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)); - } + if (oldServices.length === 0 && newServices.length !== 0) { + newServices.forEach(async (service) => { + await this.KubernetesServiceService.create(service); + if (service.Ingress) { + const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, service.Name, service.Ports); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + }); + } + + if (oldServices.length !== 0 && newServices.length === 0) { + oldServices.forEach(async (oldService) => { + if (oldService.Ingress) { + const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + }); + await this.KubernetesServiceService.deleteAll(oldServices); + } + + if (oldServices.length !== 0 && newServices.length !== 0) { + newServices.forEach(async (newService) => { + const oldServiceMatched = _.find(oldServices, { Name: newService.Name }); + if (oldServiceMatched) { + await this.KubernetesServiceService.patch(oldServiceMatched, newService); + if (newService.Ingress) { + const ingresses = KubernetesIngressConverter.editingFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + } else { + await this.KubernetesServiceService.create(newService); + if (newService.Ingress) { + const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + } + }); + + oldServices.forEach(async (oldService) => { + const newServiceMatched = _.find(newServices, { Name: oldService.Name }); + if (!newServiceMatched) { + await this.KubernetesServiceService.deleteSingle(oldService); + if (oldService.Ingress) { + const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } + } + }); } const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); @@ -381,16 +430,16 @@ class KubernetesApplicationService { } if (application.ServiceType) { - await this.KubernetesServiceService.delete(servicePayload); - const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length; - if (isIngress) { + await this.KubernetesServiceService.delete(application.Services); + + if (application.Ingresses.length) { 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); + const ingresses = KubernetesIngressConverter.applicationFormValuesToDeleteIngresses(formValues, application); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); } } diff --git a/app/kubernetes/services/serviceService.js b/app/kubernetes/services/serviceService.js index 64dcaa3cb..30aa82d07 100644 --- a/app/kubernetes/services/serviceService.js +++ b/app/kubernetes/services/serviceService.js @@ -14,6 +14,8 @@ class KubernetesServiceService { this.createAsync = this.createAsync.bind(this); this.patchAsync = this.patchAsync.bind(this); this.deleteAsync = this.deleteAsync.bind(this); + this.deleteSingleAsync = this.deleteSingleAsync.bind(this); + this.deleteAllAsync = this.deleteAllAsync.bind(this); } /** @@ -95,7 +97,43 @@ class KubernetesServiceService { /** * DELETE */ - async deleteAsync(service) { + async deleteAsync(services) { + services.forEach(async (service) => { + try { + const params = new KubernetesCommonParams(); + params.id = service.metadata.name; + const namespace = service.metadata.namespace; + await this.KubernetesServices(namespace).delete(params).$promise; + } catch (err) { + // eslint-disable-next-line no-console + console.error('unable to remove service', err); + } + }); + } + + delete(services) { + return this.$async(this.deleteAsync, services); + } + + async deleteAllAsync(formValuesServices) { + formValuesServices.forEach(async (service) => { + try { + const params = new KubernetesCommonParams(); + params.id = service.Name; + const namespace = service.Namespace; + await this.KubernetesServices(namespace).delete(params).$promise; + } catch (err) { + // eslint-disable-next-line no-console + console.error('unable to remove service', err); + } + }); + } + + deleteAll(formValuesServices) { + return this.$async(this.deleteAllAsync, formValuesServices); + } + + async deleteSingleAsync(service) { try { const params = new KubernetesCommonParams(); params.id = service.Name; @@ -107,8 +145,8 @@ class KubernetesServiceService { } } - delete(service) { - return this.$async(this.deleteAsync, service); + deleteSingle(service) { + return this.$async(this.deleteSingleAsync, service); } } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 912a583de..c06674084 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -9,1764 +9,1291 @@ -
- -
- Namespace -
- -
- -
- +
+ +
+ Namespace
-
-
-
- - This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the - namespace. -
-
-
-
- - You do not have access to any namespace. Contact your administrator to get access to a namespace. -
-
- - - - - - - - - - -

- - Portainer uses Kompose to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not - all the Compose format options are supported by Kompose at the moment. -

-

- You can get more information about Compose file format in the - official documentation. -

-
- -

- - This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). -

-

- You can get more information about Kubernetes file format in the - official documentation. -

-
-
-
- -
-
- Application -
- -
- + +
+
- + data-cy="k8sAppCreate-nsSelect" + >
-
-
-
-

This field is required.

-

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic - character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

-
-

An application with the same name already exists inside the selected namespace.

+
+
+ + This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + namespace. +
+
+
+
+ + You do not have access to any namespace. Contact your administrator to get access to a namespace.
- + + + -
-
- -
-
-
+ + + + +

+ + Portainer uses Kompose to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not + all the Compose format options are supported by Kompose at the moment. +

+

+ You can get more information about Compose file format in the + official documentation. +

+
+ +

+ + This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). +

+

+ You can get more information about Kubernetes file format in the + official documentation. +

+
+
+
+ +
- Stack + Application
- +
-
- - Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use - the application name. -
-
- -
- +
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters or '-', start with an + alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+

An application with the same name already exists inside the selected namespace.

+
+
-
- Environment -
- + +
- - - add environment variable - + +
+
+
+
+ Stack +
+ +
+
+ + Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to + use the application name. +
-
-
-
-
-
- name +
+ +
+ +
+
+ + +
+ Environment +
+ +
+
+ + + add environment variable + +
+ +
+
+
+
+
+ name + +
+
+ +
+ value
+ +
+ + +
+
+
+
+
+ +

Environment variable name is required.

+

This field must consist of alphabetic characters, digits, '_', '-', or '.', and + must not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

+
+

This environment variable is already defined.

+
+
+
+
+
+
+
+
+ + +
+ Configurations +
+ +
+
+ + + add configuration + +
+
+ + Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key + via the override button. +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+
+
+ The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: + + {{ key }}{{ $last ? '' : ', ' }} + +
+
+ + + +
+
+
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +
+ + +
-
- value +
+
+
+
+
+ +

Path is required.

+
+

This path is already used.

+
+
+
+
+
+
+ +
+ + + +
+ Persisting data +
+ +
+
+ + No storage option is available to persist data, contact your administrator to enable a storage option. +
+
+ +
+
+ + + add persisted folder + +
+ +
+
+
+ path in container -
- -
- - -
-
-
-
-
- -

Environment variable name is required.

-

This field must consist of alphabetic characters, digits, '_', '-', or '.', and must - not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

-
-

This environment variable is already defined.

-
-
-
-
-
-
-
-
- - -
- Configurations -
- -
-
- - - add configuration - -
-
- - Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key - via the override button. -
-
- - -
- -
- -
-
- - - -
- -
-
-
- The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: - - {{ key }}{{ $last ? '' : ', ' }} - -
-
- - - -
-
-
-
-
- configuration key - -
- -
-
- path on disk - -
-
- -
- - -
-
- -
-
-
-
-
- -

Path is required.

-
-

This path is already used.

-
-
-
-
-
-
- -
- - - -
- Persisting data -
- -
-
- - No storage option is available to persist data, contact your administrator to enable a storage option. -
-
- -
-
- - - add persisted folder - -
- -
-
-
- path in container - -
- -
- - - - -
- -
- requested size - - - - -
- -
- storage - - -
- -
- volume - -
- -
-
- - -
-
-
- -
-
-
- -

Path is required.

-
-

This path is already defined.

-
-
- -
- -
-
- -

Size is required.

-

This value must be greater than zero.

-
-
-
- -

Volume is required.

-
-

This volume is already used.

-
-
- -
-
-
-
- - - -
-
-
- -
-
- -
-
- Specify how the data will be used across instances. -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
- - -
- Resource reservations -
- -
-
- - Resource reservations are applied per instance of the application. -
-
- -
-
- - A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums - are inherited from the namespace quota. -
-
- -
-
- - This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the - namespace. -
-
- - -
- -
- -
-
- -
-
-

- Maximum memory usage (MB) -

-
-
-
-
-
-

Value must be between {{ ctrl.state.sliders.memory.min }} and - {{ ctrl.state.sliders.memory.max }} -

-
-
-
- - -
- -
- -
-
-

- Maximum CPU usage -

-
-
- -
-
- - These reservations would exceed the resources currently available in the cluster. -
-
- - - -
- Deployment -
- -
-
- Select how you want to deploy your application inside the cluster. -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
-
- - - -
-
- - -
-
-
-
- -

Instance count is required.

-

Instance count must be greater than 0.

-
-
-
- - -
-
- - This application will reserve the following resources: - {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and - {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. -
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the instance count. -
-
- -
-
- - The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. -
-
- - - -
- Auto-scaling -
- -
-
- - -
-
- -
-
-

- This feature is currently disabled and must be enabled by an administrator user. -

-

- Server metrics features must be enabled in the - environment configuration view. -

-
-
- -
- - - - - - - - - - - - - -
Minimum instancesMaximum instances - Target CPU usage (%) - - -
-
- -
-
-
- -

Minimum instances is required.

-

Minimum instances must be greater than 0.

-

Minimum instances must be smaller than maximum instances.

-
-
-
-
-
- -
-
-
- -

Maximum instances is required.

-

Maximum instances must be greater than minimum instances.

-
-
-
-
-
- -
-
-
- -

Target CPU usage is required.

-

Target CPU usage must be greater than 0.

-

Target CPU usage must be smaller than 100.

-
-
-
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. -
-
-
- - -
-
- Placement preferences and constraints -
- - -
-
- - - add rule - -
- -
- - Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. -
- -
-
-
- -
-
- -
- -
- - -
-
-
-
-
-

- This label is already defined. -

-
-
-
-
-
-
-
-
- -
-
- -
-
- Specify the policy associated to the placement rules. -
-
- - -
-
-
- - -
-
- - -
-
-
- -
- -
- -
- Publishing the application -
- -
-
- - -
-
- - -
-
- Select how you want to publish your application. -
-
- - -
-
-
- - - -
- -
- - - -
-
- - - -
-
- - - -
-
-
- - - -
-
- - - publish a new 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 30000-32767. -
-
- At least one published port must be defined. -
- -
- -
-
- container port - -
- -
- node port - -
- -
- load balancer port - -
- -
- ingress - -
- -
- hostname - -
- -
- route -
-
+ New volume Existing volume + +
+ +
+ requested size + + + + +
+ +
+ storage + + +
+ +
+ volume + +
+ +
+
+ +
- -
- -
-
+
-
-

Container port number is required.

-

Container port number must be inside the range 1-65535.

-

Container port number must be inside the range 1-65535.

-
-

- This port is already used. -

+ +

Path is required.

+
+

This path is already defined.

-
-
-
-

Node port number must be inside the range 30000-32767.

-

Node port number must be inside the range 30000-32767.

-
-

- This port is already used. -

-
-
+
-
-
-
-

Ingress selection is required.

-
+
+
+ +

Size is required.

+

This value must be greater than zero.

+
-
-
-
-

Route is required.

-

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').

-
-

- This route is already used. -

-
-
- -
-
-
-

Load balancer port number is required.

-

Load balancer port number must be inside the range 1-65535.

-

Load balancer port number must be inside the range 1-65535.

-
-

- - This port is already used. -

+ +

Volume is required.

+
+

This volume is already used.

-
- - + - - + +
+
+
+ +
+
+ +
+
+ Specify how the data will be used across instances. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+ Resource reservations +
+ +
+
+ + Resource reservations are applied per instance of the application. +
+
+ +
+
+ + A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums + are inherited from the namespace quota. +
+
+ +
+
+ + This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of + the namespace. +
+
+ + +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.state.sliders.memory.min }} and + {{ ctrl.state.sliders.memory.max }} +

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+ + These reservations would exceed the resources currently available in the cluster. +
+
+ + + +
+ Deployment +
+ +
+
+ Select how you want to deploy your application inside the cluster. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+
+
+ +

Instance count is required.

+

Instance count must be greater than 0.

+
+
+
+ + +
+
+ + This application will reserve the following resources: + {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. +
+
+ +
+
+ + This application would exceed available resources. Please review resource reservations or the instance count. +
+
+ +
+
+ + The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. +
+
+ + + +
+ Auto-scaling +
+ +
+
+ + +
+
+ +
+
+

+ This feature is currently disabled and must be enabled by an administrator user. +

+

+ Server metrics features must be enabled in the + environment configuration view. +

+
+
+ +
+ + + + + + + + + + + + + +
Minimum instancesMaximum instances + Target CPU usage (%) + + +
+
+ +
+
+
+ +

Minimum instances is required.

+

Minimum instances must be greater than 0.

+

Minimum instances must be smaller than maximum instances.

+
+
+
+
+
+ +
+
+
+ +

Maximum instances is required.

+

Maximum instances must be greater than minimum instances.

+
+
+
+
+
+ +
+
+
+ +

Target CPU usage is required.

+

Target CPU usage must be greater than 0.

+

Target CPU usage must be smaller than 100.

+
+
+
+
+ +
+
+ + This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. +
+
+
+ + +
+
+ Placement preferences and constraints +
+ + +
+
+ + + add rule + +
+ +
+ + Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. +
+ +
+
+
+ +
+
+ +
+ +
+ + +
+
+
+
+
+

+ This label is already defined. +

+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ Specify the policy associated to the placement rules. +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + + + + + + +
+
+
+ Namespace +
+ +
+ +
+ +
+
+
+
+ + This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + namespace. +
+
+
+
+ + You do not have access to any namespace. Contact your administrator to get access to a namespace. +
+
+ + + +
+ + +
Actions
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 9ba03ce19..15f41a3d9 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -720,7 +720,7 @@ class KubernetesCreateApplicationController { } publishViaLoadBalancerEnabled() { - return this.state.useLoadBalancer; + return this.state.useLoadBalancer && this.state.maxLoadBalancersQuota !== 0; } publishViaIngressEnabled() { @@ -799,6 +799,14 @@ class KubernetesCreateApplicationController { return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || isPublishingWithoutPorts; } + isExternalApplication() { + if (this.application) { + return KubernetesApplicationHelper.isExternalApplication(this.application); + } else { + return false; + } + } + disableLoadBalancerEdit() { return ( this.state.isEdit && diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 49df77531..49b4694d2 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -208,6 +208,17 @@ > Edit this application +