mirror of https://github.com/portainer/portainer
463 lines
15 KiB
JavaScript
463 lines
15 KiB
JavaScript
import angular from 'angular';
|
|
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, KubernetesNodeAvailabilities } from 'Kubernetes/node/models';
|
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
|
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
|
|
import { confirmUpdate } from '@@/modals/confirm';
|
|
|
|
class KubernetesNodeController {
|
|
/* @ngInject */
|
|
constructor(
|
|
$async,
|
|
$state,
|
|
Notifications,
|
|
LocalStorage,
|
|
KubernetesNodeService,
|
|
KubernetesEventService,
|
|
KubernetesPodService,
|
|
KubernetesApplicationService,
|
|
KubernetesEndpointService,
|
|
KubernetesMetricsService,
|
|
Authentication
|
|
) {
|
|
this.$async = $async;
|
|
this.$state = $state;
|
|
this.Notifications = Notifications;
|
|
this.LocalStorage = LocalStorage;
|
|
this.KubernetesNodeService = KubernetesNodeService;
|
|
this.KubernetesEventService = KubernetesEventService;
|
|
this.KubernetesPodService = KubernetesPodService;
|
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
|
this.KubernetesEndpointService = KubernetesEndpointService;
|
|
this.KubernetesMetricsService = KubernetesMetricsService;
|
|
this.Authentication = Authentication;
|
|
|
|
this.onInit = this.onInit.bind(this);
|
|
this.getNodesAsync = this.getNodesAsync.bind(this);
|
|
this.getEvents = this.getEvents.bind(this);
|
|
this.getEventsAsync = this.getEventsAsync.bind(this);
|
|
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
|
|
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
|
this.updateNodeAsync = this.updateNodeAsync.bind(this);
|
|
this.drainNodeAsync = this.drainNodeAsync.bind(this);
|
|
this.hasResourceUsageAccess = this.hasResourceUsageAccess.bind(this);
|
|
this.getNodeUsageAsync = this.getNodeUsageAsync.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 cordon */
|
|
|
|
computeCordonWarning() {
|
|
return this.formValues.Availability === this.availabilities.PAUSE;
|
|
}
|
|
|
|
/* #endregion */
|
|
|
|
/* #region drain */
|
|
|
|
computeDrainWarning() {
|
|
return this.formValues.Availability === this.availabilities.DRAIN;
|
|
}
|
|
|
|
async drainNodeAsync() {
|
|
const pods = _.flatten(_.map(this.applications, (app) => app.Pods));
|
|
let actionCount = pods.length;
|
|
for (const pod of pods) {
|
|
try {
|
|
await this.KubernetesPodService.eviction(pod);
|
|
this.Notifications.success('Pod successfully evicted', pod.Name);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to evict pod');
|
|
this.formValues.Availability = this.availabilities.PAUSE;
|
|
await this.KubernetesNodeService.patch(this.node, this.formValues);
|
|
} finally {
|
|
--actionCount;
|
|
if (actionCount === 0) {
|
|
this.formValues.Availability = this.availabilities.PAUSE;
|
|
await this.KubernetesNodeService.patch(this.node, this.formValues);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drainNode() {
|
|
return this.$async(this.drainNodeAsync);
|
|
}
|
|
|
|
/* #endregion */
|
|
|
|
/* #region actions */
|
|
|
|
isNoChangesMade() {
|
|
const newNode = KubernetesNodeConverter.formValuesToNode(this.node, this.formValues);
|
|
const payload = KubernetesNodeConverter.patchPayload(this.node, newNode);
|
|
return !payload.length;
|
|
}
|
|
|
|
isDrainError() {
|
|
return (this.state.isDrainOperation || this.state.isContainPortainer) && this.formValues.Availability === this.availabilities.DRAIN;
|
|
}
|
|
|
|
isFormValid() {
|
|
return !this.state.hasDuplicateTaintKeys && !this.state.hasDuplicateLabelKeys && !this.isNoChangesMade() && !this.isDrainError();
|
|
}
|
|
|
|
resetFormValues() {
|
|
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
|
}
|
|
|
|
/* #endregion */
|
|
|
|
async getEndpointsAsync() {
|
|
try {
|
|
const endpoints = await this.KubernetesEndpointService.get();
|
|
this.endpoint = _.find(endpoints, { Name: 'kubernetes' });
|
|
if (this.endpoint && this.endpoint.Subsets) {
|
|
_.forEach(this.endpoint.Subsets, (subset) => {
|
|
return _.forEach(subset.Ips, (ip) => {
|
|
if (ip === this.node.IPAddress) {
|
|
this.node.Api = true;
|
|
this.node.Port = subset.Port;
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve environments');
|
|
}
|
|
}
|
|
|
|
getEndpoints() {
|
|
return this.$async(this.getEndpointsAsync);
|
|
}
|
|
|
|
async updateNodeAsync() {
|
|
try {
|
|
this.node = await this.KubernetesNodeService.patch(this.node, this.formValues);
|
|
if (this.formValues.Availability === 'Drain') {
|
|
await this.drainNode();
|
|
}
|
|
this.Notifications.success('Success', 'Node updated successfully');
|
|
this.$state.reload(this.$state.current);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to update node');
|
|
}
|
|
}
|
|
|
|
updateNode() {
|
|
const taintsWarning = this.computeTaintsWarning();
|
|
const labelsWarning = this.computeLabelsWarning();
|
|
const cordonWarning = this.computeCordonWarning();
|
|
const drainWarning = this.computeDrainWarning();
|
|
|
|
if (taintsWarning && !labelsWarning) {
|
|
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) {
|
|
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) {
|
|
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 if (cordonWarning) {
|
|
confirmUpdate(
|
|
'Marking this node as unschedulable will effectively cordon the node and prevent any new workload from being scheduled on that node. Are you sure?',
|
|
(confirmed) => {
|
|
if (confirmed) {
|
|
return this.$async(this.updateNodeAsync);
|
|
}
|
|
}
|
|
);
|
|
} else if (drainWarning) {
|
|
confirmUpdate('Draining this node will cause all workloads to be evicted from that node. This might lead to some service interruption. Are you sure?', (confirmed) => {
|
|
if (confirmed) {
|
|
return this.$async(this.updateNodeAsync);
|
|
}
|
|
});
|
|
} else {
|
|
return this.$async(this.updateNodeAsync);
|
|
}
|
|
}
|
|
|
|
async getNodesAsync() {
|
|
try {
|
|
this.state.dataLoading = true;
|
|
const nodeName = this.$transition$.params().name;
|
|
this.nodes = await this.KubernetesNodeService.get();
|
|
this.node = _.find(this.nodes, { Name: nodeName });
|
|
this.state.isDrainOperation = _.find(this.nodes, { Availability: this.availabilities.DRAIN });
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve node');
|
|
} finally {
|
|
this.state.dataLoading = false;
|
|
}
|
|
}
|
|
|
|
getNodes() {
|
|
return this.$async(this.getNodesAsync);
|
|
}
|
|
|
|
hasResourceUsageAccess() {
|
|
return this.state.isAdmin && this.state.useServerMetrics;
|
|
}
|
|
|
|
async getNodeUsageAsync() {
|
|
try {
|
|
const nodeName = this.$transition$.params().name;
|
|
const node = await this.KubernetesMetricsService.getNode(nodeName);
|
|
this.resourceUsage = new KubernetesResourceReservation();
|
|
this.resourceUsage.CPU = KubernetesResourceReservationHelper.parseCPU(node.usage.cpu);
|
|
this.resourceUsage.Memory = KubernetesResourceReservationHelper.megaBytesValue(node.usage.memory);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve node resource usage');
|
|
}
|
|
}
|
|
|
|
getNodeUsage() {
|
|
return this.$async(this.getNodeUsageAsync);
|
|
}
|
|
|
|
hasEventWarnings() {
|
|
return this.state.eventWarningCount;
|
|
}
|
|
|
|
async getEventsAsync() {
|
|
try {
|
|
this.state.eventsLoading = true;
|
|
const events = await this.KubernetesEventService.get();
|
|
this.events = events.filter((item) => item.Involved.kind === 'Node');
|
|
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve node events');
|
|
} finally {
|
|
this.state.eventsLoading = false;
|
|
}
|
|
}
|
|
|
|
getEvents() {
|
|
return this.$async(this.getEventsAsync);
|
|
}
|
|
|
|
showEditor() {
|
|
this.state.showEditorTab = true;
|
|
this.selectTab(2);
|
|
}
|
|
|
|
async getApplicationsAsync() {
|
|
try {
|
|
this.state.applicationsLoading = true;
|
|
this.applications = await this.KubernetesApplicationService.get();
|
|
|
|
this.resourceReservation = new KubernetesResourceReservation();
|
|
this.applications = _.map(this.applications, (app) => {
|
|
app.Pods = _.filter(app.Pods, (pod) => pod.Node === this.node.Name);
|
|
return app;
|
|
});
|
|
this.applications = _.filter(this.applications, (app) => app.Pods.length !== 0);
|
|
this.applications = _.map(this.applications, (app) => {
|
|
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
|
|
app.CPU = resourceReservation.CPU;
|
|
app.Memory = resourceReservation.Memory;
|
|
this.resourceReservation.CPU += resourceReservation.CPU;
|
|
this.resourceReservation.Memory += resourceReservation.Memory;
|
|
return app;
|
|
});
|
|
this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory);
|
|
this.memoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory);
|
|
this.state.isContainPortainer = _.find(this.applications, { ApplicationName: 'portainer' });
|
|
|
|
if (this.hasResourceUsageAccess()) {
|
|
await this.getNodeUsage();
|
|
}
|
|
} catch (err) {
|
|
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
|
|
} finally {
|
|
this.state.applicationsLoading = false;
|
|
}
|
|
}
|
|
|
|
getApplications() {
|
|
return this.$async(this.getApplicationsAsync);
|
|
}
|
|
|
|
async onInit() {
|
|
this.availabilities = KubernetesNodeAvailabilities;
|
|
|
|
this.state = {
|
|
isAdmin: this.Authentication.isAdmin(),
|
|
activeTab: this.LocalStorage.getActiveTab('node'),
|
|
currentName: this.$state.$current.name,
|
|
dataLoading: true,
|
|
eventsLoading: true,
|
|
applicationsLoading: true,
|
|
showEditorTab: false,
|
|
viewReady: false,
|
|
eventWarningCount: 0,
|
|
duplicateTaintKeys: [],
|
|
hasDuplicateTaintKeys: false,
|
|
duplicateLabelKeys: [],
|
|
hasDuplicateLabelKeys: false,
|
|
isDrainOperation: false,
|
|
isContainPortainer: false,
|
|
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
|
};
|
|
|
|
await this.getNodes();
|
|
await this.getEvents();
|
|
await this.getApplications();
|
|
await this.getEndpoints();
|
|
|
|
this.availableEffects = _.values(KubernetesNodeTaintEffects);
|
|
this.formValues = KubernetesNodeConverter.nodeToFormValues(this.node);
|
|
this.formValues.Labels = KubernetesNodeHelper.computeUsedLabels(this.applications, this.formValues.Labels);
|
|
this.formValues.Labels = KubernetesNodeHelper.reorderLabels(this.formValues.Labels);
|
|
|
|
this.state.viewReady = true;
|
|
}
|
|
|
|
$onInit() {
|
|
return this.$async(this.onInit);
|
|
}
|
|
|
|
$onDestroy() {
|
|
if (this.state.currentName !== this.$state.$current.name) {
|
|
this.LocalStorage.storeActiveTab('node', 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default KubernetesNodeController;
|
|
angular.module('portainer.kubernetes').controller('KubernetesNodeController', KubernetesNodeController);
|