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 @@
+ 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 instances | +Maximum 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. + |
+
+
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 @@+ + Ensure that server metrics is + running inside your cluster. +
+ +