From ccf6babc021be40dbc0046a87a3d9c9449b4516e Mon Sep 17 00:00:00 2001 From: Alice Groux Date: Fri, 26 Feb 2021 16:50:33 +0100 Subject: [PATCH] refactor(app): backport technical changes (#4679) * refactor(app): backport technical changes * refactor(app): remove EE only features * feat(app): small review changes to match EE codebase layout on some files Co-authored-by: xAt0mZ --- app/docker/helpers/containerHelper.js | 6 +- app/kubernetes/converters/application.js | 2 +- .../converters/persistentVolumeClaim.js | 6 +- app/kubernetes/converters/resourcePool.js | 23 + app/kubernetes/converters/resourceQuota.js | 98 +-- app/kubernetes/converters/service.js | 2 +- app/kubernetes/converters/storageClass.js | 3 +- app/kubernetes/helpers/application/index.js | 4 +- app/kubernetes/helpers/commonHelper.js | 8 + app/kubernetes/helpers/resourceQuotaHelper.js | 22 + .../helpers/resourceReservationHelper.js | 2 +- app/kubernetes/ingress/converter.js | 9 +- app/kubernetes/ingress/helper.js | 2 +- app/kubernetes/ingress/service.js | 131 ++-- .../models/application/formValues.js | 4 +- app/kubernetes/models/namespace/models.js | 27 +- .../models/resource-pool/formValues.js | 4 +- .../models/resource-quota/models.js | 41 +- .../models/resource-quota/payloads.js | 56 +- app/kubernetes/rest/resourceQuota.js | 6 + app/kubernetes/services/applicationService.js | 2 +- .../services/resourcePoolService.js | 150 +++-- .../services/resourceQuotaService.js | 126 ++-- app/kubernetes/services/volumeService.js | 5 +- .../applications/applicationsController.js | 2 +- .../create/createApplication.html | 235 +++---- .../create/createApplicationController.js | 590 +++++++++--------- .../edit/applicationController.js | 6 +- .../placements-datatable/controller.js | 2 +- .../create/createConfiguration.html | 1 - .../views/configure/configureController.js | 8 +- .../create/createResourcePoolController.js | 11 +- .../resource-pools/edit/resourcePool.html | 81 ++- .../edit/resourcePoolController.js | 180 +++--- app/kubernetes/views/volumes/edit/volume.html | 5 +- .../views/volumes/edit/volumeController.js | 19 +- .../views/volumes/volumesController.js | 28 +- package.json | 1 + webpack/webpack.common.js | 9 + webpack/webpack.develop.js | 10 - 40 files changed, 951 insertions(+), 976 deletions(-) diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index 29a7e99b9..6daf5d4e7 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -5,7 +5,7 @@ const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655 function parsePort(port) { if (portPattern.test(port)) { - return parseInt(port); + return parseInt(port, 10); } else { return 0; } @@ -211,14 +211,14 @@ angular.module('portainer.docker').factory('ContainerHelper', [ _.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => { // Sort by host port const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => { - return parseInt(_.split(portKey, '/')[0]); + return parseInt(_.split(portKey, '/')[0], 10); }); let previousHostPort = -1; let previousContainerPort = -1; _.forEach(sortedPortBindingKeys, (portKey) => { const portKeySplit = _.split(portKey, '/'); - const containerPort = parseInt(portKeySplit[0]); + const containerPort = parseInt(portKeySplit[0], 10); const portBinding = portBindings[portKey][0]; portBindings[portKey].shift(); const hostPort = parsePort(portBinding.HostPort); diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 94363767c..97f257f95 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import { diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js index 5985a4723..2d0f2f308 100644 --- a/app/kubernetes/converters/persistentVolumeClaim.js +++ b/app/kubernetes/converters/persistentVolumeClaim.js @@ -12,7 +12,7 @@ class KubernetesPersistentVolumeClaimConverter { res.Name = data.metadata.name; res.Namespace = data.metadata.namespace; res.CreationDate = data.metadata.creationTimestamp; - res.Storage = data.spec.resources.requests.storage.replace('i', 'B'); + res.Storage = `${data.spec.resources.requests.storage}B`; res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName }); res.Yaml = yaml ? yaml.data : ''; res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : ''; @@ -35,7 +35,7 @@ class KubernetesPersistentVolumeClaimConverter { pvc.PreviousName = item.PersistentVolumeClaimName; } pvc.StorageClass = existantPVC.StorageClass; - pvc.Storage = existantPVC.Storage.charAt(0) + 'i'; + pvc.Storage = existantPVC.Storage.charAt(0); pvc.CreationDate = existantPVC.CreationDate; pvc.Id = existantPVC.Id; } else { @@ -45,7 +45,7 @@ class KubernetesPersistentVolumeClaimConverter { } else { pvc.Name = formValues.Name + '-' + pvc.Name; } - pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i'; + pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0); pvc.StorageClass = item.StorageClass; } pvc.MountPath = item.ContainerPath; diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js index daf2f002d..234d13d27 100644 --- a/app/kubernetes/converters/resourcePool.js +++ b/app/kubernetes/converters/resourcePool.js @@ -1,4 +1,9 @@ +import _ from 'lodash-es'; + import { KubernetesResourcePool } from 'Kubernetes/models/resource-pool/models'; +import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; +import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; +import KubernetesResourceQuotaConverter from './resourceQuota'; class KubernetesResourcePoolConverter { static apiToResourcePool(namespace) { @@ -7,6 +12,24 @@ class KubernetesResourcePoolConverter { res.Yaml = namespace.Yaml; return res; } + + static formValuesToResourcePool(formValues) { + const namespace = new KubernetesNamespace(); + namespace.Name = formValues.Name; + namespace.ResourcePoolName = formValues.Name; + namespace.ResourcePoolOwner = formValues.Owner; + + const quota = KubernetesResourceQuotaConverter.resourcePoolFormValuesToResourceQuota(formValues); + + const ingMap = _.map(formValues.IngressClasses, (c) => { + if (c.Selected) { + c.Namespace = namespace.Name; + return KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); + } + }); + const ingresses = _.without(ingMap, undefined); + return [namespace, quota, ingresses]; + } } export default KubernetesResourcePoolConverter; diff --git a/app/kubernetes/converters/resourceQuota.js b/app/kubernetes/converters/resourceQuota.js index 5ba76276f..ae22510cb 100644 --- a/app/kubernetes/converters/resourceQuota.js +++ b/app/kubernetes/converters/resourceQuota.js @@ -1,10 +1,20 @@ +import * as JsonPatch from 'fast-json-patch'; import filesizeParser from 'filesize-parser'; -import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; -import { KubernetesResourceQuotaCreatePayload, KubernetesResourceQuotaUpdatePayload } from 'Kubernetes/models/resource-quota/payloads'; +import { + KubernetesResourceQuota, + KubernetesPortainerResourceQuotaCPULimit, + KubernetesPortainerResourceQuotaMemoryLimit, + KubernetesPortainerResourceQuotaCPURequest, + KubernetesPortainerResourceQuotaMemoryRequest, + KubernetesResourceQuotaDefaults, +} from 'Kubernetes/models/resource-quota/models'; +import { KubernetesResourceQuotaCreatePayload } from 'Kubernetes/models/resource-quota/payloads'; import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues'; class KubernetesResourceQuotaConverter { static apiToResourceQuota(data, yaml) { @@ -14,21 +24,21 @@ class KubernetesResourceQuotaConverter { res.Name = data.metadata.name; res.CpuLimit = 0; res.MemoryLimit = 0; - if (data.spec.hard && data.spec.hard['limits.cpu']) { - res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard['limits.cpu']); + if (data.spec.hard && data.spec.hard[KubernetesPortainerResourceQuotaCPULimit]) { + res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard[KubernetesPortainerResourceQuotaCPULimit]); } - if (data.spec.hard && data.spec.hard['limits.memory']) { - res.MemoryLimit = filesizeParser(data.spec.hard['limits.memory'], { base: 10 }); + if (data.spec.hard && data.spec.hard[KubernetesPortainerResourceQuotaMemoryLimit]) { + res.MemoryLimit = filesizeParser(data.spec.hard[KubernetesPortainerResourceQuotaMemoryLimit], { base: 10 }); } res.MemoryLimitUsed = 0; - if (data.status.used && data.status.used['limits.memory']) { - res.MemoryLimitUsed = filesizeParser(data.status.used['limits.memory'], { base: 10 }); + if (data.status.used && data.status.used[KubernetesPortainerResourceQuotaMemoryLimit]) { + res.MemoryLimitUsed = filesizeParser(data.status.used[KubernetesPortainerResourceQuotaMemoryLimit], { base: 10 }); } res.CpuLimitUsed = 0; - if (data.status.used && data.status.used['limits.cpu']) { - res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used['limits.cpu']); + if (data.status.used && data.status.used[KubernetesPortainerResourceQuotaCPULimit]) { + res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used[KubernetesPortainerResourceQuotaCPULimit]); } res.Yaml = yaml ? yaml.data : ''; res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; @@ -40,48 +50,54 @@ class KubernetesResourceQuotaConverter { const res = new KubernetesResourceQuotaCreatePayload(); res.metadata.name = KubernetesResourceQuotaHelper.generateResourceQuotaName(quota.Namespace); res.metadata.namespace = quota.Namespace; - res.spec.hard['requests.cpu'] = quota.CpuLimit; - res.spec.hard['requests.memory'] = quota.MemoryLimit; - res.spec.hard['limits.cpu'] = quota.CpuLimit; - res.spec.hard['limits.memory'] = quota.MemoryLimit; + KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaCPURequest}']`, quota.CpuLimit); + KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaMemoryRequest}']`, quota.MemoryLimit); + KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaCPULimit}']`, quota.CpuLimit); + KubernetesCommonHelper.assignOrDeleteIfEmptyOrZero(res, `spec.hard['${KubernetesPortainerResourceQuotaMemoryLimit}']`, quota.MemoryLimit); res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; if (quota.ResourcePoolOwner) { res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; } - if (!quota.CpuLimit || quota.CpuLimit === 0) { - delete res.spec.hard['requests.cpu']; - delete res.spec.hard['limits.cpu']; - } - if (!quota.MemoryLimit || quota.MemoryLimit === 0) { - delete res.spec.hard['requests.memory']; - delete res.spec.hard['limits.memory']; - } return res; } static updatePayload(quota) { - const res = new KubernetesResourceQuotaUpdatePayload(); - res.metadata.name = quota.Name; - res.metadata.namespace = quota.Namespace; + const res = KubernetesResourceQuotaConverter.createPayload(quota); res.metadata.uid = quota.Id; - res.spec.hard['requests.cpu'] = quota.CpuLimit; - res.spec.hard['requests.memory'] = quota.MemoryLimit; - res.spec.hard['limits.cpu'] = quota.CpuLimit; - res.spec.hard['limits.memory'] = quota.MemoryLimit; - res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; - if (quota.ResourcePoolOwner) { - res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; - } - if (!quota.CpuLimit || quota.CpuLimit === 0) { - delete res.spec.hard['requests.cpu']; - delete res.spec.hard['limits.cpu']; - } - if (!quota.MemoryLimit || quota.MemoryLimit === 0) { - delete res.spec.hard['requests.memory']; - delete res.spec.hard['limits.memory']; - } return res; } + + static patchPayload(oldQuota, newQuota) { + const oldPayload = KubernetesResourceQuotaConverter.createPayload(oldQuota); + const newPayload = KubernetesResourceQuotaConverter.createPayload(newQuota); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } + + static quotaToResourcePoolFormValues(quota) { + const res = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults); + res.Name = quota.Namespace; + res.CpuLimit = quota.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit); + if (res.CpuLimit || res.MemoryLimit) { + res.HasQuota = true; + } + res.StorageClasses = quota.StorageRequests; + return res; + } + + static resourcePoolFormValuesToResourceQuota(formValues) { + if (formValues.HasQuota) { + const quota = new KubernetesResourceQuota(formValues.Name); + if (formValues.HasQuota) { + quota.CpuLimit = formValues.CpuLimit; + quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + } + quota.ResourcePoolName = formValues.Name; + quota.ResourcePoolOwner = formValues.Owner; + return quota; + } + } } export default KubernetesResourceQuotaConverter; diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js index 7c8897be5..d83b0e636 100644 --- a/app/kubernetes/converters/service.js +++ b/app/kubernetes/converters/service.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; diff --git a/app/kubernetes/converters/storageClass.js b/app/kubernetes/converters/storageClass.js index 7491c32e7..7a2afca0b 100644 --- a/app/kubernetes/converters/storageClass.js +++ b/app/kubernetes/converters/storageClass.js @@ -1,6 +1,7 @@ +import * as JsonPatch from 'fast-json-patch'; + import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-class/payload'; -import * as JsonPatch from 'fast-json-patch'; class KubernetesStorageClassConverter { /** diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index edfe44c5c..429803b06 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; @@ -322,7 +322,7 @@ class KubernetesApplicationHelper { const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName)); const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass); res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName; - res.Size = parseInt(pvc.Storage.slice(0, -2)); + res.Size = parseInt(pvc.Storage, 10); res.SizeUnit = pvc.Storage.slice(-2); res.ContainerPath = folder.MountPath; return res; diff --git a/app/kubernetes/helpers/commonHelper.js b/app/kubernetes/helpers/commonHelper.js index fab1bdf28..1ad2ba34f 100644 --- a/app/kubernetes/helpers/commonHelper.js +++ b/app/kubernetes/helpers/commonHelper.js @@ -16,5 +16,13 @@ class KubernetesCommonHelper { label = _.replace(label, /[-_.]*$/g, ''); return label; } + + static assignOrDeleteIfEmptyOrZero(obj, path, value) { + if (!value || value === 0 || (value instanceof Array && !value.length)) { + _.unset(obj, path); + } else { + _.set(obj, path, value); + } + } } export default KubernetesCommonHelper; diff --git a/app/kubernetes/helpers/resourceQuotaHelper.js b/app/kubernetes/helpers/resourceQuotaHelper.js index add667dd7..46d4cf033 100644 --- a/app/kubernetes/helpers/resourceQuotaHelper.js +++ b/app/kubernetes/helpers/resourceQuotaHelper.js @@ -4,6 +4,28 @@ class KubernetesResourceQuotaHelper { static generateResourceQuotaName(name) { return KubernetesPortainerResourceQuotaPrefix + name; } + + static formatBytes(bytes, decimals = 0, base10 = true) { + const res = { + Size: 0, + SizeUnit: 'B', + }; + + if (bytes === 0) { + return res; + } + + const k = base10 ? 1000 : 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return { + Size: parseFloat((bytes / Math.pow(k, i)).toFixed(dm)), + SizeUnit: sizes[i], + }; + } } export default KubernetesResourceQuotaHelper; diff --git a/app/kubernetes/helpers/resourceReservationHelper.js b/app/kubernetes/helpers/resourceReservationHelper.js index 5df674fc5..3a24e62be 100644 --- a/app/kubernetes/helpers/resourceReservationHelper.js +++ b/app/kubernetes/helpers/resourceReservationHelper.js @@ -26,7 +26,7 @@ class KubernetesResourceReservationHelper { } static parseCPU(cpu) { - let res = parseInt(cpu); + let res = parseInt(cpu, 10); if (_.endsWith(cpu, 'm')) { res /= 1000; } diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index 966a61ca6..c1e9915a6 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; @@ -80,18 +80,18 @@ export class KubernetesIngressConverter { } res.Annotations[KubernetesIngressClassAnnotation] = formValues.IngressClass.Name; res.Host = formValues.Host; + res.Paths = formValues.Paths; return res; } /** * * @param {KubernetesIngressClass} ics Ingress classes (saved in Portainer DB) - * @param {KubernetesIngress} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and passed for RP EDIT VIEW + * @param {KubernetesIngress[]} ingresses Existing Kubernetes ingresses. Must be empty for RP CREATE VIEW and filled for RP EDIT VIEW */ static ingressClassesToFormValues(ics, ingresses) { const res = _.map(ics, (ic) => { - const fv = new KubernetesResourcePoolIngressClassFormValue(); - fv.IngressClass = ic; + const fv = new KubernetesResourcePoolIngressClassFormValue(ic); const ingress = _.find(ingresses, { Name: ic.Name }); if (ingress) { fv.Selected = true; @@ -110,6 +110,7 @@ export class KubernetesIngressConverter { }); fv.Annotations = _.without(annotations, undefined); fv.AdvancedConfig = fv.Annotations.length > 0; + fv.Paths = ingress.Paths; } return fv; }); diff --git a/app/kubernetes/ingress/helper.js b/app/kubernetes/ingress/helper.js index b40178733..0c3e58cf2 100644 --- a/app/kubernetes/ingress/helper.js +++ b/app/kubernetes/ingress/helper.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; export class KubernetesIngressHelper { static findSBoundServiceIngressesRules(ingresses, serviceName) { diff --git a/app/kubernetes/ingress/service.js b/app/kubernetes/ingress/service.js index d5c97d527..01fc26b44 100644 --- a/app/kubernetes/ingress/service.js +++ b/app/kubernetes/ingress/service.js @@ -1,30 +1,23 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; import { KubernetesIngressConverter } from './converter'; -class KubernetesIngressService { - /* @ngInject */ - constructor($async, KubernetesIngresses) { - this.$async = $async; - this.KubernetesIngresses = KubernetesIngresses; +/* @ngInject */ +export function KubernetesIngressService($async, KubernetesIngresses) { + return { + get, + create, + patch, + delete: _delete, + }; - 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.deleteAsync = this.deleteAsync.bind(this); - } - - /** - * GET - */ - async getAsync(namespace, name) { + async function getOne(namespace, name) { try { const params = new KubernetesCommonParams(); params.id = name; - const [raw, yaml] = await Promise.all([this.KubernetesIngresses(namespace).get(params).$promise, this.KubernetesIngresses(namespace).getYaml(params).$promise]); + const [raw, yaml] = await Promise.all([KubernetesIngresses(namespace).get(params).$promise, KubernetesIngresses(namespace).getYaml(params).$promise]); const res = { Raw: KubernetesIngressConverter.apiToModel(raw), Yaml: yaml.data, @@ -35,9 +28,9 @@ class KubernetesIngressService { } } - async getAllAsync(namespace) { + async function getAll(namespace) { try { - const data = await this.KubernetesIngresses(namespace).get().$promise; + const data = await KubernetesIngresses(namespace).get().$promise; const res = _.reduce(data.items, (arr, item) => _.concat(arr, KubernetesIngressConverter.apiToModel(item)), []); return res; } catch (err) { @@ -45,73 +38,57 @@ class KubernetesIngressService { } } - get(namespace, name) { + function get(namespace, name) { if (name) { - return this.$async(this.getAsync, namespace, name); + return $async(getOne, namespace, name); } - return this.$async(this.getAllAsync, namespace); + return $async(getAll, namespace); } - /** - * CREATE - */ - async createAsync(ingress) { - try { - const params = {}; - const payload = KubernetesIngressConverter.createPayload(ingress); - const namespace = payload.metadata.namespace; - const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise; - return data; - } catch (err) { - throw new PortainerError('Unable to create ingress', err); - } - } - - create(ingress) { - return this.$async(this.createAsync, ingress); - } - - /** - * PATCH - */ - async patchAsync(oldIngress, newIngress) { - try { - const params = new KubernetesCommonParams(); - params.id = newIngress.Name; - const namespace = newIngress.Namespace; - const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress); - if (!payload.length) { - return; + function create(ingress) { + return $async(async () => { + try { + const params = {}; + const payload = KubernetesIngressConverter.createPayload(ingress); + const namespace = payload.metadata.namespace; + const data = await KubernetesIngresses(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create ingress', err); } - const data = await this.KubernetesIngresses(namespace).patch(params, payload).$promise; - return data; - } catch (err) { - throw new PortainerError('Unable to patch ingress', err); - } + }); } - patch(oldIngress, newIngress) { - return this.$async(this.patchAsync, oldIngress, newIngress); + function patch(oldIngress, newIngress) { + return $async(async () => { + try { + const params = new KubernetesCommonParams(); + params.id = newIngress.Name; + const namespace = newIngress.Namespace; + const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress); + if (!payload.length) { + return; + } + const data = await KubernetesIngresses(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch ingress', err); + } + }); } - /** - * DELETE - */ - async deleteAsync(ingress) { - try { - const params = new KubernetesCommonParams(); - params.id = ingress.IngressClass.Name; - const namespace = ingress.Namespace; - await this.KubernetesIngresses(namespace).delete(params).$promise; - } catch (err) { - throw new PortainerError('Unable to delete ingress', err); - } - } - - delete(ingress) { - return this.$async(this.deleteAsync, ingress); + function _delete(ingress) { + return $async(async () => { + try { + const params = new KubernetesCommonParams(); + params.id = ingress.Name; + const namespace = ingress.Namespace; + await KubernetesIngresses(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete ingress', err); + } + }); } } -export default KubernetesIngressService; angular.module('portainer.kubernetes').service('KubernetesIngressService', KubernetesIngressService); diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 53cb670c2..a5ed94391 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -153,9 +153,9 @@ export class KubernetesApplicationAutoScalerFormValue { } } -export function KubernetesFormValueDuplicate() { +export function KubernetesFormValidationReferences() { return { refs: {}, - hasDuplicates: false, + hasRefs: false, }; } diff --git a/app/kubernetes/models/namespace/models.js b/app/kubernetes/models/namespace/models.js index d4038141a..ca19450c8 100644 --- a/app/kubernetes/models/namespace/models.js +++ b/app/kubernetes/models/namespace/models.js @@ -1,18 +1,11 @@ -/** - * KubernetesNamespace Model - */ -const _KubernetesNamespace = Object.freeze({ - Id: '', - Name: '', - CreationDate: '', - Status: '', - Yaml: '', - ResourcePoolName: '', - ResourcePoolOwner: '', -}); - -export class KubernetesNamespace { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespace))); - } +export function KubernetesNamespace() { + return { + Id: '', + Name: '', + CreationDate: '', + Status: '', + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', + }; } diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index 9d2277f5d..4238086d6 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -1,8 +1,9 @@ export function KubernetesResourcePoolFormValues(defaults) { return { + Name: '', MemoryLimit: defaults.MemoryLimit, CpuLimit: defaults.CpuLimit, - HasQuota: true, + HasQuota: false, IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue }; } @@ -20,6 +21,7 @@ export function KubernetesResourcePoolIngressClassFormValue(ingressClass) { Selected: false, WasSelected: false, AdvancedConfig: false, + Paths: [], // will be filled to save IngressClass.Paths inside ingressClassesToFormValues() on RP EDIT }; } diff --git a/app/kubernetes/models/resource-quota/models.js b/app/kubernetes/models/resource-quota/models.js index 63190b760..8501b6495 100644 --- a/app/kubernetes/models/resource-quota/models.js +++ b/app/kubernetes/models/resource-quota/models.js @@ -1,34 +1,27 @@ import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; export const KubernetesPortainerResourceQuotaPrefix = 'portainer-rq-'; +export const KubernetesPortainerResourceQuotaCPULimit = 'limits.cpu'; +export const KubernetesPortainerResourceQuotaMemoryLimit = 'limits.memory'; +export const KubernetesPortainerResourceQuotaCPURequest = 'requests.cpu'; +export const KubernetesPortainerResourceQuotaMemoryRequest = 'requests.memory'; export const KubernetesResourceQuotaDefaults = { CpuLimit: 0, MemoryLimit: 0, }; -/** - * KubernetesResourceQuota Model - */ -const _KubernetesResourceQuota = Object.freeze({ - Id: '', - Namespace: '', - Name: '', - CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit, - MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit, - CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit, - MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit, - Yaml: '', - ResourcePoolName: '', - ResourcePoolOwner: '', -}); - -export class KubernetesResourceQuota { - constructor(namespace) { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuota))); - if (namespace) { - this.Name = KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace); - this.Namespace = namespace; - } - } +export function KubernetesResourceQuota(namespace) { + return { + Id: '', + Namespace: namespace ? namespace : '', + Name: namespace ? KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace) : '', + CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit, + CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit, + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', + }; } diff --git a/app/kubernetes/models/resource-quota/payloads.js b/app/kubernetes/models/resource-quota/payloads.js index 9a04dbf6c..5c4a378a0 100644 --- a/app/kubernetes/models/resource-quota/payloads.js +++ b/app/kubernetes/models/resource-quota/payloads.js @@ -1,43 +1,21 @@ import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; +import { + KubernetesPortainerResourceQuotaCPURequest, + KubernetesPortainerResourceQuotaMemoryRequest, + KubernetesPortainerResourceQuotaCPULimit, + KubernetesPortainerResourceQuotaMemoryLimit, +} from './models'; -/** - * KubernetesResourceQuotaCreatePayload Model - */ -const _KubernetesResourceQuotaCreatePayload = Object.freeze({ - metadata: new KubernetesCommonMetadataPayload(), - spec: { - hard: { - 'requests.cpu': 0, - 'requests.memory': 0, - 'limits.cpu': 0, - 'limits.memory': 0, +export function KubernetesResourceQuotaCreatePayload() { + return { + metadata: new KubernetesCommonMetadataPayload(), + spec: { + hard: { + [KubernetesPortainerResourceQuotaCPURequest]: 0, + [KubernetesPortainerResourceQuotaMemoryRequest]: 0, + [KubernetesPortainerResourceQuotaCPULimit]: 0, + [KubernetesPortainerResourceQuotaMemoryLimit]: 0, + }, }, - }, -}); - -export class KubernetesResourceQuotaCreatePayload { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaCreatePayload))); - } -} - -/** - * KubernetesResourceQuotaUpdatePayload Model - */ -const _KubernetesResourceQuotaUpdatePayload = Object.freeze({ - metadata: new KubernetesCommonMetadataPayload(), - spec: { - hard: { - 'requests.cpu': 0, - 'requests.memory': 0, - 'limits.cpu': 0, - 'limits.memory': 0, - }, - }, -}); - -export class KubernetesResourceQuotaUpdatePayload { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaUpdatePayload))); - } + }; } diff --git a/app/kubernetes/rest/resourceQuota.js b/app/kubernetes/rest/resourceQuota.js index a2bf9e8c2..d578e102f 100644 --- a/app/kubernetes/rest/resourceQuota.js +++ b/app/kubernetes/rest/resourceQuota.js @@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [ }, create: { method: 'POST' }, update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, delete: { method: 'DELETE' }, } ); diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index c204e918f..0452d829d 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import angular from 'angular'; import PortainerError from 'Portainer/error'; diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index a1b1029aa..f492c6ef4 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -1,35 +1,22 @@ -import * as _ from 'lodash-es'; -import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; +import _ from 'lodash-es'; import angular from 'angular'; import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'; import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; -import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; -import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; -import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; -import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; -class KubernetesResourcePoolService { - /* @ngInject */ - constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { - this.$async = $async; - this.KubernetesNamespaceService = KubernetesNamespaceService; - this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; - this.KubernetesIngressService = KubernetesIngressService; +/* @ngInject */ +export function KubernetesResourcePoolService($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { + return { + get, + create, + patch, + delete: _delete, + }; - this.getAsync = this.getAsync.bind(this); - this.getAllAsync = this.getAllAsync.bind(this); - this.createAsync = this.createAsync.bind(this); - this.deleteAsync = this.deleteAsync.bind(this); - } - - /** - * GET - */ - async getAsync(name) { + async function getOne(name) { try { - const namespace = await this.KubernetesNamespaceService.get(name); - const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const namespace = await KubernetesNamespaceService.get(name); + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); if (quotaAttempt.status === 'fulfilled') { pool.Quota = quotaAttempt.value; @@ -41,13 +28,13 @@ class KubernetesResourcePoolService { } } - async getAllAsync() { + async function getAll() { try { - const namespaces = await this.KubernetesNamespaceService.get(); + const namespaces = await KubernetesNamespaceService.get(); const pools = await Promise.all( _.map(namespaces, async (namespace) => { const name = namespace.Name; - const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); if (quotaAttempt.status === 'fulfilled') { pool.Quota = quotaAttempt.value; @@ -62,66 +49,75 @@ class KubernetesResourcePoolService { } } - get(name) { + function get(name) { if (name) { - return this.$async(this.getAsync, name); + return $async(getOne, name); } - return this.$async(this.getAllAsync); + return $async(getAll); } - /** - * CREATE - * @param {KubernetesResourcePoolFormValues} formValues - */ - async createAsync(formValues) { - formValues.Owner = KubernetesCommonHelper.ownerToLabel(formValues.Owner); + function create(formValues) { + return $async(async () => { + try { + const [namespace, quota, ingresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(formValues); + await KubernetesNamespaceService.create(namespace); - try { - const namespace = new KubernetesNamespace(); - namespace.Name = formValues.Name; - namespace.ResourcePoolName = formValues.Name; - namespace.ResourcePoolOwner = formValues.Owner; - await this.KubernetesNamespaceService.create(namespace); - if (formValues.HasQuota) { - const quota = new KubernetesResourceQuota(formValues.Name); - quota.CpuLimit = formValues.CpuLimit; - quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); - quota.ResourcePoolName = formValues.Name; - quota.ResourcePoolOwner = formValues.Owner; - await this.KubernetesResourceQuotaService.create(quota); - } - const ingressPromises = _.map(formValues.IngressClasses, (c) => { - if (c.Selected) { - c.Namespace = namespace.Name; - const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); - return this.KubernetesIngressService.create(ingress); + if (quota) { + await KubernetesResourceQuotaService.create(quota); } - }); - await Promise.all(ingressPromises); - } catch (err) { - throw err; - } + const ingressPromises = _.map(ingresses, (i) => KubernetesIngressService.create(i)); + await Promise.all(ingressPromises); + } catch (err) { + throw err; + } + }); } - create(formValues) { - return this.$async(this.createAsync, formValues); + function patch(oldFormValues, newFormValues) { + return $async(async () => { + try { + const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues); + const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues); + void oldNamespace, newNamespace; + + if (oldQuota && newQuota) { + await KubernetesResourceQuotaService.patch(oldQuota, newQuota); + } else if (!oldQuota && newQuota) { + await KubernetesResourceQuotaService.create(newQuota); + } else if (oldQuota && !newQuota) { + await KubernetesResourceQuotaService.delete(oldQuota); + } + + const create = _.filter(newIngresses, (ing) => !_.find(oldIngresses, { Name: ing.Name })); + const del = _.filter(oldIngresses, (ing) => !_.find(newIngresses, { Name: ing.Name })); + const patch = _.without(newIngresses, ...create); + + const createPromises = _.map(create, (i) => KubernetesIngressService.create(i)); + const delPromises = _.map(del, (i) => KubernetesIngressService.delete(i)); + const patchPromises = _.map(patch, (ing) => { + const old = _.find(oldIngresses, { Name: ing.Name }); + ing.Paths = angular.copy(old.Paths); + ing.PreviousHost = old.Host; + return KubernetesIngressService.patch(old, ing); + }); + + const promises = _.flatten([createPromises, delPromises, patchPromises]); + await Promise.all(promises); + } catch (err) { + throw err; + } + }); } - /** - * DELETE - */ - async deleteAsync(pool) { - try { - await this.KubernetesNamespaceService.delete(pool.Namespace); - } catch (err) { - throw err; - } - } - - delete(pool) { - return this.$async(this.deleteAsync, pool); + function _delete(pool) { + return $async(async () => { + try { + await KubernetesNamespaceService.delete(pool.Namespace); + } catch (err) { + throw err; + } + }); } } -export default KubernetesResourcePoolService; angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService); diff --git a/app/kubernetes/services/resourceQuotaService.js b/app/kubernetes/services/resourceQuotaService.js index 0a41da03f..2622a428f 100644 --- a/app/kubernetes/services/resourceQuotaService.js +++ b/app/kubernetes/services/resourceQuotaService.js @@ -5,105 +5,85 @@ import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota'; -class KubernetesResourceQuotaService { - /* @ngInject */ - constructor($async, KubernetesResourceQuotas) { - this.$async = $async; - this.KubernetesResourceQuotas = KubernetesResourceQuotas; +/* @ngInject */ +export function KubernetesResourceQuotaService($async, KubernetesResourceQuotas) { + return { + get, + create, + patch, + delete: _delete, + }; - this.getAsync = this.getAsync.bind(this); - this.getAllAsync = this.getAllAsync.bind(this); - this.createAsync = this.createAsync.bind(this); - this.updateAsync = this.updateAsync.bind(this); - this.deleteAsync = this.deleteAsync.bind(this); - } - - /** - * GET - */ - async getAsync(namespace, name) { + async function getOne(namespace, name) { try { const params = new KubernetesCommonParams(); params.id = name; - const [raw, yaml] = await Promise.all([this.KubernetesResourceQuotas(namespace).get(params).$promise, this.KubernetesResourceQuotas(namespace).getYaml(params).$promise]); + const [raw, yaml] = await Promise.all([KubernetesResourceQuotas(namespace).get(params).$promise, KubernetesResourceQuotas(namespace).getYaml(params).$promise]); return KubernetesResourceQuotaConverter.apiToResourceQuota(raw, yaml); } catch (err) { throw new PortainerError('Unable to retrieve resource quota', err); } } - async getAllAsync(namespace) { + async function getAll(namespace) { try { - const data = await this.KubernetesResourceQuotas(namespace).get().$promise; + const data = await KubernetesResourceQuotas(namespace).get().$promise; return _.map(data.items, (item) => KubernetesResourceQuotaConverter.apiToResourceQuota(item)); } catch (err) { throw new PortainerError('Unable to retrieve resource quotas', err); } } - get(namespace, name) { + function get(namespace, name) { if (name) { - return this.$async(this.getAsync, namespace, name); + return $async(getOne, namespace, name); } - return this.$async(this.getAllAsync, namespace); + return $async(getAll, namespace); } - /** - * CREATE - */ - async createAsync(quota) { - try { - const payload = KubernetesResourceQuotaConverter.createPayload(quota); - const namespace = payload.metadata.namespace; - const params = {}; - const data = await this.KubernetesResourceQuotas(namespace).create(params, payload).$promise; - return KubernetesResourceQuotaConverter.apiToResourceQuota(data); - } catch (err) { - throw new PortainerError('Unable to create quota', err); - } + function create(quota) { + return $async(async () => { + try { + const payload = KubernetesResourceQuotaConverter.createPayload(quota); + const namespace = payload.metadata.namespace; + const params = {}; + const data = await KubernetesResourceQuotas(namespace).create(params, payload).$promise; + return KubernetesResourceQuotaConverter.apiToResourceQuota(data); + } catch (err) { + throw new PortainerError('Unable to create quota', err); + } + }); } - create(quota) { - return this.$async(this.createAsync, quota); + function patch(oldQuota, newQuota) { + return $async(async () => { + try { + const params = new KubernetesCommonParams(); + params.id = newQuota.Name; + const namespace = newQuota.Namespace; + const payload = KubernetesResourceQuotaConverter.patchPayload(oldQuota, newQuota); + if (!payload.length) { + return; + } + const data = await KubernetesResourceQuotas(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to update resource quota', err); + } + }); } - /** - * UPDATE - */ - async updateAsync(quota) { - try { - const payload = KubernetesResourceQuotaConverter.updatePayload(quota); - const params = new KubernetesCommonParams(); - params.id = payload.metadata.name; - const namespace = payload.metadata.namespace; - const data = await this.KubernetesResourceQuotas(namespace).update(params, payload).$promise; - return data; - } catch (err) { - throw new PortainerError('Unable to update resource quota', err); - } - } - - update(quota) { - return this.$async(this.updateAsync, quota); - } - - /** - * DELETE - */ - async deleteAsync(quota) { - try { - const params = new KubernetesCommonParams(); - params.id = quota.Name; - await this.KubernetesResourceQuotas(quota.Namespace).delete(params).$promise; - } catch (err) { - throw new PortainerError('Unable to delete quota', err); - } - } - - delete(quota) { - return this.$async(this.deleteAsync, quota); + function _delete(quota) { + return $async(async () => { + try { + const params = new KubernetesCommonParams(); + params.id = quota.Name; + await KubernetesResourceQuotas(quota.Namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete quota', err); + } + }); } } -export default KubernetesResourceQuotaService; angular.module('portainer.kubernetes').service('KubernetesResourceQuotaService', KubernetesResourceQuotaService); diff --git a/app/kubernetes/services/volumeService.js b/app/kubernetes/services/volumeService.js index b65b4cb93..abbf0019d 100644 --- a/app/kubernetes/services/volumeService.js +++ b/app/kubernetes/services/volumeService.js @@ -21,7 +21,7 @@ class KubernetesVolumeService { */ async getAsync(namespace, name) { try { - const [pvc, pool] = await Promise.all([await this.KubernetesPersistentVolumeClaimService.get(namespace, name), await this.KubernetesResourcePoolService.get(namespace)]); + const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]); return KubernetesVolumeConverter.pvcToVolume(pvc, pool); } catch (err) { throw err; @@ -30,7 +30,8 @@ class KubernetesVolumeService { async getAllAsync(namespace) { try { - const pools = await this.KubernetesResourcePoolService.get(namespace); + const data = await this.KubernetesResourcePoolService.get(namespace); + const pools = data instanceof Array ? data : [data]; const res = await Promise.all( _.map(pools, async (pool) => { const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 8a7dfec4b..d7896d42d 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -1,7 +1,7 @@ require('../../templates/advancedDeploymentPanel.html'); import angular from 'angular'; -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 0b5a931fb..1618a9ca9 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -143,60 +143,72 @@
-
-
- name +
+
+
+ name + +
+
+ +
+ value
-
- -

Environment variable name is required.

-

This field must consist alphanumeric characters, '-' or '_', start with an alphabetic - character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').

-
-

This environment variable is already defined.

+ +
+ +
- -
- value - -
- -
- - +
+
+
+ +

Environment variable name is required.

+

This field must consist alphanumeric characters, '-' or '_', start with an alphabetic + character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').

+
+

This environment variable is already defined.

+
+
+
+
@@ -272,54 +284,65 @@
-
-
- configuration key - +
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +
+ + +
-
- path on disk - -
-
- -

Path is required.

-
-

This path is already used.

+
+
+
+ +

Path is required.

+
+

This path is already used.

+
-
- -
- - +
@@ -342,12 +365,7 @@
- + add persisted folder
@@ -383,7 +401,7 @@ ng-model="persistedFolder.UseNewVolume" uib-btn-radio="true" ng-change="ctrl.useNewVolume($index)" - ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)" + ng-disabled="ctrl.isNewVolumeButtonDisabled($index)" >New volume @@ -419,12 +437,7 @@
-
+
storage
@@ -786,7 +798,7 @@ style="margin-left: 20px;" ng-model="ctrl.formValues.ReplicaCount" ng-disabled="!ctrl.supportScalableReplicaDeployment()" - ng-change="ctrl.onChangeVolumeRequestedSize()" + ng-change="ctrl.enforceReplicaCountMinimum()" required />
@@ -807,7 +819,8 @@ >
- This application will reserve the following resources: {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + 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.
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 9de41e5ec..de55e32e5 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import * as JsonPatch from 'fast-json-patch'; @@ -20,7 +20,7 @@ import { KubernetesApplicationPersistedFolderFormValue, KubernetesApplicationPublishedPortFormValue, KubernetesApplicationPlacementFormValue, - KubernetesFormValueDuplicate, + KubernetesFormValidationReferences, } from 'Kubernetes/models/application/formValues'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import KubernetesApplicationConverter from 'Kubernetes/converters/application'; @@ -75,15 +75,8 @@ class KubernetesCreateApplicationController { this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes; this.ServiceTypes = KubernetesServiceTypes; - this.onInit = this.onInit.bind(this); this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.deployApplicationAsync = this.deployApplicationAsync.bind(this); - this.updateSlidersAsync = this.updateSlidersAsync.bind(this); - this.refreshStacksAsync = this.refreshStacksAsync.bind(this); - this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this); - this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this); - this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this); - this.getApplicationAsync = this.getApplicationAsync.bind(this); } /* #endregion */ @@ -92,7 +85,7 @@ class KubernetesCreateApplicationController { this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication); } - /* #region AUTO SCLAER UI MANAGEMENT */ + /* #region AUTO SCALER UI MANAGEMENT */ unselectAutoScaler() { if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) { this.formValues.AutoScaler.IsUsed = false; @@ -156,7 +149,7 @@ class KubernetesCreateApplicationController { }); }); - this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0; + this.state.duplicates.configurationPaths.hasRefs = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0; } /* #endregion */ @@ -184,7 +177,7 @@ class KubernetesCreateApplicationController { onChangeEnvironmentName() { this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name')); - this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0; + this.state.duplicates.environmentVariables.hasRefs = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0; } /* #endregion */ @@ -195,12 +188,14 @@ class KubernetesCreateApplicationController { storageClass = this.storageClasses[0]; } - this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass)); + const newPf = new KubernetesApplicationPersistedFolderFormValue(storageClass); + this.formValues.PersistedFolders.push(newPf); this.resetDeploymentType(); } restorePersistedFolder(index) { this.formValues.PersistedFolders[index].NeedsDeletion = false; + this.validatePersistedFolders(); } resetPersistedFolders() { @@ -208,6 +203,7 @@ class KubernetesCreateApplicationController { persistedFolder.ExistingVolume = null; persistedFolder.UseNewVolume = true; }); + this.validatePersistedFolders(); } removePersistedFolder(index) { @@ -216,6 +212,29 @@ class KubernetesCreateApplicationController { } else { this.formValues.PersistedFolders.splice(index, 1); } + this.validatePersistedFolders(); + } + + useNewVolume(index) { + this.formValues.PersistedFolders[index].UseNewVolume = true; + this.formValues.PersistedFolders[index].ExistingVolume = null; + this.state.persistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true); + this.validatePersistedFolders(); + } + + useExistingVolume(index) { + this.formValues.PersistedFolders[index].UseNewVolume = false; + this.state.persistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false; + if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) { + this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED; + this.resetDeploymentType(); + } + this.validatePersistedFolders(); + } + /* #endregion */ + + /* #region PERSISTENT FOLDERS ON CHANGE VALIDATION */ + validatePersistedFolders() { this.onChangePersistedFolderPath(); this.onChangeExistingVolumeSelection(); } @@ -229,31 +248,19 @@ class KubernetesCreateApplicationController { return persistedFolder.ContainerPath; }) ); - this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0; + this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0; } onChangeExistingVolumeSelection() { this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates( _.map(this.formValues.PersistedFolders, (persistedFolder) => { + if (persistedFolder.NeedsDeletion) { + return undefined; + } return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : ''; }) ); - this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0; - } - - useNewVolume(index) { - this.formValues.PersistedFolders[index].UseNewVolume = true; - this.formValues.PersistedFolders[index].ExistingVolume = null; - this.state.PersistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true); - } - - useExistingVolume(index) { - this.formValues.PersistedFolders[index].UseNewVolume = false; - this.state.PersistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false; - if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) { - this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED; - this.resetDeploymentType(); - } + this.state.duplicates.existingVolumes.hasRefs = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0; } /* #endregion */ @@ -296,7 +303,7 @@ class KubernetesCreateApplicationController { const source = _.map(this.formValues.Placements, (p) => (p.NeedsDeletion ? undefined : p.Label.Key)); const duplicates = KubernetesFormValidationHelper.getDuplicates(source); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } /* #endregion */ @@ -351,10 +358,10 @@ class KubernetesCreateApplicationController { const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol)); const duplicates = KubernetesFormValidationHelper.getDuplicates(source); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } else { state.refs = {}; - state.hasDuplicates = false; + state.hasRefs = false; } } @@ -364,10 +371,10 @@ class KubernetesCreateApplicationController { const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort)); const duplicates = KubernetesFormValidationHelper.getDuplicates(source); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } else { state.refs = {}; - state.hasDuplicates = false; + state.hasRefs = false; } } @@ -382,9 +389,9 @@ class KubernetesCreateApplicationController { const state = this.state.duplicates.publishedPorts.ingressRoutes; if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { - const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); - const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined)); - const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path)); + const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined)); + const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined)); + const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => `${p.Host || i.Name}${p.Path}`)); const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes); _.forEach(newRoutes, (route, idx) => { if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) { @@ -392,10 +399,10 @@ class KubernetesCreateApplicationController { } }); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } else { state.refs = {}; - state.hasDuplicates = false; + state.hasRefs = false; } } @@ -405,10 +412,10 @@ class KubernetesCreateApplicationController { const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort)); const duplicates = KubernetesFormValidationHelper.getDuplicates(source); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } else { state.refs = {}; - state.hasDuplicates = false; + state.hasRefs = false; } } @@ -428,14 +435,14 @@ class KubernetesCreateApplicationController { isValid() { return ( !this.state.alreadyExists && - !this.state.duplicates.environmentVariables.hasDuplicates && - !this.state.duplicates.persistedFolders.hasDuplicates && - !this.state.duplicates.configurationPaths.hasDuplicates && - !this.state.duplicates.existingVolumes.hasDuplicates && - !this.state.duplicates.publishedPorts.containerPorts.hasDuplicates && - !this.state.duplicates.publishedPorts.nodePorts.hasDuplicates && - !this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates && - !this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates + !this.state.duplicates.environmentVariables.hasRefs && + !this.state.duplicates.persistedFolders.hasRefs && + !this.state.duplicates.configurationPaths.hasRefs && + !this.state.duplicates.existingVolumes.hasRefs && + !this.state.duplicates.publishedPorts.containerPorts.hasRefs && + !this.state.duplicates.publishedPorts.nodePorts.hasRefs && + !this.state.duplicates.publishedPorts.ingressRoutes.hasRefs && + !this.state.duplicates.publishedPorts.loadBalancerPorts.hasRefs ); } @@ -501,12 +508,20 @@ class KubernetesCreateApplicationController { if (folder.StorageClass && _.isEqual(folder.StorageClass.AccessModes, ['RWO'])) { storageOptions.push(folder.StorageClass.Name); + } else { + storageOptions.push(''); } } return _.uniq(storageOptions).join(', '); } + enforceReplicaCountMinimum() { + if (this.formValues.ReplicaCount === null) { + this.formValues.ReplicaCount = 1; + } + } + resourceQuotaCapacityExceeded() { return !this.state.sliders.memory.max || !this.state.sliders.cpu.max; } @@ -562,9 +577,29 @@ class KubernetesCreateApplicationController { return !this.editChanges.length; } + /* #region PERSISTED FOLDERS */ + /* #region BUTTONS STATES */ + isAddPersistentFolderButtonShowed() { + return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1; + } + + isNewVolumeButtonDisabled(index) { + return this.isEditAndExistingPersistedFolder(index); + } + + isExistingVolumeButtonDisabled() { + return !this.hasAvailableVolumes() || (this.isEdit && this.application.ApplicationType === this.ApplicationTypes.STATEFULSET); + } + /* #endregion */ + + hasAvailableVolumes() { + return this.availableVolumes.length > 0; + } + isEditAndExistingPersistedFolder(index) { return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName; } + /* #endregion */ isEditAndNotNewPublishedPort(index) { return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew; @@ -639,126 +674,126 @@ class KubernetesCreateApplicationController { /* #endregion */ /* #region DATA AUTO REFRESH */ - async updateSlidersAsync() { - try { - const quota = this.formValues.ResourcePool.Quota; - let minCpu, - maxCpu, - minMemory, - maxMemory = 0; - if (quota) { + updateSliders() { + this.state.resourcePoolHasQuota = false; + + const quota = this.formValues.ResourcePool.Quota; + let minCpu, + maxCpu, + minMemory, + maxMemory = 0; + if (quota) { + if (quota.CpuLimit) { this.state.resourcePoolHasQuota = true; - if (quota.CpuLimit) { - minCpu = KubernetesApplicationQuotaDefaults.CpuLimit; - maxCpu = quota.CpuLimit - quota.CpuLimitUsed; - if (this.state.isEdit && this.savedFormValues.CpuLimit) { - maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount; - } - } else { - minCpu = 0; - maxCpu = this.state.nodes.cpu; - } - if (quota.MemoryLimit) { - minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit; - maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed; - if (this.state.isEdit && this.savedFormValues.MemoryLimit) { - maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount; - } - } else { - minMemory = 0; - maxMemory = this.state.nodes.memory; + minCpu = KubernetesApplicationQuotaDefaults.CpuLimit; + maxCpu = quota.CpuLimit - quota.CpuLimitUsed; + if (this.state.isEdit && this.savedFormValues.CpuLimit) { + maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount; } } else { - this.state.resourcePoolHasQuota = false; minCpu = 0; maxCpu = this.state.nodes.cpu; + } + if (quota.MemoryLimit) { + this.state.resourcePoolHasQuota = true; + minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit; + maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed; + if (this.state.isEdit && this.savedFormValues.MemoryLimit) { + maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount; + } + } else { minMemory = 0; maxMemory = this.state.nodes.memory; } - this.state.sliders.memory.min = minMemory; - this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory); - this.state.sliders.cpu.min = minCpu; - this.state.sliders.cpu.max = _.round(maxCpu, 2); - if (!this.state.isEdit) { - this.formValues.CpuLimit = minCpu; - this.formValues.MemoryLimit = minMemory; - } - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to update resources selector'); + } else { + minCpu = 0; + maxCpu = this.state.nodes.cpu; + minMemory = 0; + maxMemory = this.state.nodes.memory; } - } - - updateSliders() { - return this.$async(this.updateSlidersAsync); - } - - async refreshStacksAsync(namespace) { - try { - this.stacks = await this.KubernetesStackService.get(namespace); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve stacks'); + this.state.sliders.memory.min = minMemory; + this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory); + this.state.sliders.cpu.min = minCpu; + this.state.sliders.cpu.max = _.round(maxCpu, 2); + if (!this.state.isEdit) { + this.formValues.CpuLimit = minCpu; + this.formValues.MemoryLimit = minMemory; } } refreshStacks(namespace) { - return this.$async(this.refreshStacksAsync, namespace); - } - - async refreshConfigurationsAsync(namespace) { - try { - this.configurations = await this.KubernetesConfigurationService.get(namespace); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); - } + return this.$async(async () => { + try { + this.stacks = await this.KubernetesStackService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve stacks'); + } + }); } refreshConfigurations(namespace) { - return this.$async(this.refreshConfigurationsAsync, namespace); - } - - async refreshApplicationsAsync(namespace) { - try { - this.applications = await this.KubernetesApplicationService.get(namespace); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve applications'); - } + return this.$async(async () => { + try { + this.configurations = await this.KubernetesConfigurationService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); + } + }); } refreshApplications(namespace) { - return this.$async(this.refreshApplicationsAsync, namespace); + return this.$async(async () => { + try { + this.applications = await this.KubernetesApplicationService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } + }); } refreshVolumes(namespace) { - const filteredVolumes = _.filter(this.volumes, (volume) => { - const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace; - const isUnused = !KubernetesVolumeHelper.isUsed(volume); - const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX'); - return isSameNamespace && (isUnused || isRWX); + return this.$async(async () => { + try { + const volumes = await this.KubernetesVolumeService.get(namespace); + _.forEach(volumes, (volume) => { + volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, this.applications); + }); + this.volumes = volumes; + const filteredVolumes = _.filter(this.volumes, (volume) => { + const isUnused = !KubernetesVolumeHelper.isUsed(volume); + const isRWX = volume.PersistentVolumeClaim.StorageClass && _.includes(volume.PersistentVolumeClaim.StorageClass.AccessModes, 'RWX'); + return isUnused || isRWX; + }); + this.availableVolumes = filteredVolumes; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve volumes'); + } }); - this.availableVolumes = filteredVolumes; } refreshIngresses(namespace) { this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace }); if (!this.publishViaIngressEnabled()) { - this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; + if (this.savedFormValues) { + this.formValues.PublishingType = this.savedFormValues.PublishingType; + } else { + this.formValues.PublishingType = this.ApplicationPublishingTypes.INTERNAL; + } } this.formValues.OriginalIngresses = this.filteredIngresses; } - async refreshNamespaceDataAsync(namespace) { - await Promise.all([ - this.refreshStacks(namespace), - this.refreshConfigurations(namespace), - this.refreshApplications(namespace), - this.refreshIngresses(namespace), - this.refreshVolumes(namespace), - ]); - this.onChangeName(); - } - refreshNamespaceData(namespace) { - return this.$async(this.refreshNamespaceDataAsync, namespace); + return this.$async(async () => { + await Promise.all([ + this.refreshStacks(namespace), + this.refreshConfigurations(namespace), + this.refreshApplications(namespace), + this.refreshIngresses(namespace), + this.refreshVolumes(namespace), + ]); + this.onChangeName(); + }); } resetFormValues() { @@ -768,10 +803,12 @@ class KubernetesCreateApplicationController { } onResourcePoolSelectionChange() { - const namespace = this.formValues.ResourcePool.Namespace.Name; - this.updateSliders(); - this.refreshNamespaceData(namespace); - this.resetFormValues(); + return this.$async(async () => { + const namespace = this.formValues.ResourcePool.Namespace.Name; + this.updateSliders(); + await this.refreshNamespaceData(namespace); + this.resetFormValues(); + }); } /* #endregion */ @@ -818,154 +855,143 @@ class KubernetesCreateApplicationController { /* #endregion */ /* #region APPLICATION - used on edit context only */ - async getApplicationAsync() { - try { - const namespace = this.state.params.namespace; - [this.application, this.persistentVolumeClaims] = await Promise.all([ - this.KubernetesApplicationService.get(namespace, this.state.params.name), - this.KubernetesPersistentVolumeClaimService.get(namespace), - ]); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve application details'); - } - } - getApplication() { - return this.$async(this.getApplicationAsync); + return this.$async(async () => { + try { + const namespace = this.state.params.namespace; + [this.application, this.persistentVolumeClaims] = await Promise.all([ + this.KubernetesApplicationService.get(namespace, this.state.params.name), + this.KubernetesPersistentVolumeClaimService.get(namespace), + ]); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application details'); + } + }); } /* #endregion */ /* #region ON INIT */ - async onInit() { - try { - this.state = { - actionInProgress: false, - useLoadBalancer: false, - useServerMetrics: false, - sliders: { - cpu: { - min: 0, - max: 0, - }, - memory: { - min: 0, - max: 0, - }, - }, - nodes: { - memory: 0, - cpu: 0, - }, - resourcePoolHasQuota: false, - viewReady: false, - availableSizeUnits: ['MB', 'GB', 'TB'], - alreadyExists: false, - duplicates: { - environmentVariables: new KubernetesFormValueDuplicate(), - persistedFolders: new KubernetesFormValueDuplicate(), - configurationPaths: new KubernetesFormValueDuplicate(), - existingVolumes: new KubernetesFormValueDuplicate(), - publishedPorts: { - containerPorts: new KubernetesFormValueDuplicate(), - nodePorts: new KubernetesFormValueDuplicate(), - ingressRoutes: new KubernetesFormValueDuplicate(), - loadBalancerPorts: new KubernetesFormValueDuplicate(), - }, - placements: new KubernetesFormValueDuplicate(), - }, - isEdit: false, - params: { - namespace: this.$transition$.params().namespace, - name: this.$transition$.params().name, - }, - PersistedFoldersUseExistingVolumes: false, - }; - - this.isAdmin = this.Authentication.isAdmin(); - - this.editChanges = []; - - if (this.$transition$.params().namespace && this.$transition$.params().name) { - this.state.isEdit = true; - } - - 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(); - - const [resourcePools, nodes, ingresses] = await Promise.all([ - this.KubernetesResourcePoolService.get(), - this.KubernetesNodeService.get(), - this.KubernetesIngressService.get(), - ]); - this.ingresses = ingresses; - - this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); - this.formValues.ResourcePool = this.resourcePools[0]; - - // TODO: refactor @Max - // Don't pull all volumes and applications across all namespaces - // Use refreshNamespaceData flow (triggered on Init + on Namespace change) - // and query only accross the selected namespace - if (this.storageClassAvailable()) { - const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]); - this.volumes = volumes; - _.forEach(this.volumes, (volume) => { - volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); - }); - } - - _.forEach(nodes, (item) => { - this.state.nodes.memory += filesizeParser(item.Memory); - this.state.nodes.cpu += item.CPU; - }); - this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes); - - const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name; - await this.refreshNamespaceData(namespace); - - if (this.state.isEdit) { - await this.getApplication(); - this.formValues = KubernetesApplicationConverter.applicationToFormValues( - this.application, - this.resourcePools, - this.configurations, - this.persistentVolumeClaims, - this.nodesLabels - ); - this.formValues.OriginalIngresses = this.filteredIngresses; - this.savedFormValues = angular.copy(this.formValues); - delete this.formValues.ApplicationType; - - if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) { - _.forEach(this.formValues.PersistedFolders, (persistedFolder) => { - const volume = _.find(this.availableVolumes, (vol) => vol.PersistentVolumeClaim.Name === persistedFolder.PersistentVolumeClaimName); - if (volume) { - persistedFolder.UseNewVolume = false; - persistedFolder.ExistingVolume = volume; - } - }); - } - } else { - this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount); - this.formValues.OriginalIngressClasses = angular.copy(this.ingresses); - } - - await this.updateSliders(); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to load view data'); - } finally { - this.state.viewReady = true; - } - } - $onInit() { - return this.$async(this.onInit); + return this.$async(async () => { + try { + this.state = { + actionInProgress: false, + useLoadBalancer: false, + useServerMetrics: false, + sliders: { + cpu: { + min: 0, + max: 0, + }, + memory: { + min: 0, + max: 0, + }, + }, + nodes: { + memory: 0, + cpu: 0, + }, + resourcePoolHasQuota: false, + viewReady: false, + availableSizeUnits: ['MB', 'GB', 'TB'], + alreadyExists: false, + duplicates: { + environmentVariables: new KubernetesFormValidationReferences(), + persistedFolders: new KubernetesFormValidationReferences(), + configurationPaths: new KubernetesFormValidationReferences(), + existingVolumes: new KubernetesFormValidationReferences(), + publishedPorts: { + containerPorts: new KubernetesFormValidationReferences(), + nodePorts: new KubernetesFormValidationReferences(), + ingressRoutes: new KubernetesFormValidationReferences(), + loadBalancerPorts: new KubernetesFormValidationReferences(), + }, + placements: new KubernetesFormValidationReferences(), + }, + isEdit: false, + params: { + namespace: this.$transition$.params().namespace, + name: this.$transition$.params().name, + }, + persistedFoldersUseExistingVolumes: false, + }; + + this.isAdmin = this.Authentication.isAdmin(); + + this.editChanges = []; + + if (this.state.params.namespace && this.state.params.name) { + this.state.isEdit = true; + } + + 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(); + + const [resourcePools, nodes, ingresses] = await Promise.all([ + this.KubernetesResourcePoolService.get(), + this.KubernetesNodeService.get(), + this.KubernetesIngressService.get(), + ]); + this.ingresses = ingresses; + + this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); + this.formValues.ResourcePool = this.resourcePools[0]; + if (!this.formValues.ResourcePool) { + return; + } + + _.forEach(nodes, (item) => { + this.state.nodes.memory += filesizeParser(item.Memory); + this.state.nodes.cpu += item.CPU; + }); + this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes); + + const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name; + await this.refreshNamespaceData(namespace); + + if (this.state.isEdit) { + await this.getApplication(); + this.formValues = KubernetesApplicationConverter.applicationToFormValues( + this.application, + this.resourcePools, + this.configurations, + this.persistentVolumeClaims, + this.nodesLabels + ); + this.formValues.OriginalIngresses = this.filteredIngresses; + this.savedFormValues = angular.copy(this.formValues); + delete this.formValues.ApplicationType; + + if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) { + _.forEach(this.formValues.PersistedFolders, (persistedFolder) => { + const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.PersistentVolumeClaimName]); + if (volume) { + persistedFolder.UseNewVolume = false; + persistedFolder.ExistingVolume = volume; + } + }); + } + await this.refreshNamespaceData(namespace); + } else { + this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount); + this.formValues.OriginalIngressClasses = angular.copy(this.ingresses); + } + + this.updateSliders(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + }); } + /* #endregion */ } diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index f12195edc..ac3f97442 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; @@ -67,8 +67,8 @@ function computeAffinities(nodes, application) { (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) || (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) || (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) || - (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key]) > parseInt(e.values[0])) || - (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key]) < parseInt(e.values[0])) + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key], 10) > parseInt(e.values[0], 10)) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key], 10) < parseInt(e.values[0], 10)) ) { return; } diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js b/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js index e35fbc74d..ea550309a 100644 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js @@ -1,4 +1,4 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); diff --git a/app/kubernetes/views/configurations/create/createConfiguration.html b/app/kubernetes/views/configurations/create/createConfiguration.html index c490f4fad..729713fcc 100644 --- a/app/kubernetes/views/configurations/create/createConfiguration.html +++ b/app/kubernetes/views/configurations/create/createConfiguration.html @@ -56,7 +56,6 @@ id="resource-pool-selector" ng-model="ctrl.formValues.ResourcePool" ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools" - ng-change="ctrl.onResourcePoolSelectionChange()" >
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index b4b296e99..8c53ce502 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -1,7 +1,7 @@ -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import angular from 'angular'; import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models'; -import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues'; +import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues'; import { KubernetesIngressClass } from 'Kubernetes/ingress/models'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; @@ -82,7 +82,7 @@ class KubernetesConfigureController { const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name)); const duplicates = KubernetesFormValidationHelper.getDuplicates(source); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } onChangeIngressClassName(index) { @@ -212,7 +212,7 @@ class KubernetesConfigureController { viewReady: false, endpointId: this.$stateParams.id, duplicates: { - ingressClasses: new KubernetesFormValueDuplicate(), + ingressClasses: new KubernetesFormValidationReferences(), }, }; diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js index 4f4e0163f..a72ec2db7 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -6,7 +6,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues'; import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; -import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues'; +import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues'; import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; class KubernetesCreateResourcePoolController { @@ -44,7 +44,7 @@ class KubernetesCreateResourcePoolController { } }); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } /* #region ANNOTATIONS MANAGEMENT */ @@ -58,7 +58,7 @@ class KubernetesCreateResourcePoolController { /* #endregion */ isCreateButtonDisabled() { - return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasDuplicates; + return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasRefs; } onChangeName() { @@ -109,13 +109,10 @@ class KubernetesCreateResourcePoolController { /* #region GET INGRESSES */ async getIngressesAsync() { - this.state.ingressesLoading = true; try { this.allIngresses = await this.KubernetesIngressService.get(); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.'); - } finally { - this.state.ingressesLoading = false; } } @@ -154,7 +151,7 @@ class KubernetesCreateResourcePoolController { isAlreadyExist: false, canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, duplicates: { - ingressHosts: new KubernetesFormValueDuplicate(), + ingressHosts: new KubernetesFormValidationReferences(), }, }; diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index 16a04f15e..41d210513 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -36,6 +36,17 @@

At least a single limit must be set for the quota to be valid.

+
+ + +
@@ -50,7 +61,7 @@

Value must be between {{ ctrl.defaults.MemoryLimit }} and + > Value must be between {{ ctrl.ResourceQuotaDefaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}

@@ -93,7 +104,7 @@
-
- - + +
+ Load balancers
+ +
+ + + You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use + of load balancers in this resource pool. + +
+
+
+ + + + + This feature is available in Portainer Business Edition. + +
+
+
Ingresses @@ -250,33 +275,7 @@
- -
- Load balancers -
- -
- - - You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use - of load balancers in this resource pool. - -
-
-
- - - - - This feature is available in Portainer Business Edition. - -
-
- - - +
Storages
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index 867e83cbb..b91d337ff 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -1,14 +1,15 @@ import angular from 'angular'; -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues'; import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; -import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues'; +import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues'; import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants'; +import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota'; class KubernetesResourcePoolController { /* #region CONSTRUCTOR */ @@ -28,7 +29,8 @@ class KubernetesResourcePoolController { KubernetesPodService, KubernetesApplicationService, KubernetesNamespaceHelper, - KubernetesIngressService + KubernetesIngressService, + KubernetesVolumeService ) { this.$async = $async; this.$state = $state; @@ -46,18 +48,17 @@ class KubernetesResourcePoolController { this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesIngressService = KubernetesIngressService; + this.KubernetesVolumeService = KubernetesVolumeService; this.IngressClassTypes = KubernetesIngressClassTypes; + this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults; this.onInit = this.onInit.bind(this); this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this); this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this); this.getEvents = this.getEvents.bind(this); - this.getEventsAsync = this.getEventsAsync.bind(this); this.getApplications = this.getApplications.bind(this); - this.getApplicationsAsync = this.getApplicationsAsync.bind(this); this.getIngresses = this.getIngresses.bind(this); - this.getIngressesAsync = this.getIngressesAsync.bind(this); } /* #endregion */ @@ -74,7 +75,7 @@ class KubernetesResourcePoolController { } }); state.refs = duplicates; - state.hasDuplicates = Object.keys(duplicates).length > 0; + state.hasRefs = Object.keys(duplicates).length > 0; } /* #region ANNOTATIONS MANAGEMENT */ @@ -92,7 +93,7 @@ class KubernetesResourcePoolController { } isUpdateButtonDisabled() { - return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasDuplicates; + return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasRefs; } isQuotaValid() { @@ -107,11 +108,11 @@ class KubernetesResourcePoolController { } checkDefaults() { - if (this.formValues.CpuLimit < this.defaults.CpuLimit) { - this.formValues.CpuLimit = this.defaults.CpuLimit; + if (this.formValues.CpuLimit < KubernetesResourceQuotaDefaults.CpuLimit) { + this.formValues.CpuLimit = KubernetesResourceQuotaDefaults.CpuLimit; } - if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) { - this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit); + if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit)) { + this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit); } } @@ -140,45 +141,12 @@ class KubernetesResourcePoolController { return false; } + /* #region UPDATE RESOURCE POOL */ async updateResourcePoolAsync() { this.state.actionInProgress = true; try { this.checkDefaults(); - const namespace = this.pool.Namespace.Name; - const cpuLimit = this.formValues.CpuLimit; - const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit); - const owner = this.pool.Namespace.ResourcePoolOwner; - const quota = this.pool.Quota; - - if (this.formValues.HasQuota) { - if (quota) { - quota.CpuLimit = cpuLimit; - quota.MemoryLimit = memoryLimit; - await this.KubernetesResourceQuotaService.update(quota); - } else { - await this.createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit); - } - } else if (quota) { - await this.KubernetesResourceQuotaService.delete(quota); - } - - const promises = _.map(this.formValues.IngressClasses, (c) => { - c.Namespace = namespace; - if (c.WasSelected === false && c.Selected === true) { - const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); - return this.KubernetesIngressService.create(ingress); - } else if (c.WasSelected === true && c.Selected === false) { - return this.KubernetesIngressService.delete(c); - } else if (c.WasSelected === true && c.Selected === true) { - const oldIngress = _.find(this.ingresses, { Name: c.IngressClass.Name }); - const newIngress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c); - newIngress.Paths = angular.copy(oldIngress.Paths); - newIngress.PreviousHost = oldIngress.Host; - return this.KubernetesIngressService.patch(oldIngress, newIngress); - } - }); - await Promise.all(promises); - + await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues); this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name); this.$state.reload(); } catch (err) { @@ -212,75 +180,70 @@ class KubernetesResourcePoolController { return this.$async(this.updateResourcePoolAsync); } } + /* #endregion */ hasEventWarnings() { return this.state.eventWarningCount; } /* #region GET EVENTS */ - async getEventsAsync() { - try { - this.state.eventsLoading = true; - this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name); - this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events'); - } finally { - this.state.eventsLoading = false; - } - } - getEvents() { - return this.$async(this.getEventsAsync); + return this.$async(async () => { + try { + this.state.eventsLoading = true; + this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events'); + } finally { + this.state.eventsLoading = false; + } + }); } /* #endregion */ /* #region GET APPLICATIONS */ - async getApplicationsAsync() { - try { - this.state.applicationsLoading = true; - this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name); - this.applications = _.map(this.applications, (app) => { - const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods); - app.CPU = resourceReservation.CPU; - app.Memory = resourceReservation.Memory; - return app; - }); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve applications.'); - } finally { - this.state.applicationsLoading = false; - } - } - getApplications() { - return this.$async(this.getApplicationsAsync); + return this.$async(async () => { + try { + this.state.applicationsLoading = true; + this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name); + this.applications = _.map(this.applications, (app) => { + const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods); + app.CPU = resourceReservation.CPU; + app.Memory = resourceReservation.Memory; + return app; + }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications.'); + } finally { + this.state.applicationsLoading = false; + } + }); } /* #endregion */ /* #region GET INGRESSES */ - async getIngressesAsync() { - this.state.ingressesLoading = true; - try { - const namespace = this.pool.Namespace.Name; - this.allIngresses = await this.KubernetesIngressService.get(); - this.ingresses = _.filter(this.allIngresses, { Namespace: namespace }); - _.forEach(this.ingresses, (ing) => { - ing.Namespace = namespace; - _.forEach(ing.Paths, (path) => { - const application = _.find(this.applications, { ServiceName: path.ServiceName }); - path.ApplicationName = application && application.Name ? application.Name : '-'; - }); - }); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.'); - } finally { - this.state.ingressesLoading = false; - } - } - getIngresses() { - return this.$async(this.getIngressesAsync); + return this.$async(async () => { + this.state.ingressesLoading = true; + try { + const namespace = this.pool.Namespace.Name; + this.allIngresses = await this.KubernetesIngressService.get(); + this.ingresses = _.filter(this.allIngresses, { Namespace: namespace }); + _.forEach(this.ingresses, (ing) => { + ing.Namespace = namespace; + _.forEach(ing.Paths, (path) => { + const application = _.find(this.applications, { ServiceName: path.ServiceName }); + path.ApplicationName = application && application.Name ? application.Name : '-'; + }); + }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.'); + } finally { + this.state.ingressesLoading = false; + } + }); } /* #endregion */ @@ -290,9 +253,6 @@ class KubernetesResourcePoolController { const endpoint = this.EndpointProvider.currentEndpoint(); this.endpoint = endpoint; this.isAdmin = this.Authentication.isAdmin(); - this.defaults = KubernetesResourceQuotaDefaults; - this.formValues = new KubernetesResourcePoolFormValues(this.defaults); - this.formValues.HasQuota = false; this.state = { actionInProgress: false, @@ -312,7 +272,7 @@ class KubernetesResourcePoolController { eventWarningCount: 0, canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, duplicates: { - ingressHosts: new KubernetesFormValueDuplicate(), + ingressHosts: new KubernetesFormValidationReferences(), }, }; @@ -320,9 +280,11 @@ class KubernetesResourcePoolController { const name = this.$transition$.params().id; - const [nodes, pool] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get(name)]); + const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]); - this.pool = pool; + this.pool = _.find(pools, { Namespace: { Name: name } }); + this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults); + this.formValues.Name = this.pool.Namespace.Name; _.forEach(nodes, (item) => { this.state.sliderMaxMemory += filesizeParser(item.Memory); @@ -330,13 +292,10 @@ class KubernetesResourcePoolController { }); this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); - const quota = pool.Quota; + const quota = this.pool.Quota; if (quota) { this.oldQuota = angular.copy(quota); - this.formValues.HasQuota = true; - this.formValues.CpuLimit = quota.CpuLimit; - this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit); - + this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota); this.state.cpuUsed = quota.CpuLimitUsed; this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); } @@ -354,6 +313,7 @@ class KubernetesResourcePoolController { const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses); } + this.savedFormValues = angular.copy(this.formValues); } catch (err) { this.Notifications.error('Failure', err, 'Unable to load view data'); } finally { diff --git a/app/kubernetes/views/volumes/edit/volume.html b/app/kubernetes/views/volumes/edit/volume.html index 55e65e649..da4040d98 100644 --- a/app/kubernetes/views/volumes/edit/volume.html +++ b/app/kubernetes/views/volumes/edit/volume.html @@ -79,6 +79,7 @@ ng-model="ctrl.state.volumeSize" placeholder="20" ng-min="0" + min="0" ng-change="ctrl.onChangeSize()" required /> @@ -100,11 +101,11 @@
-
+

This field is required.

-

The new size must be greater than the actual size.

diff --git a/app/kubernetes/views/volumes/edit/volumeController.js b/app/kubernetes/views/volumes/edit/volumeController.js index 98ccaa268..b41408080 100644 --- a/app/kubernetes/views/volumes/edit/volumeController.js +++ b/app/kubernetes/views/volumes/edit/volumeController.js @@ -64,18 +64,17 @@ class KubernetesVolumeController { onChangeSize() { if (this.state.volumeSize) { - const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit); + const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 }); if (this.state.oldVolumeSize > size) { - this.state.volumeSizeError = true; + this.state.errors.volumeSize = true; } else { - this.volume.PersistentVolumeClaim.Storage = size; - this.state.volumeSizeError = false; + this.state.errors.volumeSize = false; } } } sizeIsValid() { - return !this.state.volumeSizeError && this.state.oldVolumeSize !== this.volume.PersistentVolumeClaim.Storage; + return !this.state.errors.volumeSize && this.state.volumeSize && this.state.oldVolumeSize !== filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 }); } /** @@ -84,7 +83,7 @@ class KubernetesVolumeController { async updateVolumeAsync(redeploy) { try { - this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0) + 'i'; + this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0); await this.KubernetesPersistentVolumeClaimService.patch(this.oldVolume.PersistentVolumeClaim, this.volume.PersistentVolumeClaim); this.Notifications.success('Volume successfully updated'); @@ -126,9 +125,9 @@ class KubernetesVolumeController { volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); this.volume = volume; this.oldVolume = angular.copy(volume); - this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2)); + this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2), 10); this.state.volumeSizeUnit = volume.PersistentVolumeClaim.Storage.slice(-2); - this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage); + this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage, { base: 10 }); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve volume'); } @@ -179,9 +178,11 @@ class KubernetesVolumeController { increaseSize: false, volumeSize: 0, volumeSizeUnit: 'GB', - volumeSizeError: false, volumeSharedAccessPolicy: '', volumeSharedAccessPolicyTooltip: '', + errors: { + volumeSize: false, + }, }; this.state.activeTab = this.LocalStorage.getActiveTab('volume'); diff --git a/app/kubernetes/views/volumes/volumesController.js b/app/kubernetes/views/volumes/volumesController.js index 9dc18f866..252df28cd 100644 --- a/app/kubernetes/views/volumes/volumesController.js +++ b/app/kubernetes/views/volumes/volumesController.js @@ -1,9 +1,10 @@ require('../../templates/advancedDeploymentPanel.html'); -import * as _ from 'lodash-es'; +import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import angular from 'angular'; import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; function buildStorages(storages, volumes) { _.forEach(storages, (s) => { @@ -15,28 +16,9 @@ function buildStorages(storages, volumes) { } function computeSize(volumes) { - let hasT, - hasG, - hasM = false; - const size = _.sumBy(volumes, (v) => { - const storage = v.PersistentVolumeClaim.Storage; - if (!hasT && _.endsWith(storage, 'TB')) { - hasT = true; - } else if (!hasG && _.endsWith(storage, 'GB')) { - hasG = true; - } else if (!hasM && _.endsWith(storage, 'MB')) { - hasM = true; - } - return filesizeParser(storage, { base: 10 }); - }); - if (hasT) { - return size / 1000 / 1000 / 1000 / 1000 + 'TB'; - } else if (hasG) { - return size / 1000 / 1000 / 1000 + 'GB'; - } else if (hasM) { - return size / 1000 / 1000 + 'MB'; - } - return size; + const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 })); + const format = KubernetesResourceQuotaHelper.formatBytes(size); + return `${format.Size}${format.SizeUnit}`; } class KubernetesVolumesController { diff --git a/package.json b/package.json index a3185c2eb..af5b2759b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "start:server": "grunt clean:server && grunt start:server", "start:client": "grunt clean:client && grunt start:client", "dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js", + "dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js", "dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client", "start:toolkit": "grunt start:toolkit", "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 684db2a8b..afec1cd8f 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -60,6 +60,15 @@ module.exports = { }, ], }, + devServer: { + contentBase: path.join(__dirname, '.tmp'), + compress: true, + port: 8999, + proxy: { + '/api': 'http://localhost:9000', + }, + open: true, + }, plugins: [ new HtmlWebpackPlugin({ template: './app/index.html', diff --git a/webpack/webpack.develop.js b/webpack/webpack.develop.js index 5802a363b..b393b65ef 100644 --- a/webpack/webpack.develop.js +++ b/webpack/webpack.develop.js @@ -1,4 +1,3 @@ -const path = require('path'); const webpackMerge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); @@ -18,13 +17,4 @@ module.exports = webpackMerge(commonConfig, { }, ], }, - devServer: { - contentBase: path.join(__dirname, '.tmp'), - compress: true, - port: 8999, - proxy: { - '/api': 'http://localhost:9000', - }, - open: true, - }, });