From ce31de5e9e40953c33239da7f870c46d2a075b93 Mon Sep 17 00:00:00 2001 From: zees-dev <63374656+zees-dev@users.noreply.github.com> Date: Wed, 28 Jul 2021 14:26:03 +1200 Subject: [PATCH] feat(kubernetes/resource-usage): k8s resource usage for cluster, node and namespace EE-3 EE-1112 (#5301) * backported resource usage functionality from EE * utilising view bound endpoint object instead of depracated EndpointProvider * refactor flatmap * addressed merge conflict issues --- .../resourceReservation.html | 32 ++++++++++++--- .../resourceReservation.js | 7 +++- .../resourceReservationController.js | 9 ++++- app/kubernetes/metrics/metrics.js | 40 +++++++++++++++++++ app/kubernetes/metrics/rest.js | 8 ++++ app/kubernetes/views/cluster/cluster.html | 11 +++-- app/kubernetes/views/cluster/cluster.js | 3 ++ .../views/cluster/clusterController.js | 28 +++++++++++-- app/kubernetes/views/cluster/node/node.html | 7 +++- app/kubernetes/views/cluster/node/node.js | 1 + .../views/cluster/node/nodeController.js | 31 +++++++++++++- .../resource-pools/edit/resourcePool.html | 7 +++- .../edit/resourcePoolController.js | 34 ++++++++++++---- 13 files changed, 188 insertions(+), 30 deletions(-) diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.html b/app/kubernetes/components/resource-reservation/resourceReservation.html index e03e62ace..d838145ae 100644 --- a/app/kubernetes/components/resource-reservation/resourceReservation.html +++ b/app/kubernetes/components/resource-reservation/resourceReservation.html @@ -10,22 +10,42 @@
-
+
+ +
+ + {{ $ctrl.memoryUsage }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryUsagePercent }}%
-
+
+ +
+ + {{ $ctrl.cpuUsage | kubernetesApplicationCPUValue }} / {{ $ctrl.cpuLimit }} - {{ $ctrl.cpuUsagePercent }}%
diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.js b/app/kubernetes/components/resource-reservation/resourceReservation.js index 617f70079..d104fda73 100644 --- a/app/kubernetes/components/resource-reservation/resourceReservation.js +++ b/app/kubernetes/components/resource-reservation/resourceReservation.js @@ -3,9 +3,12 @@ angular.module('portainer.kubernetes').component('kubernetesResourceReservation' controller: 'KubernetesResourceReservationController', bindings: { description: '@', - cpu: '<', + cpuReservation: '<', + cpuUsage: '<', cpuLimit: '<', - memory: '<', + memoryReservation: '<', + memoryUsage: '<', memoryLimit: '<', + displayUsage: '<', }, }); diff --git a/app/kubernetes/components/resource-reservation/resourceReservationController.js b/app/kubernetes/components/resource-reservation/resourceReservationController.js index f55a014a1..1c25cb23f 100644 --- a/app/kubernetes/components/resource-reservation/resourceReservationController.js +++ b/app/kubernetes/components/resource-reservation/resourceReservationController.js @@ -3,10 +3,15 @@ import angular from 'angular'; class KubernetesResourceReservationController { usageValues() { if (this.cpuLimit) { - this.cpuUsage = Math.round((this.cpu / this.cpuLimit) * 100); + this.cpuReservationPercent = Math.round((this.cpuReservation / this.cpuLimit) * 100); } if (this.memoryLimit) { - this.memoryUsage = Math.round((this.memory / this.memoryLimit) * 100); + this.memoryReservationPercent = Math.round((this.memoryReservation / this.memoryLimit) * 100); + } + + if (this.displayUsage && this.cpuLimit && this.memoryLimit) { + this.cpuUsagePercent = Math.round((this.cpuUsage / this.cpuLimit) * 100); + this.memoryUsagePercent = Math.round((this.memoryUsage / this.memoryLimit) * 100); } } diff --git a/app/kubernetes/metrics/metrics.js b/app/kubernetes/metrics/metrics.js index 391a26509..c374bd2e0 100644 --- a/app/kubernetes/metrics/metrics.js +++ b/app/kubernetes/metrics/metrics.js @@ -9,8 +9,12 @@ class KubernetesMetricsService { this.KubernetesMetrics = KubernetesMetrics; this.capabilitiesAsync = this.capabilitiesAsync.bind(this); + this.getPodAsync = this.getPodAsync.bind(this); this.getNodeAsync = this.getNodeAsync.bind(this); + + this.getPodsAsync = this.getPodsAsync.bind(this); + this.getNodesAsync = this.getNodesAsync.bind(this); } /** @@ -68,6 +72,42 @@ class KubernetesMetricsService { getPod(namespace, podName) { return this.$async(this.getPodAsync, namespace, podName); } + + /** + * Stats of Nodes in cluster + * + * @param {string} endpointID + */ + async getNodesAsync(endpointID) { + try { + const data = await this.KubernetesMetrics().getNodes({ endpointId: endpointID }).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to retrieve nodes stats', err); + } + } + + getNodes(endpointID) { + return this.$async(this.getNodesAsync, endpointID); + } + + /** + * Stats of Pods in a namespace + * + * @param {string} namespace + */ + async getPodsAsync(namespace) { + try { + const data = await this.KubernetesMetrics(namespace).getPods().$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to retrieve pod stats', err); + } + } + + getPods(namespace) { + return this.$async(this.getPodsAsync, namespace); + } } export default KubernetesMetricsService; diff --git a/app/kubernetes/metrics/rest.js b/app/kubernetes/metrics/rest.js index 92fa6b95b..1dc97c212 100644 --- a/app/kubernetes/metrics/rest.js +++ b/app/kubernetes/metrics/rest.js @@ -24,6 +24,14 @@ angular.module('portainer.kubernetes').factory('KubernetesMetrics', [ method: 'GET', url: `${url}/nodes/:id`, }, + getPods: { + method: 'GET', + url: `${url}/namespaces/:namespace/pods`, + }, + getNodes: { + method: 'GET', + url: `${url}/nodes`, + }, } ); }; diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html index d1c9f08cb..f64e517b5 100644 --- a/app/kubernetes/views/cluster/cluster.html +++ b/app/kubernetes/views/cluster/cluster.html @@ -12,11 +12,14 @@
diff --git a/app/kubernetes/views/cluster/cluster.js b/app/kubernetes/views/cluster/cluster.js index 688cf877e..708076037 100644 --- a/app/kubernetes/views/cluster/cluster.js +++ b/app/kubernetes/views/cluster/cluster.js @@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesClusterView', { templateUrl: './cluster.html', controller: 'KubernetesClusterController', controllerAs: 'ctrl', + bindings: { + endpoint: '<', + }, }); diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js index 6824e0738..3b78e230d 100644 --- a/app/kubernetes/views/cluster/clusterController.js +++ b/app/kubernetes/views/cluster/clusterController.js @@ -13,10 +13,10 @@ class KubernetesClusterController { Notifications, LocalStorage, KubernetesNodeService, + KubernetesMetricsService, KubernetesApplicationService, KubernetesComponentStatusService, - KubernetesEndpointService, - EndpointProvider + KubernetesEndpointService ) { this.$async = $async; this.$state = $state; @@ -24,10 +24,10 @@ class KubernetesClusterController { this.Notifications = Notifications; this.LocalStorage = LocalStorage; this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesMetricsService = KubernetesMetricsService; this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesComponentStatusService = KubernetesComponentStatusService; this.KubernetesEndpointService = KubernetesEndpointService; - this.EndpointProvider = EndpointProvider; this.onInit = this.onInit.bind(this); this.getNodes = this.getNodes.bind(this); @@ -106,6 +106,10 @@ class KubernetesClusterController { new KubernetesResourceReservation() ); this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory); + + if (this.isAdmin) { + await this.getResourceUsage(this.endpoint.Id); + } } catch (err) { this.Notifications.error('Failure', 'Unable to retrieve applications', err); } finally { @@ -117,14 +121,31 @@ class KubernetesClusterController { return this.$async(this.getApplicationsAsync); } + async getResourceUsage(endpointId) { + try { + const nodeMetrics = await this.KubernetesMetricsService.getNodes(endpointId); + const resourceUsageList = nodeMetrics.items.map((i) => i.usage); + const clusterResourceUsage = resourceUsageList.reduce((total, u) => { + total.CPU += KubernetesResourceReservationHelper.parseCPU(u.cpu); + total.Memory += KubernetesResourceReservationHelper.megaBytesValue(u.memory); + return total; + }, new KubernetesResourceReservation()); + this.resourceUsage = clusterResourceUsage; + } catch (err) { + this.Notifications.error('Failure', 'Unable to retrieve cluster resource usage', err); + } + } + async onInit() { this.state = { applicationsLoading: true, viewReady: false, hasUnhealthyComponentStatus: false, + useServerMetrics: false, }; this.isAdmin = this.Authentication.isAdmin(); + this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; await this.getNodes(); if (this.isAdmin) { @@ -134,7 +155,6 @@ class KubernetesClusterController { } this.state.viewReady = true; - this.state.useServerMetrics = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.UseServerMetrics; } $onInit() { diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html index c4c9229c1..50105fe5b 100644 --- a/app/kubernetes/views/cluster/node/node.html +++ b/app/kubernetes/views/cluster/node/node.html @@ -78,11 +78,14 @@
diff --git a/app/kubernetes/views/cluster/node/node.js b/app/kubernetes/views/cluster/node/node.js index 38fb14ede..bf29d91c1 100644 --- a/app/kubernetes/views/cluster/node/node.js +++ b/app/kubernetes/views/cluster/node/node.js @@ -3,6 +3,7 @@ angular.module('portainer.kubernetes').component('kubernetesNodeView', { controller: 'KubernetesNodeController', controllerAs: 'ctrl', bindings: { + endpoint: '<', $transition$: '<', }, }); diff --git a/app/kubernetes/views/cluster/node/nodeController.js b/app/kubernetes/views/cluster/node/nodeController.js index fe55409e1..8b4369415 100644 --- a/app/kubernetes/views/cluster/node/nodeController.js +++ b/app/kubernetes/views/cluster/node/nodeController.js @@ -21,7 +21,9 @@ class KubernetesNodeController { KubernetesEventService, KubernetesPodService, KubernetesApplicationService, - KubernetesEndpointService + KubernetesEndpointService, + KubernetesMetricsService, + Authentication ) { this.$async = $async; this.$state = $state; @@ -33,6 +35,8 @@ class KubernetesNodeController { this.KubernetesPodService = KubernetesPodService; this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesEndpointService = KubernetesEndpointService; + this.KubernetesMetricsService = KubernetesMetricsService; + this.Authentication = Authentication; this.onInit = this.onInit.bind(this); this.getNodesAsync = this.getNodesAsync.bind(this); @@ -42,6 +46,7 @@ class KubernetesNodeController { this.getEndpointsAsync = this.getEndpointsAsync.bind(this); this.updateNodeAsync = this.updateNodeAsync.bind(this); this.drainNodeAsync = this.drainNodeAsync.bind(this); + this.getNodeUsageAsync = this.getNodeUsageAsync.bind(this); } selectTab(index) { @@ -327,6 +332,22 @@ class KubernetesNodeController { return this.$async(this.getNodesAsync); } + async getNodeUsageAsync() { + try { + const nodeName = this.$transition$.params().name; + const node = await this.KubernetesMetricsService.getNode(nodeName); + this.resourceUsage = new KubernetesResourceReservation(); + this.resourceUsage.CPU = KubernetesResourceReservationHelper.parseCPU(node.usage.cpu); + this.resourceUsage.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory); + } catch (err) { + this.Notifications.error('Failure', 'Unable to retrieve node resource usage', err); + } + } + + getNodeUsage() { + return this.$async(this.getNodeUsageAsync); + } + hasEventWarnings() { return this.state.eventWarningCount; } @@ -375,6 +396,10 @@ class KubernetesNodeController { this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory); this.memoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory); this.state.isContainPortainer = _.find(this.applications, { ApplicationName: 'portainer' }); + + if (this.state.isAdmin) { + await this.getNodeUsage(); + } } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve applications'); } finally { @@ -388,6 +413,7 @@ class KubernetesNodeController { async onInit() { this.state = { + isAdmin: this.Authentication.isAdmin(), activeTab: 0, currentName: this.$state.$current.name, dataLoading: true, @@ -402,10 +428,13 @@ class KubernetesNodeController { hasDuplicateLabelKeys: false, isDrainOperation: false, isContainPortainer: false, + useServerMetrics: false, }; this.availabilities = KubernetesNodeAvailabilities; + this.state.useServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; + this.state.activeTab = this.LocalStorage.getActiveTab('node'); await this.getNodes(); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index 2a7ece853..de5e75f60 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -40,10 +40,13 @@ diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index 31c5ff6f3..6e2da1339 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -3,6 +3,7 @@ import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import { KubernetesResourcePoolFormValues, @@ -27,6 +28,7 @@ class KubernetesResourcePoolController { EndpointService, ModalService, KubernetesNodeService, + KubernetesMetricsService, KubernetesResourceQuotaService, KubernetesResourcePoolService, KubernetesEventService, @@ -45,6 +47,7 @@ class KubernetesResourcePoolController { EndpointService, ModalService, KubernetesNodeService, + KubernetesMetricsService, KubernetesResourceQuotaService, KubernetesResourcePoolService, KubernetesEventService, @@ -240,6 +243,8 @@ class KubernetesResourcePoolController { app.Memory = resourceReservation.Memory; return app; }); + + await this.getResourceUsage(this.pool.Namespace.Name); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve applications.'); } finally { @@ -300,11 +305,26 @@ class KubernetesResourcePoolController { } /* #endregion */ + async getResourceUsage(namespace) { + try { + const namespaceMetrics = await this.KubernetesMetricsService.getPods(namespace); + // extract resource usage of all containers within each pod of the namespace + const containerResourceUsageList = namespaceMetrics.items.flatMap((i) => i.containers.map((c) => c.usage)); + const namespaceResourceUsage = containerResourceUsageList.reduce((total, u) => { + total.CPU += KubernetesResourceReservationHelper.parseCPU(u.cpu); + total.Memory += KubernetesResourceReservationHelper.megaBytesValue(u.memory); + return total; + }, new KubernetesResourceReservation()); + this.state.resourceUsage = namespaceResourceUsage; + } catch (err) { + this.Notifications.error('Failure', 'Unable to retrieve namespace resource usage', err); + } + } + /* #region ON INIT */ $onInit() { return this.$async(async () => { try { - const endpoint = this.endpoint; this.isAdmin = this.Authentication.isAdmin(); this.state = { @@ -312,9 +332,8 @@ class KubernetesResourcePoolController { sliderMaxMemory: 0, sliderMaxCpu: 0, cpuUsage: 0, - cpuUsed: 0, memoryUsage: 0, - memoryUsed: 0, + resourceReservation: { CPU: 0, Memory: 0 }, activeTab: 0, currentName: this.$state.$current.name, showEditorTab: false, @@ -323,7 +342,8 @@ class KubernetesResourcePoolController { ingressesLoading: true, viewReady: false, eventWarningCount: 0, - canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length, + canUseIngress: this.endpoint.Kubernetes.Configuration.IngressClasses.length, + useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics, duplicates: { ingressHosts: new KubernetesFormValidationReferences(), }, @@ -352,8 +372,8 @@ class KubernetesResourcePoolController { this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota); this.formValues.EndpointId = this.endpoint.Id; - this.state.cpuUsed = quota.CpuLimitUsed; - this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); + this.state.resourceReservation.CPU = quota.CpuLimitUsed; + this.state.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); } this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name); @@ -366,7 +386,7 @@ class KubernetesResourcePoolController { if (this.state.canUseIngress) { await this.getIngresses(); - const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses; + const ingressClasses = this.endpoint.Kubernetes.Configuration.IngressClasses; this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses); _.forEach(this.formValues.IngressClasses, (ic) => { if (ic.Hosts.length === 0) {