diff --git a/app/kubernetes/services/namespaceService.js b/app/kubernetes/services/namespaceService.js index 1b85bd639..3e25ae636 100644 --- a/app/kubernetes/services/namespaceService.js +++ b/app/kubernetes/services/namespaceService.js @@ -1,5 +1,3 @@ -import _ from 'lodash-es'; - import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; @@ -65,10 +63,13 @@ class KubernetesNamespaceService { async getAllAsync() { try { + // get the list of all namespaces (RBAC allows users to see the list of namespaces) const data = await this.KubernetesNamespaces().get().$promise; - const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise); + // get the status of each namespace (RBAC will give permission denied for status of unauthorised namespaces) + const promises = data.items.map((item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise); const namespaces = await $allSettled(promises); - const allNamespaces = _.map(namespaces.fulfilled, (item) => { + // only return namespaces if the user has access to namespaces + const allNamespaces = namespaces.fulfilled.map((item) => { return KubernetesNamespaceConverter.apiToNamespace(item); }); updateNamespaces(allNamespaces); diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index 5922370cb..639c44db1 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -21,27 +21,33 @@ export function KubernetesResourcePoolService( toggleSystem, }; - async function getOne(name) { + // getting quota isn't a costly operation for one namespace, so we can get it by default + async function getOne(name, { getQuota = true }) { 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; - pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + if (getQuota) { + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } } return pool; } - async function getAll() { + // getting the quota for all namespaces is costly by default, so disable getting it by default + async function getAll({ getQuota = false }) { const namespaces = await KubernetesNamespaceService.get(); const pools = await Promise.all( _.map(namespaces, async (namespace) => { const name = namespace.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; - pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + if (getQuota) { + const [quotaAttempt] = await Promise.allSettled([KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } } return pool; }) @@ -49,11 +55,11 @@ export function KubernetesResourcePoolService( return pools; } - function get(name) { + function get(name, options = {}) { if (name) { - return $async(getOne, name); + return $async(getOne, name, options); } - return $async(getAll); + return $async(getAll, options); } function create(formValues) { diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index bc72cdcbe..db6a7e531 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -872,8 +872,8 @@ class KubernetesCreateApplicationController { /* #endregion */ /* #region DATA AUTO REFRESH */ - updateSliders() { - const quota = this.formValues.ResourcePool.Quota; + updateSliders(namespaceWithQuota) { + const quota = namespaceWithQuota.Quota; let minCpu = 0, minMemory = 0, maxCpu = this.state.namespaceLimits.cpu, @@ -906,33 +906,36 @@ class KubernetesCreateApplicationController { } } - updateNamespaceLimits() { - let maxCpu = this.state.nodes.cpu; - let maxMemory = this.state.nodes.memory; - const quota = this.formValues.ResourcePool.Quota; + updateNamespaceLimits(namespaceWithQuota) { + return this.$async(async () => { + let maxCpu = this.state.nodes.cpu; + let maxMemory = this.state.nodes.memory; - this.state.resourcePoolHasQuota = false; + const quota = namespaceWithQuota.Quota; - if (quota) { - if (quota.CpuLimit) { - this.state.resourcePoolHasQuota = true; - maxCpu = quota.CpuLimit - quota.CpuLimitUsed; - if (this.state.isEdit && this.savedFormValues.CpuLimit) { - maxCpu += this.savedFormValues.CpuLimit * this.effectiveInstances(); + this.state.resourcePoolHasQuota = false; + + if (quota) { + if (quota.CpuLimit) { + this.state.resourcePoolHasQuota = true; + maxCpu = quota.CpuLimit - quota.CpuLimitUsed; + if (this.state.isEdit && this.savedFormValues.CpuLimit) { + maxCpu += this.savedFormValues.CpuLimit * this.effectiveInstances(); + } + } + + if (quota.MemoryLimit) { + this.state.resourcePoolHasQuota = true; + maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed; + if (this.state.isEdit && this.savedFormValues.MemoryLimit) { + maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.effectiveInstances(); + } } } - if (quota.MemoryLimit) { - this.state.resourcePoolHasQuota = true; - maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed; - if (this.state.isEdit && this.savedFormValues.MemoryLimit) { - maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.effectiveInstances(); - } - } - } - - this.state.namespaceLimits.cpu = maxCpu; - this.state.namespaceLimits.memory = maxMemory; + this.state.namespaceLimits.cpu = maxCpu; + this.state.namespaceLimits.memory = maxMemory; + }); } refreshStacks(namespace) { @@ -1026,9 +1029,10 @@ class KubernetesCreateApplicationController { onResourcePoolSelectionChange() { return this.$async(async () => { + const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.formValues.ResourcePool.Namespace.Name); const namespace = this.formValues.ResourcePool.Namespace.Name; - this.updateNamespaceLimits(); - this.updateSliders(); + this.updateNamespaceLimits(namespaceWithQuota); + this.updateSliders(namespaceWithQuota); await this.refreshNamespaceData(namespace); this.resetFormValues(); }); @@ -1222,7 +1226,9 @@ class KubernetesCreateApplicationController { this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name); this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1)); + const namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name); this.formValues.ResourcePool = this.resourcePools[0]; + this.formValues.ResourcePool.Quota = namespaceWithQuota.Quota; if (!this.formValues.ResourcePool) { return; } @@ -1289,8 +1295,8 @@ class KubernetesCreateApplicationController { this.oldFormValues = angular.copy(this.formValues); - this.updateNamespaceLimits(); - this.updateSliders(); + this.updateNamespaceLimits(namespaceWithQuota); + this.updateSliders(namespaceWithQuota); } catch (err) { this.Notifications.error('Failure', err, 'Unable to load view data'); } finally { diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js index 39034d733..3759175c8 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -151,7 +151,7 @@ class KubernetesCreateResourcePoolController { getResourcePools() { return this.$async(async () => { try { - this.resourcePools = await this.KubernetesResourcePoolService.get(); + this.resourcePools = await this.KubernetesResourcePoolService.get('', { getQuota: true }); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve namespaces'); } diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index 8c8901b41..29e740c40 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -378,7 +378,7 @@ class KubernetesResourcePoolController { const name = this.$state.params.id; - const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]); + const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get('', { getQuota: true })]); this.ingressControllers = []; if (this.state.ingressAvailabilityPerNamespace) { diff --git a/app/kubernetes/views/resource-pools/resourcePoolsController.js b/app/kubernetes/views/resource-pools/resourcePoolsController.js index de3ba6b7b..51e380004 100644 --- a/app/kubernetes/views/resource-pools/resourcePoolsController.js +++ b/app/kubernetes/views/resource-pools/resourcePoolsController.js @@ -62,7 +62,7 @@ class KubernetesResourcePoolsController { async getResourcePoolsAsync() { try { - this.resourcePools = await this.KubernetesResourcePoolService.get(); + this.resourcePools = await this.KubernetesResourcePoolService.get('', { getQuota: true }); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retreive namespaces'); }