mirror of
https://github.com/portainer/portainer.git
synced 2025-07-23 15:29:42 +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:
parent
1f614ee95a
commit
1bf97426bf
11 changed files with 568 additions and 8 deletions
|
@ -1,7 +1,10 @@
|
||||||
import _ from 'lodash-es';
|
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 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 {
|
class KubernetesNodeConverter {
|
||||||
static apiToNode(data, res) {
|
static apiToNode(data, res) {
|
||||||
|
@ -40,7 +43,13 @@ class KubernetesNodeConverter {
|
||||||
res.Version = data.status.nodeInfo.kubeletVersion;
|
res.Version = data.status.nodeInfo.kubeletVersion;
|
||||||
const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
|
const internalIP = _.find(data.status.addresses, { type: 'InternalIP' });
|
||||||
res.IPAddress = internalIP ? internalIP.address : '-';
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +63,82 @@ class KubernetesNodeConverter {
|
||||||
res.Yaml = yaml ? yaml.data : '';
|
res.Yaml = yaml ? yaml.data : '';
|
||||||
return res;
|
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({
|
export const KubernetesNodeConditionTypes = Object.freeze({
|
||||||
|
|
40
app/kubernetes/node/formValues.js
Normal file
40
app/kubernetes/node/formValues.js
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
23
app/kubernetes/node/helper.js
Normal file
23
app/kubernetes/node/helper.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,3 +42,24 @@ export class KubernetesNodeDetails {
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(_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',
|
||||||
|
});
|
||||||
|
|
31
app/kubernetes/node/payload.js
Normal file
31
app/kubernetes/node/payload.js
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesNodes', [
|
||||||
},
|
},
|
||||||
create: { method: 'POST' },
|
create: { method: 'POST' },
|
||||||
update: { method: 'PUT' },
|
update: { method: 'PUT' },
|
||||||
|
patch: {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
},
|
||||||
delete: { method: 'DELETE' },
|
delete: { method: 'DELETE' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,6 +13,7 @@ class KubernetesNodeService {
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
this.getAllAsync = this.getAllAsync.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);
|
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;
|
export default KubernetesNodeService;
|
||||||
|
|
|
@ -17,10 +17,10 @@ function computeTolerations(nodes, application) {
|
||||||
}
|
}
|
||||||
n.UnmetTaints = [];
|
n.UnmetTaints = [];
|
||||||
_.forEach(n.Taints, (t) => {
|
_.forEach(n.Taints, (t) => {
|
||||||
const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: t.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 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 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 matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.Key, Operator: 'Exists', Effect: '' });
|
||||||
const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' });
|
const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' });
|
||||||
|
|
||||||
if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) {
|
if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) {
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
<!-- ADMIN + UNMET TAINTS -->
|
<!-- ADMIN + UNMET TAINTS -->
|
||||||
<tr ng-if="$ctrl.isAdmin" ng-show="item.Expanded" ng-repeat="taint in item.UnmetTaints" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
<tr ng-if="$ctrl.isAdmin" ng-show="item.Expanded" ng-repeat="taint in item.UnmetTaints" ng-style="{ background: item.Highlighted ? '#d5e8f3' : '#f5f5f5' }">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
This application is missing a toleration for the taint <code>{{ taint.key }}{{ taint.value ? '=' + taint.value : '' }}:{{ taint.effect }}</code>
|
This application is missing a toleration for the taint <code>{{ taint.Key }}{{ taint.Value ? '=' + taint.Value : '' }}:{{ taint.effect }}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- !ADMIN + UNMET TAINTS -->
|
<!-- !ADMIN + UNMET TAINTS -->
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||||
<uib-tab-heading> <i class="fa fa-hdd space-right" aria-hidden="true"></i> Node </uib-tab-heading>
|
<uib-tab-heading> <i class="fa fa-hdd space-right" aria-hidden="true"></i> Node </uib-tab-heading>
|
||||||
|
|
||||||
<form class="form-horizontal" style="padding: 20px;">
|
<form class="form-horizontal" name="kubernetesNodeUpdateForm" style="padding: 20px;" autocomplete="off">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody ng-if="ctrl.node">
|
<tbody ng-if="ctrl.node">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -54,6 +54,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div style="padding: 8px;">
|
<div style="padding: 8px;">
|
||||||
<kubernetes-resource-reservation
|
<kubernetes-resource-reservation
|
||||||
ng-if="ctrl.resourceReservation"
|
ng-if="ctrl.resourceReservation"
|
||||||
|
@ -65,6 +66,151 @@
|
||||||
>
|
>
|
||||||
</kubernetes-resource-reservation>
|
</kubernetes-resource-reservation>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<!-- #region labels -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Labels
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<span class="label label-default interactive" ng-click="ctrl.addLabel()"> <i class="fa fa-plus-circle" aria-hidden="true"></i> add label </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline" ng-repeat="label in ctrl.formValues.Labels" style="padding: 3px 0 3px 0;">
|
||||||
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: label.NeedsDeletion }">
|
||||||
|
<span class="input-group-addon">Key</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="label_key_{{ $index }}"
|
||||||
|
ng-model="label.Key"
|
||||||
|
ng-change="ctrl.onChangeLabelKey($index)"
|
||||||
|
ng-disabled="ctrl.isSystemLabel($index)"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: label.NeedsDeletion }">
|
||||||
|
<span class="input-group-addon">Value</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="label_value_{{ $index }}"
|
||||||
|
ng-change="ctrl.onChangeLabel($index)"
|
||||||
|
ng-model="label.Value"
|
||||||
|
ng-disabled="ctrl.isSystemLabel($index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
|
<div style="white-space: nowrap;">
|
||||||
|
<button ng-if="!ctrl.isSystemLabel($index) && !label.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeLabel($index)">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button ng-if="!ctrl.isSystemLabel($index) && label.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreLabel($index)">
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
<span class="label label-warning label-sm image-tag" ng-if="label.IsUsed && !ctrl.isSystemLabel($index)" style="margin-left: 5px;">used</span>
|
||||||
|
<span class="label label-info image-tag" ng-if="ctrl.isSystemLabel($index)" style="margin-left: 5px;">system</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-show="kubernetesNodeUpdateForm['label_key_' + $index].$invalid || ctrl.state.duplicateLabelKeys[$index] !== undefined"
|
||||||
|
>
|
||||||
|
<ng-messages for="kubernetesNodeUpdateForm['label_key_' + $index].$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Label key is required.</p>
|
||||||
|
</ng-messages>
|
||||||
|
<p ng-if="ctrl.state.duplicateLabelKeys[$index] !== undefined"
|
||||||
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This label key is already defined.</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region taints -->
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title" style="margin-top: 20px;">
|
||||||
|
Taints
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<span class="label label-default interactive" ng-click="ctrl.addTaint()"> <i class="fa fa-plus-circle" aria-hidden="true"></i> add taint </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline" ng-repeat="taint in ctrl.formValues.Taints" style="padding: 3px 0 3px 0;">
|
||||||
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: taint.NeedsDeletion }">
|
||||||
|
<span class="input-group-addon">Key</span>
|
||||||
|
<input type="text" class="form-control" name="taint_key_{{ $index }}" ng-model="taint.Key" ng-change="ctrl.onChangeTaintKey($index)" required />
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: taint.NeedsDeletion }">
|
||||||
|
<span class="input-group-addon">Value</span>
|
||||||
|
<input type="text" class="form-control" name="taint_value_{{ $index }}" ng-model="taint.Value" ng-change="ctrl.onChangeTaint($index)" />
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: taint.NeedsDeletion }">
|
||||||
|
<span class="input-group-addon">Effect</span>
|
||||||
|
<select
|
||||||
|
id="taint_effect_{{ $index }}"
|
||||||
|
name="taint_effect_{{ $index }}"
|
||||||
|
class="form-control"
|
||||||
|
ng-model="taint.Effect"
|
||||||
|
ng-change="ctrl.onChangeTaint($index)"
|
||||||
|
;
|
||||||
|
ng-options="effect as effect for effect in ctrl.availableEffects"
|
||||||
|
></select>
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
|
<div>
|
||||||
|
<button ng-if="!taint.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeTaint($index)">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button ng-if="taint.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreTaint($index)">
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
style="margin-top: 5px;"
|
||||||
|
ng-show="kubernetesNodeUpdateForm['taint_key_' + $index].$invalid || ctrl.state.duplicateTaintKeys[$index] !== undefined"
|
||||||
|
>
|
||||||
|
<ng-messages for="kubernetesNodeUpdateForm['taint_key_' + $index].$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Taint key is required.</p>
|
||||||
|
</ng-messages>
|
||||||
|
<p ng-if="ctrl.state.duplicateTaintKeys[$index] !== undefined"
|
||||||
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This taint key is already defined.</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- #region actions -->
|
||||||
|
|
||||||
|
<div class="col-sm-12 form-section-title" style="margin-top: 20px;">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
style="margin-left: 0;"
|
||||||
|
ng-click="ctrl.updateNode()"
|
||||||
|
ng-disabled="kubernetesNodeUpdateForm.$invalid || !ctrl.isFormValid()"
|
||||||
|
>
|
||||||
|
Update node
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default btn-sm" type="button" ng-click="ctrl.resetFormValues()" ng-disabled="ctrl.isNoChangesMade()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- #endregion -->
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||||
|
|
|
@ -3,6 +3,11 @@ import _ from 'lodash-es';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
|
import KubernetesNodeConverter from 'Kubernetes/node/converter';
|
||||||
|
import { KubernetesNodeLabelFormValues, KubernetesNodeTaintFormValues } from 'Kubernetes/node/formValues';
|
||||||
|
import { KubernetesNodeTaintEffects } from 'Kubernetes/node/models';
|
||||||
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
|
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
||||||
|
|
||||||
class KubernetesNodeController {
|
class KubernetesNodeController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -11,6 +16,7 @@ class KubernetesNodeController {
|
||||||
$state,
|
$state,
|
||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
|
ModalService,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
KubernetesEventService,
|
KubernetesEventService,
|
||||||
KubernetesPodService,
|
KubernetesPodService,
|
||||||
|
@ -21,6 +27,7 @@ class KubernetesNodeController {
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
|
this.ModalService = ModalService;
|
||||||
this.KubernetesNodeService = KubernetesNodeService;
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
this.KubernetesEventService = KubernetesEventService;
|
this.KubernetesEventService = KubernetesEventService;
|
||||||
this.KubernetesPodService = KubernetesPodService;
|
this.KubernetesPodService = KubernetesPodService;
|
||||||
|
@ -33,12 +40,136 @@ class KubernetesNodeController {
|
||||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||||
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
||||||
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
||||||
|
this.updateNodeAsync = this.updateNodeAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectTab(index) {
|
selectTab(index) {
|
||||||
this.LocalStorage.storeActiveTab('node', index);
|
this.LocalStorage.storeActiveTab('node', index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* #region taint */
|
||||||
|
|
||||||
|
onChangeTaintKey(index) {
|
||||||
|
this.state.duplicateTaintKeys = KubernetesFormValidationHelper.getDuplicates(
|
||||||
|
_.map(this.formValues.Taints, (taint) => {
|
||||||
|
if (taint.NeedsDeletion) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return taint.Key;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.state.hasDuplicateTaintKeys = Object.keys(this.state.duplicateTaintKeys).length > 0;
|
||||||
|
this.onChangeTaint(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeTaint(index) {
|
||||||
|
if (this.formValues.Taints[index]) {
|
||||||
|
this.formValues.Taints[index].IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTaint() {
|
||||||
|
const taint = new KubernetesNodeTaintFormValues();
|
||||||
|
taint.IsNew = true;
|
||||||
|
taint.Effect = KubernetesNodeTaintEffects.NOSCHEDULE;
|
||||||
|
this.formValues.Taints.push(taint);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTaint(index) {
|
||||||
|
const taint = this.formValues.Taints[index];
|
||||||
|
if (taint.IsNew) {
|
||||||
|
this.formValues.Taints.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
taint.NeedsDeletion = true;
|
||||||
|
}
|
||||||
|
this.onChangeTaintKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreTaint(index) {
|
||||||
|
this.formValues.Taints[index].NeedsDeletion = false;
|
||||||
|
this.onChangeTaintKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTaintsWarning() {
|
||||||
|
return _.filter(this.formValues.Taints, (taint) => {
|
||||||
|
return taint.Effect === KubernetesNodeTaintEffects.NOEXECUTE && (taint.IsNew || taint.IsChanged);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region label */
|
||||||
|
|
||||||
|
onChangeLabelKey(index) {
|
||||||
|
this.state.duplicateLabelKeys = KubernetesFormValidationHelper.getDuplicates(
|
||||||
|
_.map(this.formValues.Labels, (label) => {
|
||||||
|
if (label.NeedsDeletion) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return label.Key;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.state.hasDuplicateLabelKeys = Object.keys(this.state.duplicateLabelKeys).length > 0;
|
||||||
|
this.onChangeLabel(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeLabel(index) {
|
||||||
|
if (this.formValues.Labels[index]) {
|
||||||
|
this.formValues.Labels[index].IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addLabel() {
|
||||||
|
const label = new KubernetesNodeLabelFormValues();
|
||||||
|
label.IsNew = true;
|
||||||
|
this.formValues.Labels.push(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLabel(index) {
|
||||||
|
const label = this.formValues.Labels[index];
|
||||||
|
if (label.IsNew) {
|
||||||
|
this.formValues.Labels.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
label.NeedsDeletion = true;
|
||||||
|
}
|
||||||
|
this.onChangeLabelKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreLabel(index) {
|
||||||
|
this.formValues.Labels[index].NeedsDeletion = false;
|
||||||
|
this.onChangeLabelKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSystemLabel(index) {
|
||||||
|
return KubernetesNodeHelper.isSystemLabel(this.formValues.Labels[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
computeLabelsWarning() {
|
||||||
|
return _.filter(this.formValues.Labels, (label) => {
|
||||||
|
return label.IsUsed && (label.NeedsDeletion || label.IsChanged);
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region actions */
|
||||||
|
|
||||||
|
isNoChangesMade() {
|
||||||
|
const newNode = KubernetesNodeConverter.formValuesToNode(this.node, this.formValues);
|
||||||
|
const payload = KubernetesNodeConverter.patchPayload(this.node, newNode);
|
||||||
|
return !payload.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
return !this.state.hasDuplicateTaintKeys && !this.state.hasDuplicateLabelKeys && !this.isNoChangesMade();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFormValues() {
|
||||||
|
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
async getEndpointsAsync() {
|
async getEndpointsAsync() {
|
||||||
try {
|
try {
|
||||||
const endpoints = await this.KubernetesEndpointService.get();
|
const endpoints = await this.KubernetesEndpointService.get();
|
||||||
|
@ -63,6 +194,52 @@ class KubernetesNodeController {
|
||||||
return this.$async(this.getEndpointsAsync);
|
return this.$async(this.getEndpointsAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateNodeAsync() {
|
||||||
|
try {
|
||||||
|
await this.KubernetesNodeService.patch(this.node, this.formValues);
|
||||||
|
this.Notifications.success('Node updated successfully');
|
||||||
|
this.$state.reload();
|
||||||
|
} catch (err) {
|
||||||
|
this.Notifications.error('Failure', err, 'Unable to update node');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNode() {
|
||||||
|
const taintsWarning = this.computeTaintsWarning();
|
||||||
|
const labelsWarning = this.computeLabelsWarning();
|
||||||
|
|
||||||
|
if (taintsWarning && !labelsWarning) {
|
||||||
|
this.ModalService.confirmUpdate(
|
||||||
|
'Changes to taints will immediately deschedule applications running on this node without the corresponding tolerations. Do you wish to continue?',
|
||||||
|
(confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
return this.$async(this.updateNodeAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (!taintsWarning && labelsWarning) {
|
||||||
|
this.ModalService.confirmUpdate(
|
||||||
|
'Removing or changing a label that is used might prevent applications from being scheduled on this node in the future. Do you wish to continue?',
|
||||||
|
(confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
return this.$async(this.updateNodeAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (taintsWarning && labelsWarning) {
|
||||||
|
this.ModalService.confirmUpdate(
|
||||||
|
'Changes to taints will immediately deschedule applications running on this node without the corresponding tolerations.<br/></br/>Removing or changing a label that is used might prevent applications from scheduling on this node in the future.\n\nDo you wish to continue?',
|
||||||
|
(confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
return this.$async(this.updateNodeAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.$async(this.updateNodeAsync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getNodeAsync() {
|
async getNodeAsync() {
|
||||||
try {
|
try {
|
||||||
this.state.dataLoading = true;
|
this.state.dataLoading = true;
|
||||||
|
@ -147,6 +324,10 @@ class KubernetesNodeController {
|
||||||
showEditorTab: false,
|
showEditorTab: false,
|
||||||
viewReady: false,
|
viewReady: false,
|
||||||
eventWarningCount: 0,
|
eventWarningCount: 0,
|
||||||
|
duplicateTaintKeys: [],
|
||||||
|
hasDuplicateTaintKeys: false,
|
||||||
|
duplicateLabelKeys: [],
|
||||||
|
hasDuplicateLabelKeys: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.activeTab = this.LocalStorage.getActiveTab('node');
|
this.state.activeTab = this.LocalStorage.getActiveTab('node');
|
||||||
|
@ -156,6 +337,11 @@ class KubernetesNodeController {
|
||||||
await this.getApplications();
|
await this.getApplications();
|
||||||
await this.getEndpoints();
|
await this.getEndpoints();
|
||||||
|
|
||||||
|
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
||||||
|
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
||||||
|
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
||||||
|
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
||||||
|
|
||||||
this.state.viewReady = true;
|
this.state.viewReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue