diff --git a/app/kubernetes/node/converter.js b/app/kubernetes/node/converter.js index ecc8a70a8..cc57e5c90 100644 --- a/app/kubernetes/node/converter.js +++ b/app/kubernetes/node/converter.js @@ -1,7 +1,10 @@ import _ from 'lodash-es'; -import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/node/models'; +import { KubernetesNode, KubernetesNodeDetails, KubernetesNodeTaint } 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'; +import * as JsonPatch from 'fast-json-patch'; class KubernetesNodeConverter { static apiToNode(data, res) { @@ -40,7 +43,13 @@ class KubernetesNodeConverter { res.Version = data.status.nodeInfo.kubeletVersion; const internalIP = _.find(data.status.addresses, { type: 'InternalIP' }); res.IPAddress = internalIP ? internalIP.address : '-'; - res.Taints = data.spec.taints ? data.spec.taints : []; + res.Taints = _.map(data.spec.taints, (taint) => { + const res = new KubernetesNodeTaint(); + res.Key = taint.key; + res.Value = taint.value; + res.Effect = taint.effect; + return res; + }); return res; } @@ -54,6 +63,82 @@ class KubernetesNodeConverter { res.Yaml = yaml ? yaml.data : ''; return res; } + + static nodeToFormValues(node) { + const res = new KubernetesNodeFormValues(); + + res.Taints = _.map(node.Taints, (taint) => { + const res = new KubernetesNodeTaintFormValues(); + res.Key = taint.Key; + res.Value = taint.Value; + res.Effect = taint.Effect; + res.NeedsDeletion = false; + res.IsNew = false; + return res; + }); + + res.Labels = _.map(node.Labels, (value, key) => { + const res = new KubernetesNodeLabelFormValues(); + res.Key = key; + res.Value = value; + res.NeedsDeletion = false; + res.IsNew = false; + return res; + }); + + return res; + } + + static formValuesToNode(node, formValues) { + const res = angular.copy(node); + + const filteredTaints = _.filter(formValues.Taints, (taint) => !taint.NeedsDeletion); + res.Taints = _.map(filteredTaints, (item) => { + const taint = new KubernetesNodeTaint(); + taint.Key = item.Key; + taint.Value = item.Value; + taint.Effect = item.Effect; + return taint; + }); + + const filteredLabels = _.filter(formValues.Labels, (label) => !label.NeedsDeletion); + res.Labels = _.reduce( + filteredLabels, + (acc, item) => { + acc[item.Key] = item.Value ? item.Value : ''; + return acc; + }, + {} + ); + + return res; + } + + static createPayload(node) { + const payload = new KubernetesNodeCreatePayload(); + payload.metadata.name = node.Name; + + const taints = _.map(node.Taints, (taint) => { + const res = new KubernetesNodeTaintPayload(); + res.key = taint.Key; + res.value = taint.Value; + res.effect = taint.Effect; + return res; + }); + + payload.spec.taints = taints.length ? taints : undefined; + + payload.metadata.labels = node.Labels; + + return payload; + } + + static patchPayload(oldNode, newNode) { + const oldPayload = KubernetesNodeConverter.createPayload(oldNode); + const newPayload = KubernetesNodeConverter.createPayload(newNode); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } } export const KubernetesNodeConditionTypes = Object.freeze({ diff --git a/app/kubernetes/node/formValues.js b/app/kubernetes/node/formValues.js new file mode 100644 index 000000000..d7227ce70 --- /dev/null +++ b/app/kubernetes/node/formValues.js @@ -0,0 +1,40 @@ +const _KubernetesNodeFormValues = Object.freeze({ + Taints: [], + Labels: [], +}); + +export class KubernetesNodeFormValues { + constructor() { + Object.assign(JSON.parse(JSON.stringify(_KubernetesNodeFormValues))); + } +} + +const _KubernetesNodeTaintFormValues = Object.freeze({ + Key: '', + Values: '', + Effect: '', + NeedsDeletion: false, + IsNew: false, + IsChanged: false, +}); + +export class KubernetesNodeTaintFormValues { + constructor() { + Object.assign(JSON.parse(JSON.stringify(_KubernetesNodeTaintFormValues))); + } +} + +const _KubernetesNodeLabelFormValues = Object.freeze({ + Key: '', + Values: '', + NeedsDeletion: false, + IsNew: false, + IsUsed: false, + IsChanged: false, +}); + +export class KubernetesNodeLabelFormValues { + constructor() { + Object.assign(JSON.parse(JSON.stringify(_KubernetesNodeLabelFormValues))); + } +} diff --git a/app/kubernetes/node/helper.js b/app/kubernetes/node/helper.js new file mode 100644 index 000000000..57eaa583f --- /dev/null +++ b/app/kubernetes/node/helper.js @@ -0,0 +1,23 @@ +import _ from 'lodash-es'; + +export class KubernetesNodeHelper { + static isSystemLabel(label) { + return !label.IsNew && (_.startsWith(label.Key, 'beta.kubernetes.io') || _.startsWith(label.Key, 'kubernetes.io') || label.Key === 'node-role.kubernetes.io/master'); + } + + static reorderLabels(labels) { + return _.sortBy(labels, (label) => { + return !KubernetesNodeHelper.isSystemLabel(label); + }); + } + + static computeUsedLabels(applications, labels) { + const pods = _.flatten(_.map(applications, 'Pods')); + const nodeSelectors = _.uniq(_.flatten(_.map(pods, 'NodeSelector'))); + + return _.map(labels, (label) => { + label.IsUsed = _.find(nodeSelectors, (ns) => ns && ns[label.Key] === label.Value) ? true : false; + return label; + }); + } +} diff --git a/app/kubernetes/node/models.js b/app/kubernetes/node/models.js index f1c547969..e8e54a727 100644 --- a/app/kubernetes/node/models.js +++ b/app/kubernetes/node/models.js @@ -42,3 +42,24 @@ export class KubernetesNodeDetails { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeDetails))); } } + +/** + * KubernetesNodeTaint Model + */ +const _KubernetesNodeTaint = Object.freeze({ + Key: '', + Value: '', + Effect: '', +}); + +export class KubernetesNodeTaint { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeTaint))); + } +} + +export const KubernetesNodeTaintEffects = Object.freeze({ + NOSCHEDULE: 'NoSchedule', + PREFERNOSCHEDULE: 'PreferNoSchedule', + NOEXECUTE: 'NoExecute', +}); diff --git a/app/kubernetes/node/payload.js b/app/kubernetes/node/payload.js new file mode 100644 index 000000000..fbd974f0e --- /dev/null +++ b/app/kubernetes/node/payload.js @@ -0,0 +1,31 @@ +/** + * KubernetesNode Create Payload Model + * Note: The current payload is here just to create patch payload. + */ +const _KubernetesNodeCreatePayload = Object.freeze({ + metadata: { + name: '', + labels: {}, + }, + spec: { + taints: undefined, + }, +}); + +export class KubernetesNodeCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeCreatePayload))); + } +} + +const _KubernetesNodeTaintPayload = Object.freeze({ + key: '', + value: '', + effect: '', +}); + +export class KubernetesNodeTaintPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeTaintPayload))); + } +} diff --git a/app/kubernetes/node/rest.js b/app/kubernetes/node/rest.js index 5e27a53eb..a145ae017 100644 --- a/app/kubernetes/node/rest.js +++ b/app/kubernetes/node/rest.js @@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesNodes', [ }, create: { method: 'POST' }, update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, delete: { method: 'DELETE' }, } ); diff --git a/app/kubernetes/node/service.js b/app/kubernetes/node/service.js index 30aa9157c..36b81e481 100644 --- a/app/kubernetes/node/service.js +++ b/app/kubernetes/node/service.js @@ -13,6 +13,7 @@ class KubernetesNodeService { this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); } /** @@ -44,6 +45,27 @@ class KubernetesNodeService { } return this.$async(this.getAllAsync); } + + /** + * PATCH + */ + + async patchAsync(node, nodeFormValues) { + try { + const params = new KubernetesCommonParams(); + params.id = node.Name; + const newNode = KubernetesNodeConverter.formValuesToNode(node, nodeFormValues); + const payload = KubernetesNodeConverter.patchPayload(node, newNode); + const data = await this.KubernetesNodes().patch(params, payload).$promise; + return data; + } catch (err) { + throw { msg: 'Unable to patch node', err: err }; + } + } + + patch(node, nodeFormValues) { + return this.$async(this.patchAsync, node, nodeFormValues); + } } export default KubernetesNodeService; diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 0cfa9d206..e7c957135 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -17,10 +17,10 @@ function computeTolerations(nodes, application) { } n.UnmetTaints = []; _.forEach(n.Taints, (t) => { - const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: t.effect }); - const matchKeyAnyValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: t.effect }); - const matchKeyMatchValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: '' }); - const matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: '' }); + const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Equal', Value: t.Value, Effect: t.Effect }); + const matchKeyAnyValueMatchEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Exists', Effect: t.Effect }); + const matchKeyMatchValueAnyEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Equal', Value: t.Value, Effect: '' }); + const matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Exists', Effect: '' }); const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' }); if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) { diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html index 6b85f9e0c..c2659f1e6 100644 --- a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html @@ -94,7 +94,7 @@
{{ taint.key }}{{ taint.value ? '=' + taint.value : '' }}:{{ taint.effect }}
+ This application is missing a toleration for the taint {{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.effect }}