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

@ -13,7 +13,7 @@
<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>
<form class="form-horizontal" style="padding: 20px;">
<form class="form-horizontal" name="kubernetesNodeUpdateForm" style="padding: 20px;" autocomplete="off">
<table class="table">
<tbody ng-if="ctrl.node">
<tr>
@ -54,6 +54,7 @@
</tr>
</tbody>
</table>
<div style="padding: 8px;">
<kubernetes-resource-reservation
ng-if="ctrl.resourceReservation"
@ -65,6 +66,151 @@
>
</kubernetes-resource-reservation>
</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>
</uib-tab>
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">

View file

@ -3,6 +3,11 @@ import _ from 'lodash-es';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models';
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 {
/* @ngInject */
@ -11,6 +16,7 @@ class KubernetesNodeController {
$state,
Notifications,
LocalStorage,
ModalService,
KubernetesNodeService,
KubernetesEventService,
KubernetesPodService,
@ -21,6 +27,7 @@ class KubernetesNodeController {
this.$state = $state;
this.Notifications = Notifications;
this.LocalStorage = LocalStorage;
this.ModalService = ModalService;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesPodService = KubernetesPodService;
@ -33,12 +40,136 @@ class KubernetesNodeController {
this.getEventsAsync = this.getEventsAsync.bind(this);
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
this.updateNodeAsync = this.updateNodeAsync.bind(this);
}
selectTab(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() {
try {
const endpoints = await this.KubernetesEndpointService.get();
@ -63,6 +194,52 @@ class KubernetesNodeController {
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() {
try {
this.state.dataLoading = true;
@ -147,6 +324,10 @@ class KubernetesNodeController {
showEditorTab: false,
viewReady: false,
eventWarningCount: 0,
duplicateTaintKeys: [],
hasDuplicateTaintKeys: false,
duplicateLabelKeys: [],
hasDuplicateLabelKeys: false,
};
this.state.activeTab = this.LocalStorage.getActiveTab('node');
@ -156,6 +337,11 @@ class KubernetesNodeController {
await this.getApplications();
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;
}