diff --git a/app/constants.js b/app/constants.js
index cb0e8f17f..febc848e8 100644
--- a/app/constants.js
+++ b/app/constants.js
@@ -30,3 +30,5 @@ angular
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none'])
.constant('KUBERNETES_DEFAULT_NAMESPACE', 'default')
.constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']);
+
+export const PORTAINER_FADEOUT = 1500;
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js
index c48095d81..d867a288f 100644
--- a/app/kubernetes/__module.js
+++ b/app/kubernetes/__module.js
@@ -182,6 +182,16 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
+ const nodeStats = {
+ name: 'kubernetes.cluster.node.stats',
+ url: '/stats',
+ views: {
+ 'content@': {
+ component: 'kubernetesNodeStatsView',
+ },
+ },
+ };
+
const dashboard = {
name: 'kubernetes.dashboard',
url: '/dashboard',
@@ -280,6 +290,7 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(dashboard);
$stateRegistryProvider.register(deploy);
$stateRegistryProvider.register(node);
+ $stateRegistryProvider.register(nodeStats);
$stateRegistryProvider.register(resourcePools);
$stateRegistryProvider.register(resourcePoolCreation);
$stateRegistryProvider.register(resourcePool);
diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html
index 8b976a235..b5f84a99e 100644
--- a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html
+++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html
@@ -107,6 +107,9 @@
+
+ Actions
+ |
@@ -128,6 +131,9 @@
{{ item.Memory | humansize }} |
{{ item.Version }} |
{{ item.IPAddress }} |
+
+ Stats
+ |
Loading... |
diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js
index 3c1312302..17fc80b92 100644
--- a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js
+++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js
@@ -9,5 +9,6 @@ angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', {
orderBy: '@',
refreshCallback: '<',
isAdmin: '<',
+ useServerMetrics: '<',
},
});
diff --git a/app/kubernetes/metrics/metrics.js b/app/kubernetes/metrics/metrics.js
index 6bacdbbee..391a26509 100644
--- a/app/kubernetes/metrics/metrics.js
+++ b/app/kubernetes/metrics/metrics.js
@@ -10,6 +10,7 @@ class KubernetesMetricsService {
this.capabilitiesAsync = this.capabilitiesAsync.bind(this);
this.getPodAsync = this.getPodAsync.bind(this);
+ this.getNodeAsync = this.getNodeAsync.bind(this);
}
/**
@@ -27,6 +28,26 @@ class KubernetesMetricsService {
return this.$async(this.capabilitiesAsync, endpointID);
}
+ /**
+ * Stats of Node
+ *
+ * @param {string} nodeName
+ */
+ async getNodeAsync(nodeName) {
+ try {
+ const params = new KubernetesCommonParams();
+ params.id = nodeName;
+ const data = await this.KubernetesMetrics().getNode(params).$promise;
+ return data;
+ } catch (err) {
+ throw new PortainerError('Unable to retrieve node stats', err);
+ }
+ }
+
+ getNode(nodeName) {
+ return this.$async(this.getNodeAsync, nodeName);
+ }
+
/**
* Stats
*
diff --git a/app/kubernetes/metrics/rest.js b/app/kubernetes/metrics/rest.js
index 22f071b3b..92fa6b95b 100644
--- a/app/kubernetes/metrics/rest.js
+++ b/app/kubernetes/metrics/rest.js
@@ -20,6 +20,10 @@ angular.module('portainer.kubernetes').factory('KubernetesMetrics', [
method: 'GET',
url: podUrl,
},
+ getNode: {
+ method: 'GET',
+ url: `${url}/nodes/:id`,
+ },
}
);
};
diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html
index 47a198c6e..d1c9f08cb 100644
--- a/app/kubernetes/views/cluster/cluster.html
+++ b/app/kubernetes/views/cluster/cluster.html
@@ -89,6 +89,7 @@
order-by="Name"
refresh-callback="ctrl.getNodes"
is-admin="ctrl.isAdmin"
+ use-server-metrics="ctrl.state.useServerMetrics"
>
diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js
index 7726834d2..6824e0738 100644
--- a/app/kubernetes/views/cluster/clusterController.js
+++ b/app/kubernetes/views/cluster/clusterController.js
@@ -15,7 +15,8 @@ class KubernetesClusterController {
KubernetesNodeService,
KubernetesApplicationService,
KubernetesComponentStatusService,
- KubernetesEndpointService
+ KubernetesEndpointService,
+ EndpointProvider
) {
this.$async = $async;
this.$state = $state;
@@ -26,6 +27,7 @@ class KubernetesClusterController {
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesComponentStatusService = KubernetesComponentStatusService;
this.KubernetesEndpointService = KubernetesEndpointService;
+ this.EndpointProvider = EndpointProvider;
this.onInit = this.onInit.bind(this);
this.getNodes = this.getNodes.bind(this);
@@ -132,6 +134,7 @@ class KubernetesClusterController {
}
this.state.viewReady = true;
+ this.state.useServerMetrics = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.UseServerMetrics;
}
$onInit() {
diff --git a/app/kubernetes/views/cluster/node/stats/stats.html b/app/kubernetes/views/cluster/node/stats/stats.html
new file mode 100644
index 000000000..be2341dbf
--- /dev/null
+++ b/app/kubernetes/views/cluster/node/stats/stats.html
@@ -0,0 +1,71 @@
+
+ Cluster > {{ ctrl.state.transition.nodeName }} >
+ {{ ctrl.state.transition.nodeName }}
+
+
+
+
+
+
+
+
+ Portainer was unable to retrieve any metrics associated to that node. Please contact your administrator to ensure that the Kubernetes metrics feature is properly configured.
+
+
+
+
+
+
diff --git a/app/kubernetes/views/cluster/node/stats/stats.js b/app/kubernetes/views/cluster/node/stats/stats.js
new file mode 100644
index 000000000..98e362777
--- /dev/null
+++ b/app/kubernetes/views/cluster/node/stats/stats.js
@@ -0,0 +1,8 @@
+angular.module('portainer.kubernetes').component('kubernetesNodeStatsView', {
+ templateUrl: './stats.html',
+ controller: 'KubernetesNodeStatsController',
+ controllerAs: 'ctrl',
+ bindings: {
+ $transition$: '<',
+ },
+});
diff --git a/app/kubernetes/views/cluster/node/stats/statsController.js b/app/kubernetes/views/cluster/node/stats/statsController.js
new file mode 100644
index 000000000..e56424508
--- /dev/null
+++ b/app/kubernetes/views/cluster/node/stats/statsController.js
@@ -0,0 +1,144 @@
+import angular from 'angular';
+import moment from 'moment';
+import filesizeParser from 'filesize-parser';
+import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
+import { PORTAINER_FADEOUT } from '@/constants';
+
+class KubernetesNodeStatsController {
+ /* @ngInject */
+ constructor($async, $state, $interval, $document, Notifications, KubernetesNodeService, KubernetesMetricsService, ChartService) {
+ this.$async = $async;
+ this.$state = $state;
+ this.$interval = $interval;
+ this.$document = $document;
+ this.Notifications = Notifications;
+ this.KubernetesNodeService = KubernetesNodeService;
+ this.KubernetesMetricsService = KubernetesMetricsService;
+ this.ChartService = ChartService;
+
+ this.onInit = this.onInit.bind(this);
+ }
+
+ changeUpdateRepeater() {
+ var cpuChart = this.cpuChart;
+ var memoryChart = this.memoryChart;
+
+ this.stopRepeater();
+ this.setUpdateRepeater(cpuChart, memoryChart);
+ $('#refreshRateChange').show();
+ $('#refreshRateChange').fadeOut(PORTAINER_FADEOUT);
+ }
+
+ updateCPUChart() {
+ const label = moment(this.stats.read).format('HH:mm:ss');
+ this.ChartService.UpdateCPUChart(label, this.stats.CPUUsage, this.cpuChart);
+ }
+
+ updateMemoryChart() {
+ const label = moment(this.stats.read).format('HH:mm:ss');
+ this.ChartService.UpdateMemoryChart(label, this.stats.MemoryUsage, 0, this.memoryChart);
+ }
+
+ stopRepeater() {
+ var repeater = this.repeater;
+ if (angular.isDefined(repeater)) {
+ this.$interval.cancel(repeater);
+ this.repeater = undefined;
+ }
+ }
+
+ setUpdateRepeater() {
+ const refreshRate = this.state.refreshRate;
+
+ this.repeater = this.$interval(async () => {
+ try {
+ await this.getStats();
+ this.updateCPUChart();
+ this.updateMemoryChart();
+ } catch (error) {
+ this.stopRepeater();
+ this.Notifications.error('Failure', error);
+ }
+ }, refreshRate * 1000);
+ }
+
+ initCharts() {
+ const cpuChartCtx = $('#cpuChart');
+ const cpuChart = this.ChartService.CreateCPUChart(cpuChartCtx);
+ this.cpuChart = cpuChart;
+
+ const memoryChartCtx = $('#memoryChart');
+ const memoryChart = this.ChartService.CreateMemoryChart(memoryChartCtx);
+ this.memoryChart = memoryChart;
+
+ this.updateCPUChart();
+ this.updateMemoryChart();
+ this.setUpdateRepeater();
+ }
+
+ getStats() {
+ return this.$async(async () => {
+ try {
+ const stats = await this.KubernetesMetricsService.getNode(this.state.transition.nodeName);
+ if (stats) {
+ const memory = filesizeParser(stats.usage.memory);
+ const cpu = KubernetesResourceReservationHelper.parseCPU(stats.usage.cpu);
+ this.stats = {
+ read: stats.creationTimestamp,
+ MemoryUsage: memory,
+ CPUUsage: (cpu / this.nodeCPU) * 100,
+ };
+ }
+ } catch (err) {
+ this.Notifications.error('Failure', err, 'Unable to retrieve node stats');
+ }
+ });
+ }
+
+ $onDestroy() {
+ this.stopRepeater();
+ }
+
+ async onInit() {
+ this.state = {
+ autoRefresh: false,
+ refreshRate: '30',
+ viewReady: false,
+ transition: {
+ nodeName: this.$transition$.params().name,
+ },
+ getMetrics: true,
+ };
+
+ try {
+ const nodeMetrics = await this.KubernetesMetricsService.getNode(this.state.transition.nodeName);
+
+ if (nodeMetrics) {
+ const node = await this.KubernetesNodeService.get(this.state.transition.nodeName);
+ this.nodeCPU = node.CPU || 1;
+
+ await this.getStats();
+
+ if (this.state.getMetrics) {
+ this.$document.ready(() => {
+ this.initCharts();
+ });
+ }
+ } else {
+ this.state.getMetrics = false;
+ }
+ } catch (err) {
+ this.state.getMetrics = false;
+ this.Notifications.error('Failure', err, 'Unable to retrieve node stats');
+ } finally {
+ this.state.viewReady = true;
+ }
+ }
+
+ $onInit() {
+ return this.$async(this.onInit);
+ }
+}
+
+export default KubernetesNodeStatsController;
+angular.module('portainer.kubernetes').controller('KubernetesNodeStatsController', KubernetesNodeStatsController);