portainer/app/kubernetes/views/applications/edit/applicationController.js

270 lines
9.6 KiB
JavaScript

import angular from 'angular';
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import {
KubernetesApplicationDataAccessPolicies,
KubernetesApplicationDeploymentTypes,
KubernetesApplicationTypes,
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
function computeTolerations(nodes, application) {
const pod = application.Pods[0];
_.forEach(nodes, (n) => {
n.AcceptsApplication = true;
n.Expanded = false;
if (!pod) {
return;
}
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 anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' });
if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) {
n.AcceptsApplication = false;
n.UnmetTaints.push(t);
} else {
n.AcceptsApplication = true;
}
});
});
return nodes;
}
// For node requirement format depending on operator value
// see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#nodeselectorrequirement-v1-core
// Some operators require empty "values" field, some only one element in "values" field, etc
function computeAffinities(nodes, application) {
if (!application.Pods || application.Pods.length === 0) {
return nodes;
}
const pod = application.Pods[0];
_.forEach(nodes, (n) => {
if (pod.NodeSelector) {
const patch = JsonPatch.compare(n.Labels, pod.NodeSelector);
_.remove(patch, { op: 'remove' });
n.UnmatchedNodeSelectorLabels = _.map(patch, (i) => {
return { key: _.trimStart(i.path, '/'), value: i.value };
});
if (n.UnmatchedNodeSelectorLabels.length) {
n.AcceptsApplication = false;
}
}
if (pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
const unmatchedTerms = _.map(pod.Affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => {
const unmatchedExpressions = _.map(t.matchExpressions, (e) => {
const exists = {}.hasOwnProperty.call(n.Labels, e.key);
const isIn = exists && _.includes(e.values, n.Labels[e.key]);
if (
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS && exists) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key], 10) > parseInt(e.values[0], 10)) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key], 10) < parseInt(e.values[0], 10))
) {
return;
}
return e;
});
return _.without(unmatchedExpressions, undefined);
});
_.remove(unmatchedTerms, (i) => i.length === 0);
n.UnmatchedNodeAffinities = unmatchedTerms;
if (n.UnmatchedNodeAffinities.length) {
n.AcceptsApplication = false;
}
}
});
return nodes;
}
function computePlacements(nodes, application) {
nodes = computeTolerations(nodes, application);
nodes = computeAffinities(nodes, application);
return nodes;
}
class KubernetesApplicationController {
/* @ngInject */
constructor(
$async,
$state,
clipboard,
Notifications,
LocalStorage,
KubernetesResourcePoolService,
KubernetesApplicationService,
KubernetesEventService,
KubernetesStackService,
KubernetesPodService,
KubernetesNodeService,
StackService
) {
this.$async = $async;
this.$state = $state;
this.clipboard = clipboard;
this.Notifications = Notifications;
this.LocalStorage = LocalStorage;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.StackService = StackService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesStackService = KubernetesStackService;
this.KubernetesPodService = KubernetesPodService;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes;
this.KubernetesPodContainerTypes = KubernetesPodContainerTypes;
this.onInit = this.onInit.bind(this);
this.getApplication = this.getApplication.bind(this);
this.getApplicationAsync = this.getApplicationAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
}
selectTab(index) {
this.LocalStorage.storeActiveTab('application', index);
}
showEditor() {
this.state.showEditorTab = true;
this.selectTab(3);
}
isSystemNamespace() {
return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
}
hasEventWarnings() {
return this.state.eventWarningCount;
}
/**
* EVENTS
*/
async getEventsAsync() {
try {
this.state.eventsLoading = true;
const events = await this.KubernetesEventService.get(this.state.params.namespace);
this.events = _.filter(
events,
(event) =>
event.Involved.uid === this.application.Id ||
event.Involved.uid === this.application.ServiceId ||
_.find(this.application.Pods, (pod) => pod.Id === event.Involved.uid) !== undefined
);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application related events');
} finally {
this.state.eventsLoading = false;
}
}
getEvents() {
return this.$async(this.getEventsAsync);
}
/**
* APPLICATION
*/
async getApplicationAsync() {
try {
this.state.dataLoading = true;
const [application, nodes] = await Promise.all([
this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name),
this.KubernetesNodeService.get(),
]);
this.application = application;
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
this.placements = computePlacements(nodes, this.application);
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
if (application.StackId) {
const file = await this.StackService.getStackFile(application.StackId);
this.stackFileContent = file;
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
} finally {
this.state.dataLoading = false;
}
}
getApplication() {
return this.$async(this.getApplicationAsync);
}
async onInit() {
this.limitedFeature = FeatureId.K8S_ROLLING_RESTART;
this.state = {
activeTab: 0,
currentName: this.$state.$current.name,
showEditorTab: false,
DisplayedPanel: 'pods',
eventsLoading: true,
dataLoading: true,
viewReady: false,
params: {
namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name,
},
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
eventWarningCount: 0,
placementWarning: false,
expandedNote: false,
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
publicUrl: this.endpoint.PublicURL,
};
this.state.activeTab = this.LocalStorage.getActiveTab('application');
this.formValues = {
Note: '',
SelectedRevision: undefined,
};
await this.getApplication();
await this.getEvents();
this.state.viewReady = true;
}
$onInit() {
return this.$async(this.onInit);
}
$onDestroy() {
if (this.state.currentName !== this.$state.$current.name) {
this.LocalStorage.storeActiveTab('application', 0);
}
}
}
export default KubernetesApplicationController;
angular.module('portainer.kubernetes').controller('KubernetesApplicationController', KubernetesApplicationController);