From f765c63c746e8ebbfe9a06061670b8fb73ae3c8f Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 17 Jul 2020 01:39:16 +0200 Subject: [PATCH] feat(cluster): Show the cluster health by showing the status of the underlying cluster components (#4022) * feat(cluster): add tabs * feat(cluster): add cluster status informations to cluster detail view * feat(cluster): change data display * feat(cluster): prevent regular users to see cluster health * feat(kubernetes): reviewed ComponentStatus handling * refactor(kubernetes): review apiToModel for KubernetesComponentStatus * refactor(kubernetes): remove unused variable * refactor(kubernetes): clean hasUnhealthyComponentStatus code Co-authored-by: Anthony Lapenna --- app/kubernetes/component-status/converter.js | 21 ++++++++++++ app/kubernetes/component-status/models.js | 14 ++++++++ app/kubernetes/component-status/rest.js | 24 +++++++++++++ app/kubernetes/component-status/service.js | 34 +++++++++++++++++++ app/kubernetes/views/cluster/cluster.html | 30 ++++++++++++++-- .../views/cluster/clusterController.js | 22 +++++++++++- 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 app/kubernetes/component-status/converter.js create mode 100644 app/kubernetes/component-status/models.js create mode 100644 app/kubernetes/component-status/rest.js create mode 100644 app/kubernetes/component-status/service.js diff --git a/app/kubernetes/component-status/converter.js b/app/kubernetes/component-status/converter.js new file mode 100644 index 000000000..730cf60b7 --- /dev/null +++ b/app/kubernetes/component-status/converter.js @@ -0,0 +1,21 @@ +import _ from 'lodash-es'; +import { KubernetesComponentStatus } from './models'; + +export class KubernetesComponentStatusConverter { + /** + * Convert API data to KubernetesComponentStatus model + */ + static apiToModel(data) { + const res = new KubernetesComponentStatus(); + res.ComponentName = data.metadata.name; + + const healthyCondition = _.find(data.conditions, { type: 'Healthy' }); + if (healthyCondition && healthyCondition.status === 'True') { + res.Healthy = true; + } else if (healthyCondition && healthyCondition.status === 'False') { + res.ErrorMessage = healthyCondition.message; + } + + return res; + } +} diff --git a/app/kubernetes/component-status/models.js b/app/kubernetes/component-status/models.js new file mode 100644 index 000000000..a73862c08 --- /dev/null +++ b/app/kubernetes/component-status/models.js @@ -0,0 +1,14 @@ +/** + * KubernetesComponentStatus Model + */ +const _KubernetesComponentStatus = Object.freeze({ + ComponentName: '', + Healthy: false, + ErrorMessage: '', +}); + +export class KubernetesComponentStatus { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesComponentStatus))); + } +} diff --git a/app/kubernetes/component-status/rest.js b/app/kubernetes/component-status/rest.js new file mode 100644 index 000000000..a286b584f --- /dev/null +++ b/app/kubernetes/component-status/rest.js @@ -0,0 +1,24 @@ +angular.module('portainer.kubernetes').factory('KubernetesComponentStatus', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesComponentStatusFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + '/componentstatuses/:id'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/component-status/service.js b/app/kubernetes/component-status/service.js new file mode 100644 index 000000000..1b5d4efe2 --- /dev/null +++ b/app/kubernetes/component-status/service.js @@ -0,0 +1,34 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import _ from 'lodash-es'; +import { KubernetesComponentStatusConverter } from './converter'; + +class KubernetesComponentStatusService { + /* @ngInject */ + constructor($async, KubernetesComponentStatus) { + this.$async = $async; + this.KubernetesComponentStatus = KubernetesComponentStatus; + + this.getAsync = this.getAsync.bind(this); + } + + /** + * GET + */ + async getAsync() { + try { + const data = await this.KubernetesComponentStatus().get().$promise; + const res = _.map(data.items, (item) => KubernetesComponentStatusConverter.apiToModel(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve cluster status', err); + } + } + + get() { + return this.$async(this.getAsync); + } +} + +export default KubernetesComponentStatusService; +angular.module('portainer.kubernetes').service('KubernetesComponentStatusService', KubernetesComponentStatusService); diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html index eeb43f9da..60bc353ff 100644 --- a/app/kubernetes/views/cluster/cluster.html +++ b/app/kubernetes/views/cluster/cluster.html @@ -5,11 +5,11 @@
-
+
-
+
+ +
+ Cluster status +
+ + + + + + + + + + + + + + +
ComponentStatusError
+ {{ cs.ComponentName }} + + healthy + unhealthy + + {{ cs.ErrorMessage !== '' ? cs.ErrorMessage : '-' }} +
diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js index 2a8313a87..d565c850c 100644 --- a/app/kubernetes/views/cluster/clusterController.js +++ b/app/kubernetes/views/cluster/clusterController.js @@ -6,17 +6,35 @@ import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reserv class KubernetesClusterController { /* @ngInject */ - constructor($async, Authentication, Notifications, KubernetesNodeService, KubernetesApplicationService) { + constructor($async, $state, Authentication, Notifications, LocalStorage, KubernetesNodeService, KubernetesApplicationService, KubernetesComponentStatusService) { this.$async = $async; + this.$state = $state; this.Authentication = Authentication; this.Notifications = Notifications; + this.LocalStorage = LocalStorage; this.KubernetesNodeService = KubernetesNodeService; this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesComponentStatusService = KubernetesComponentStatusService; this.onInit = this.onInit.bind(this); this.getNodes = this.getNodes.bind(this); this.getNodesAsync = this.getNodesAsync.bind(this); this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + this.getComponentStatus = this.getComponentStatus.bind(this); + this.getComponentStatusAsync = this.getComponentStatusAsync.bind(this); + } + + async getComponentStatusAsync() { + try { + this.ComponentStatuses = await this.KubernetesComponentStatusService.get(); + this.hasUnhealthyComponentStatus = _.find(this.ComponentStatuses, { Healthy: false }) ? true : false; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve cluster component statuses'); + } + } + + getComponentStatus() { + return this.$async(this.getComponentStatusAsync); } async getNodesAsync() { @@ -67,12 +85,14 @@ class KubernetesClusterController { this.state = { applicationsLoading: true, viewReady: false, + hasUnhealthyComponentStatus: false, }; this.isAdmin = this.Authentication.isAdmin(); await this.getNodes(); if (this.isAdmin) { + await this.getComponentStatus(); await this.getApplications(); }