From 6756b04b67443aae58838c764f70f62ce2ae06ac Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Wed, 5 Aug 2020 00:08:11 +0200 Subject: [PATCH] feat(k8s/application): add the ability to set the auto-scale policy of an application (#4118) * feat(application): add horizontalpodautoscaler creation * feat(application): Add the ability to set the auto-scale policy of an application * feat(k8s/application): minor UI update * fix(application): set api version and prevent to use hpa with global deployment type * feat(settings): add a switch to enable features based on server metrics * feat(k8s/applications): minor UI update Co-authored-by: Anthony Lapenna --- api/kubernetes.go | 5 +- api/portainer.go | 5 +- app/kubernetes/converters/application.js | 1 + app/kubernetes/helpers/application/index.js | 15 +++ .../horizontal-pod-auto-scaler/converter.js | 112 ++++++++++++++++- .../horizontal-pod-auto-scaler/helper.js | 28 ++--- .../horizontal-pod-auto-scaler/models.js | 2 +- .../horizontal-pod-auto-scaler/payload.js | 86 +++++++++++++ .../horizontal-pod-auto-scaler/service.js | 114 ++++++++--------- .../models/application/formValues.js | 20 ++- app/kubernetes/services/applicationService.js | 38 +++++- .../create/createApplication.html | 118 +++++++++++++++++- .../create/createApplicationController.js | 41 +++++- .../views/applications/edit/application.html | 2 +- app/kubernetes/views/configure/configure.html | 27 +++- .../views/configure/configureController.js | 4 + 16 files changed, 534 insertions(+), 84 deletions(-) create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/payload.js diff --git a/api/kubernetes.go b/api/kubernetes.go index 6ca8a3a78..a4d1beb4f 100644 --- a/api/kubernetes.go +++ b/api/kubernetes.go @@ -3,8 +3,9 @@ package portainer func KubernetesDefault() KubernetesData { return KubernetesData{ Configuration: KubernetesConfiguration{ - UseLoadBalancer: false, - StorageClasses: []KubernetesStorageClassConfig{}, + UseLoadBalancer: false, + UseServerMetrics: false, + StorageClasses: []KubernetesStorageClassConfig{}, }, Snapshots: []KubernetesSnapshot{}, } diff --git a/api/portainer.go b/api/portainer.go index b96cfdbe7..4cc3947fb 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -337,8 +337,9 @@ type ( // KubernetesConfiguration represents the configuration of a Kubernetes endpoint KubernetesConfiguration struct { - UseLoadBalancer bool `json:"UseLoadBalancer"` - StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` + UseLoadBalancer bool `json:"UseLoadBalancer"` + UseServerMetrics bool `json:"UseServerMetrics"` + StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 4491e2e4a..cd29a1160 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -260,6 +260,7 @@ class KubernetesApplicationConverter { res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env); res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); + res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler); if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 4075b2e0a..8b0513de6 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -9,6 +9,7 @@ import { KubernetesApplicationConfigurationFormValueOverridenKey, KubernetesApplicationPersistedFolderFormValue, KubernetesApplicationPublishedPortFormValue, + KubernetesApplicationAutoScalerFormValue, } from 'Kubernetes/models/application/formValues'; import { KubernetesApplicationEnvConfigMapPayload, @@ -263,6 +264,20 @@ class KubernetesApplicationHelper { return finalRes; } + static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler) { + const res = new KubernetesApplicationAutoScalerFormValue(); + if (autoScaler) { + res.IsUsed = true; + res.MinReplicas = autoScaler.MinReplicas; + res.MaxReplicas = autoScaler.MaxReplicas; + res.TargetCPUUtilization = autoScaler.TargetCPUUtilization; + res.ApiVersion = autoScaler.ApiVersion; + } else { + res.ApiVersion = 'apps/v1'; + } + return res; + } + /** * !APPLICATION TO FORMVALUES FUNCTIONS */ diff --git a/app/kubernetes/horizontal-pod-auto-scaler/converter.js b/app/kubernetes/horizontal-pod-auto-scaler/converter.js index 4c5ccea75..5ccfdf9c9 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/converter.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/converter.js @@ -1,4 +1,6 @@ +import * as JsonPatch from 'fast-json-patch'; import { KubernetesHorizontalPodAutoScaler } from './models'; +import { KubernetesHorizontalPodAutoScalerCreatePayload } from './payload'; export class KubernetesHorizontalPodAutoScalerConverter { /** @@ -11,7 +13,8 @@ export class KubernetesHorizontalPodAutoScalerConverter { res.Name = data.metadata.name; res.MinReplicas = data.spec.minReplicas; res.MaxReplicas = data.spec.maxReplicas; - res.TargetCPUUtilizationPercentage = data.spec.targetCPUUtilizationPercentage; + res.TargetCPUUtilization = data.spec.targetCPUUtilizationPercentage; + if (data.spec.scaleTargetRef) { res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; @@ -20,4 +23,111 @@ export class KubernetesHorizontalPodAutoScalerConverter { res.Yaml = yaml ? yaml.data : ''; return res; } + + static createPayload(data) { + const payload = new KubernetesHorizontalPodAutoScalerCreatePayload(); + payload.metadata.namespace = data.Namespace; + payload.metadata.name = data.TargetEntity.Name; + payload.spec.minReplicas = data.MinReplicas; + payload.spec.maxReplicas = data.MaxReplicas; + payload.spec.targetCPUUtilizationPercentage = data.TargetCPUUtilization; + payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion; + payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind; + payload.spec.scaleTargetRef.name = data.TargetEntity.Name; + return payload; + } + + static patchPayload(oldScaler, newScaler) { + const oldPayload = KubernetesHorizontalPodAutoScalerConverter.createPayload(oldScaler); + const newPayload = KubernetesHorizontalPodAutoScalerConverter.createPayload(newScaler); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } + + static applicationFormValuesToModel(formValues, kind) { + const res = new KubernetesHorizontalPodAutoScaler(); + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.MinReplicas = formValues.AutoScaler.MinReplicas; + res.MaxReplicas = formValues.AutoScaler.MaxReplicas; + res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization; + res.TargetEntity.Name = formValues.Name; + res.TargetEntity.Kind = kind; + res.TargetEntity.ApiVersion = formValues.AutoScaler.ApiVersion; + return res; + } + + /** + * Convertion functions to use with v2beta2 model + */ + + // static apiToModel(data, yaml) { + // const res = new KubernetesHorizontalPodAutoScaler(); + // res.Id = data.metadata.uid; + // res.Namespace = data.metadata.namespace; + // res.Name = data.metadata.name; + // res.MinReplicas = data.spec.minReplicas; + // res.MaxReplicas = data.spec.maxReplicas; + // res.TargetCPUUtilization = data.spec.targetCPUUtilization; + + // _.forEach(data.spec.metrics, (metric) => { + // if (metric.type === 'Resource') { + // if (metric.resource.name === 'cpu') { + // res.TargetCPUUtilization = metric.resource.target.averageUtilization; + // } + // if (metric.resource.name === 'memory') { + // res.TargetMemoryValue = parseFloat(metric.resource.target.averageValue) / 1000; + // } + // } + // }); + + // if (data.spec.scaleTargetRef) { + // res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; + // res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; + // res.TargetEntity.Name = data.spec.scaleTargetRef.name; + // } + // res.Yaml = yaml ? yaml.data : ''; + // return res; + // } + + // static createPayload(data) { + // const payload = new KubernetesHorizontalPodAutoScalerCreatePayload(); + // payload.metadata.namespace = data.Namespace; + // payload.metadata.name = data.TargetEntity.Name; + // payload.spec.minReplicas = data.MinReplicas; + // payload.spec.maxReplicas = data.MaxReplicas; + + // if (data.TargetMemoryValue) { + // const memoryMetric = new KubernetesHorizontalPodAutoScalerMemoryMetric(); + // memoryMetric.resource.target.averageValue = data.TargetMemoryValue; + // payload.spec.metrics.push(memoryMetric); + // } + + // if (data.TargetCPUUtilization) { + // const cpuMetric = new KubernetesHorizontalPodAutoScalerCPUMetric(); + // cpuMetric.resource.target.averageUtilization = data.TargetCPUUtilization; + // payload.spec.metrics.push(cpuMetric); + // } + + // payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion; + // payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind; + // payload.spec.scaleTargetRef.name = data.TargetEntity.Name; + + // return payload; + // } + + // static applicationFormValuesToModel(formValues, kind) { + // const res = new KubernetesHorizontalPodAutoScaler(); + // res.Name = formValues.Name; + // res.Namespace = formValues.ResourcePool.Namespace.Name; + // res.MinReplicas = formValues.AutoScaler.MinReplicas; + // res.MaxReplicas = formValues.AutoScaler.MaxReplicas; + // res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization; + // if (formValues.AutoScaler.TargetMemoryValue) { + // res.TargetMemoryValue = formValues.AutoScaler.TargetMemoryValue + 'M'; + // } + // res.TargetEntity.Name = formValues.Name; + // res.TargetEntity.Kind = kind; + // return res; + // } } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/helper.js b/app/kubernetes/horizontal-pod-auto-scaler/helper.js index af8061b85..663c3baf8 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/helper.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/helper.js @@ -5,22 +5,22 @@ import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; -function _getApplicationTypeString(app) { - if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { - return KubernetesApplicationTypeStrings.DEPLOYMENT; - } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { - return KubernetesApplicationTypeStrings.DAEMONSET; - } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { - return KubernetesApplicationTypeStrings.STATEFULSET; - // } else if () { ---> TODO: refactor - handle bare pod type ! - } else { - throw new PortainerError('Unable to determine application type'); - } -} - export class KubernetesHorizontalPodAutoScalerHelper { static findApplicationBoundScaler(sList, app) { - const kind = _getApplicationTypeString(app); + const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name); } + + static getApplicationTypeString(app) { + if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { + return KubernetesApplicationTypeStrings.DEPLOYMENT; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { + return KubernetesApplicationTypeStrings.DAEMONSET; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { + return KubernetesApplicationTypeStrings.STATEFULSET; + // } else if () { ---> TODO: refactor - handle bare pod type ! + } else { + throw new PortainerError('Unable to determine application type'); + } + } } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/models.js b/app/kubernetes/horizontal-pod-auto-scaler/models.js index 1cba85d7d..1e4bf9122 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/models.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/models.js @@ -7,7 +7,7 @@ const _KubernetesHorizontalPodAutoScaler = Object.freeze({ Name: '', MinReplicas: 1, MaxReplicas: 1, - TargetCPUUtilizationPercentage: undefined, + TargetCPUUtilization: 0, TargetEntity: { ApiVersion: '', Kind: '', diff --git a/app/kubernetes/horizontal-pod-auto-scaler/payload.js b/app/kubernetes/horizontal-pod-auto-scaler/payload.js new file mode 100644 index 000000000..d6c67edab --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/payload.js @@ -0,0 +1,86 @@ +/** + * KubernetesHorizontalPodAutoScaler Create Payload Model + */ +const _KubernetesHorizontalPodAutoScalerCreatePayload = Object.freeze({ + metadata: { + namespace: '', + name: '', + }, + spec: { + maxReplicas: 0, + minReplicas: 0, + targetCPUUtilizationPercentage: 0, + scaleTargetRef: { + kind: '', + name: '', + }, + }, +}); + +export class KubernetesHorizontalPodAutoScalerCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCreatePayload))); + } +} + +/** + * KubernetesHorizontalPodAutoScaler Create Payload Model for v2beta2 + * Include support of memory usage + */ + +// const _KubernetesHorizontalPodAutoScalerCreatePayload = Object.freeze({ +// metadata: { +// namespace: '', +// name: '' +// }, +// spec: { +// maxReplicas: 0, +// minReplicas: 0, +// targetCPUUtilizationPercentage: 0, +// scaleTargetRef: { +// kind: '', +// name: '' +// }, +// metrics: [] +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerCreatePayload { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCreatePayload))); +// } +// } + +// const _KubernetesHorizontalPodAutoScalerCPUMetric = Object.freeze({ +// type: 'Resource', +// resource: { +// name: 'cpu', +// target: { +// type: 'Utilization', +// averageUtilization: 0 +// } +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerCPUMetric { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCPUMetric))); +// } +// } + +// const _KubernetesHorizontalPodAutoScalerMemoryMetric = Object.freeze({ +// type: 'Resource', +// resource: { +// name: 'memory', +// target: { +// type: 'AverageValue', +// averageValue: '' +// } +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerMemoryMetric { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerMemoryMetric))); +// } +// } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/service.js b/app/kubernetes/horizontal-pod-auto-scaler/service.js index df33457ce..d9ab6f50f 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/service.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/service.js @@ -12,10 +12,10 @@ class KubernetesHorizontalPodAutoScalerService { this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); - // this.createAsync = this.createAsync.bind(this); - // this.patchAsync = this.patchAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); // this.rollbackAsync = this.rollbackAsync.bind(this); - // this.deleteAsync = this.deleteAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); } /** @@ -53,65 +53,65 @@ class KubernetesHorizontalPodAutoScalerService { return this.$async(this.getAllAsync, namespace); } - // /** - // * CREATE - // */ - // async createAsync(horizontalPodAutoScaler) { - // try { - // const params = {}; - // const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); - // const namespace = payload.metadata.namespace; - // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; - // return data; - // } catch (err) { - // throw new PortainerError('Unable to create horizontalPodAutoScaler', err); - // } - // } + /** + * CREATE + */ + async createAsync(horizontalPodAutoScaler) { + try { + const params = {}; + const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create horizontalPodAutoScaler', err); + } + } - // create(horizontalPodAutoScaler) { - // return this.$async(this.createAsync, horizontalPodAutoScaler); - // } + create(horizontalPodAutoScaler) { + return this.$async(this.createAsync, horizontalPodAutoScaler); + } - // /** - // * PATCH - // */ - // async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { - // try { - // const params = new KubernetesCommonParams(); - // params.id = newHorizontalPodAutoScaler.Name; - // const namespace = newHorizontalPodAutoScaler.Namespace; - // const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); - // if (!payload.length) { - // return; - // } - // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; - // return data; - // } catch (err) { - // throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); - // } - // } + /** + * PATCH + */ + async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + try { + const params = new KubernetesCommonParams(); + params.id = newHorizontalPodAutoScaler.Name; + const namespace = newHorizontalPodAutoScaler.Namespace; + const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + if (!payload.length) { + return; + } + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); + } + } - // patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { - // return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); - // } + patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + } - // /** - // * DELETE - // */ - // async deleteAsync(horizontalPodAutoScaler) { - // try { - // const params = new KubernetesCommonParams(); - // params.id = horizontalPodAutoScaler.Name; - // const namespace = horizontalPodAutoScaler.Namespace; - // await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; - // } catch (err) { - // throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); - // } - // } + /** + * DELETE + */ + async deleteAsync(horizontalPodAutoScaler) { + try { + const params = new KubernetesCommonParams(); + params.id = horizontalPodAutoScaler.Name; + const namespace = horizontalPodAutoScaler.Namespace; + await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); + } + } - // delete(horizontalPodAutoScaler) { - // return this.$async(this.deleteAsync, horizontalPodAutoScaler); - // } + delete(horizontalPodAutoScaler) { + return this.$async(this.deleteAsync, horizontalPodAutoScaler); + } // /** // * ROLLBACK diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 72af5bbf2..49d3cc827 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -1,4 +1,4 @@ -import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationDataAccessPolicies } from './models'; +import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes } from './models'; /** * KubernetesApplicationFormValues Model @@ -21,6 +21,7 @@ const _KubernetesApplicationFormValues = Object.freeze({ PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, Configurations: [], // KubernetesApplicationConfigurationFormValue list + AutoScaler: {}, }); export class KubernetesApplicationFormValues { @@ -116,3 +117,20 @@ export class KubernetesApplicationPublishedPortFormValue { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue))); } } + +/** + * KubernetesApplicationAutoScalerFormValue Model + */ +const _KubernetesApplicationAutoScalerFormValue = Object.freeze({ + MinReplicas: 0, + MaxReplicas: 0, + TargetCPUUtilization: 50, + ApiVersion: '', + IsUsed: false, +}); + +export class KubernetesApplicationAutoScalerFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationAutoScalerFormValue))); + } +} diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index d4172f606..a6bfdd2f7 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -12,6 +12,7 @@ import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; import { KubernetesApplication } from 'Kubernetes/models/application/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; +import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter'; class KubernetesApplicationService { /* @ngInject */ @@ -141,13 +142,14 @@ class KubernetesApplicationService { const res = await Promise.all( _.map(namespaces, async (ns) => { - const [deployments, daemonSets, statefulSets, services, pods, ingresses] = await Promise.all([ + const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([ this.KubernetesDeploymentService.get(ns), this.KubernetesDaemonSetService.get(ns), this.KubernetesStatefulSetService.get(ns), this.KubernetesServiceService.get(ns), this.KubernetesPodService.get(ns), this.KubernetesIngressService.get(ns), + this.KubernetesHorizontalPodAutoScalerService.get(ns), ]); const deploymentApplications = _.map(deployments, (item) => @@ -160,7 +162,15 @@ class KubernetesApplicationService { convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) ); - return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + await Promise.all( + _.forEach(applications, async (application) => { + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + }) + ); + return applications; }) ); return _.flatten(res); @@ -206,6 +216,12 @@ class KubernetesApplicationService { await Promise.all(_.without(claimPromises, undefined)); } + if (formValues.AutoScaler.IsUsed) { + const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); + const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind); + await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler); + } + await apiService.create(app); } catch (err) { throw err; @@ -257,6 +273,20 @@ class KubernetesApplicationService { } else if (oldService && !newService) { await this.KubernetesServiceService.delete(oldService); } + + const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); + const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind); + if (_.isEmpty(oldFormValues.AutoScaler)) { + await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); + } else { + const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp); + const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind); + if (newFormValues.AutoScaler.IsUsed) { + await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler); + } else { + await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler); + } + } } catch (err) { throw err; } @@ -319,6 +349,10 @@ class KubernetesApplicationService { if (application.ServiceType) { await this.KubernetesServiceService.delete(servicePayload); } + + if (!_.isEmpty(application.AutoScaler)) { + await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler); + } } catch (err) { throw err; } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index d9421b904..4dfaf0d00 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -635,7 +635,13 @@
- +
+ +
+ Auto-scaling +
+ +
+
+ + +
+
+ +
+
+

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

+

+ Server metrics features must be enabled in the + endpoint 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. +
+
+
+ +
Publishing the application
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 86679639f..c91ee8031 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -22,6 +22,7 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel import KubernetesApplicationConverter from 'Kubernetes/converters/application'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index'; class KubernetesCreateApplicationController { /* @ngInject */ @@ -80,6 +81,16 @@ class KubernetesCreateApplicationController { this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication); } + /** + * AUTO SCALER UI MANAGEMENT + */ + + unselectAutoScaler() { + if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) { + this.formValues.AutoScaler.IsUsed = false; + } + } + /** * CONFIGURATION UI MANAGEMENT */ @@ -319,6 +330,24 @@ class KubernetesCreateApplicationController { return false; } + autoScalerOverflow() { + const instances = this.formValues.AutoScaler.MaxReplicas; + const cpu = this.formValues.CpuLimit; + const maxCpu = this.state.sliders.cpu.max; + const memory = this.formValues.MemoryLimit; + const maxMemory = this.state.sliders.memory.max; + + if (cpu * instances > maxCpu) { + return true; + } + + if (memory * instances > maxMemory) { + return true; + } + + return false; + } + publishViaLoadBalancerEnabled() { return this.state.useLoadBalancer; } @@ -345,11 +374,12 @@ class KubernetesCreateApplicationController { isDeployUpdateButtonDisabled() { const overflow = this.resourceReservationsOverflow(); + const autoScalerOverflow = this.autoScalerOverflow(); const inProgress = this.state.actionInProgress; const invalid = !this.isValid(); const hasNoChanges = this.isEditAndNoChangesMade(); const nonScalable = this.isNonScalable(); - const res = overflow || inProgress || invalid || hasNoChanges || nonScalable; + const res = overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable; return res; } @@ -549,6 +579,7 @@ class KubernetesCreateApplicationController { this.state = { actionInProgress: false, useLoadBalancer: false, + useServerMetrics: false, sliders: { cpu: { min: 0, @@ -580,6 +611,8 @@ class KubernetesCreateApplicationController { }, }; + this.isAdmin = this.Authentication.isAdmin(); + this.editChanges = []; if (this.$transition$.params().namespace && this.$transition$.params().name) { @@ -587,8 +620,10 @@ class KubernetesCreateApplicationController { } const endpoint = this.EndpointProvider.currentEndpoint(); + this.endpoint = endpoint; this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses; this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer; + this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics; this.formValues = new KubernetesApplicationFormValues(); @@ -611,6 +646,10 @@ class KubernetesCreateApplicationController { this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims); this.savedFormValues = angular.copy(this.formValues); delete this.formValues.ApplicationType; + } else { + this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(); + this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount; + this.formValues.AutoScaler.MaxReplicas = this.formValues.ReplicaCount; } await this.updateSliders(); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 47eff6d7c..0a656635b 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -355,7 +355,7 @@ {{ ctrl.application.AutoScaler.MinReplicas }} {{ ctrl.application.AutoScaler.MaxReplicas }} - {{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}% + {{ ctrl.application.AutoScaler.TargetCPUUtilization }}% diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 697c19e6c..e50f5a2b2 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -13,10 +13,11 @@
Expose applications over external IP addresses
+
Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider. -

+

Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.

@@ -31,6 +32,30 @@
+
+ Metrics +
+ +
+ + Enabling this feature will allow users to use specific features that leverage the server metrics component. +

+ + Ensure that server metrics is + running inside your cluster. +

+
+
+ +
+
+ + +
+
+
Available storage options
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 3a16083ce..7e2430bb6 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -50,12 +50,14 @@ class KubernetesConfigureController { this.endpoint.Kubernetes.Configuration.StorageClasses = classes; this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint); const endpoints = this.EndpointProvider.endpoints(); const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id); if (modifiedEndpoint) { modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes; modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; this.EndpointProvider.setEndpoints(endpoints); } this.Notifications.success('Configuration successfully applied'); @@ -80,6 +82,7 @@ class KubernetesConfigureController { this.formValues = { UseLoadBalancer: false, + UseServerMetrics: false, }; try { @@ -100,6 +103,7 @@ class KubernetesConfigureController { }); this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; + this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve storage classes'); } finally {