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 @@
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.
+ +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.
+Path is required.
-This path is already used.
Path is required.
+This path is already used.
+At least a single limit must be set for the quota to be valid.
Value must be between {{ ctrl.defaults.MemoryLimit }} and + > Value must be between {{ ctrl.ResourceQuotaDefaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}
This field is required.
The new size must be greater than the actual size.