diff --git a/app/kubernetes/node/converter.js b/app/kubernetes/node/converter.js index cc57e5c90..a7dc4d313 100644 --- a/app/kubernetes/node/converter.js +++ b/app/kubernetes/node/converter.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; -import { KubernetesNode, KubernetesNodeDetails, KubernetesNodeTaint } from 'Kubernetes/node/models'; +import { KubernetesNode, KubernetesNodeDetails, KubernetesNodeTaint, KubernetesNodeAvailabilities, KubernetesPortainerNodeDrainLabel } from 'Kubernetes/node/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import { KubernetesNodeFormValues, KubernetesNodeTaintFormValues, KubernetesNodeLabelFormValues } from 'Kubernetes/node/formValues'; import { KubernetesNodeCreatePayload, KubernetesNodeTaintPayload } from 'Kubernetes/node/payload'; @@ -30,6 +30,11 @@ class KubernetesNodeConverter { NetworkUnavailable: networkUnavailable && networkUnavailable.status === 'True', }; + res.Availability = KubernetesNodeAvailabilities.ACTIVE; + if (data.spec.unschedulable === true) { + res.Availability = _.has(data.metadata.labels, KubernetesPortainerNodeDrainLabel) ? KubernetesNodeAvailabilities.DRAIN : KubernetesNodeAvailabilities.PAUSE; + } + if (ready.status === 'False') { res.Status = 'Unhealthy'; } else if (ready.status === 'Unknown' || res.Conditions.MemoryPressure || res.Conditions.PIDPressure || res.Conditions.DiskPressure || res.Conditions.NetworkUnavailable) { @@ -67,6 +72,8 @@ class KubernetesNodeConverter { static nodeToFormValues(node) { const res = new KubernetesNodeFormValues(); + res.Availability = node.Availability; + res.Taints = _.map(node.Taints, (taint) => { const res = new KubernetesNodeTaintFormValues(); res.Key = taint.Key; @@ -92,6 +99,8 @@ class KubernetesNodeConverter { static formValuesToNode(node, formValues) { const res = angular.copy(node); + res.Availability = formValues.Availability; + const filteredTaints = _.filter(formValues.Taints, (taint) => !taint.NeedsDeletion); res.Taints = _.map(filteredTaints, (item) => { const taint = new KubernetesNodeTaint(); @@ -130,6 +139,15 @@ class KubernetesNodeConverter { payload.metadata.labels = node.Labels; + if (node.Availability !== KubernetesNodeAvailabilities.ACTIVE) { + payload.spec.unschedulable = true; + if (node.Availability === KubernetesNodeAvailabilities.DRAIN) { + payload.metadata.labels[KubernetesPortainerNodeDrainLabel] = ''; + } else { + delete payload.metadata.labels[KubernetesPortainerNodeDrainLabel]; + } + } + return payload; } diff --git a/app/kubernetes/node/formValues.js b/app/kubernetes/node/formValues.js index d7227ce70..51e9cb43f 100644 --- a/app/kubernetes/node/formValues.js +++ b/app/kubernetes/node/formValues.js @@ -1,6 +1,7 @@ const _KubernetesNodeFormValues = Object.freeze({ Taints: [], Labels: [], + Availability: '', }); export class KubernetesNodeFormValues { diff --git a/app/kubernetes/node/models.js b/app/kubernetes/node/models.js index e8e54a727..86bf8da4a 100644 --- a/app/kubernetes/node/models.js +++ b/app/kubernetes/node/models.js @@ -1,3 +1,5 @@ +export const KubernetesPortainerNodeDrainLabel = 'io.portainer/node-status-drain'; + /** * KubernetesNode Model */ @@ -14,6 +16,7 @@ const _KubernetesNode = Object.freeze({ Api: false, Taints: [], Port: 0, + Availability: '', }); export class KubernetesNode { @@ -58,6 +61,12 @@ export class KubernetesNodeTaint { } } +export const KubernetesNodeAvailabilities = Object.freeze({ + ACTIVE: 'Active', + PAUSE: 'Pause', + DRAIN: 'Drain', +}); + export const KubernetesNodeTaintEffects = Object.freeze({ NOSCHEDULE: 'NoSchedule', PREFERNOSCHEDULE: 'PreferNoSchedule', diff --git a/app/kubernetes/node/service.js b/app/kubernetes/node/service.js index 36b81e481..e2d207f7b 100644 --- a/app/kubernetes/node/service.js +++ b/app/kubernetes/node/service.js @@ -57,7 +57,8 @@ class KubernetesNodeService { const newNode = KubernetesNodeConverter.formValuesToNode(node, nodeFormValues); const payload = KubernetesNodeConverter.patchPayload(node, newNode); const data = await this.KubernetesNodes().patch(params, payload).$promise; - return data; + const patchedNode = KubernetesNodeConverter.apiToNodeDetails(data); + return patchedNode; } catch (err) { throw { msg: 'Unable to patch node', err: err }; } diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js index a3b01b18c..0317b0901 100644 --- a/app/kubernetes/pod/converter.js +++ b/app/kubernetes/pod/converter.js @@ -10,7 +10,7 @@ import { } from 'Kubernetes/models/application/models'; import { createPayloadFactory } from './payloads/create'; -import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes } from './models'; +import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes, KubernetesPodEviction } from 'Kubernetes/pod/models'; function computeStatus(statuses) { const containerStatuses = _.map(statuses, 'state'); @@ -117,6 +117,13 @@ export default class KubernetesPodConverter { return res; } + static evictionPayload(pod) { + const res = new KubernetesPodEviction(); + res.metadata.name = pod.Name; + res.metadata.namespace = pod.Namespace; + return res; + } + static patchPayload(oldPod, newPod) { const oldPayload = createPayload(oldPod); const newPayload = createPayload(newPod); diff --git a/app/kubernetes/pod/models/index.js b/app/kubernetes/pod/models/index.js index a2590610f..d7a4950ea 100644 --- a/app/kubernetes/pod/models/index.js +++ b/app/kubernetes/pod/models/index.js @@ -65,6 +65,21 @@ export class KubernetesPodContainer { } } +const _KubernetesPodEviction = Object.freeze({ + apiVersion: 'policy/v1beta1', + kind: 'Eviction', + metadata: { + name: '', + namespace: '', + }, +}); + +export class KubernetesPodEviction { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodEviction))); + } +} + export const KubernetesPodContainerTypes = { INIT: 1, APP: 2, diff --git a/app/kubernetes/pod/service.js b/app/kubernetes/pod/service.js index af6359fb7..925b09797 100644 --- a/app/kubernetes/pod/service.js +++ b/app/kubernetes/pod/service.js @@ -2,7 +2,7 @@ import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; -import KubernetesPodConverter from './converter'; +import KubernetesPodConverter from 'Kubernetes/pod/converter'; class KubernetesPodService { /* @ngInject */ @@ -15,6 +15,7 @@ class KubernetesPodService { this.logsAsync = this.logsAsync.bind(this); this.deleteAsync = this.deleteAsync.bind(this); this.patchAsync = this.patchAsync.bind(this); + this.evictionAsync = this.evictionAsync.bind(this); } async getAsync(namespace, name) { @@ -116,6 +117,26 @@ class KubernetesPodService { delete(pod) { return this.$async(this.deleteAsync, pod); } + + /** + * EVICT + */ + async evictionAsync(pod) { + try { + const params = new KubernetesCommonParams(); + params.id = pod.Name; + params.action = 'eviction'; + const namespace = pod.Namespace; + const podEvictionPayload = KubernetesPodConverter.evictionPayload(pod); + await this.KubernetesPods(namespace).evict(params, podEvictionPayload).$promise; + } catch (err) { + throw new PortainerError('Unable to evict pod', err); + } + } + + eviction(pod) { + return this.$async(this.evictionAsync, pod); + } } export default KubernetesPodService; diff --git a/app/kubernetes/rest/pod.js b/app/kubernetes/rest/pod.js index 281b698ce..505558d41 100644 --- a/app/kubernetes/rest/pod.js +++ b/app/kubernetes/rest/pod.js @@ -42,6 +42,7 @@ angular.module('portainer.kubernetes').factory('KubernetesPods', [ params: { action: 'log' }, transformResponse: logsHandler, }, + evict: { method: 'POST' }, } ); }; diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html index 87ae50227..c4c9229c1 100644 --- a/app/kubernetes/views/cluster/node/node.html +++ b/app/kubernetes/views/cluster/node/node.html @@ -52,6 +52,26 @@ +