1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(k8s/node): Add the ability to apply taints and labels to nodes (#4176)

* feat(node): Add the ability to apply taints and labels to nodes

* feat(k8s/node): minor UI update

* feat(k8s/node): UI update and disable system labels

* feat(k8s/node): minor UI update

* fix(node): fix add first taint

* refacto(node): add KubernetesNodeHelper

* feat(node): add used label to labels

* feat(node): add node update modals

* fix(node): modal when used label changes

* feat(k8s/node): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
Maxime Bajeux 2020-08-12 01:42:55 +02:00 committed by GitHub
parent 1f614ee95a
commit 1bf97426bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 568 additions and 8 deletions

View file

@ -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({

View file

@ -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)));
}
}

View file

@ -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;
});
}
}

View file

@ -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',
});

View file

@ -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)));
}
}

View file

@ -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' },
}
);

View file

@ -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;