dashboard/models/cluster/node.js

416 lines
9.9 KiB
JavaScript

import { formatPercent } from '@/utils/string';
import { CAPI as CAPI_ANNOTATIONS, NODE_ROLES, RKE } from '@/config/labels-annotations.js';
import {
CAPI, MANAGEMENT, METRIC, NORMAN, POD
} from '@/config/types';
import { parseSi } from '@/utils/units';
import findLast from 'lodash/findLast';
import SteveModel from '@/plugins/steve/steve-class';
export default class ClusterNode extends SteveModel {
get _availableActions() {
const normanAction = this.norman?.actions || {};
const cordon = {
action: 'cordon',
enabled: !!normanAction.cordon,
icon: 'icon icon-fw icon-pause',
label: 'Cordon',
total: 1,
bulkable: true
};
const uncordon = {
action: 'uncordon',
enabled: !!normanAction.uncordon,
icon: 'icon icon-fw icon-play',
label: 'Uncordon',
total: 1,
bulkable: true
};
const drain = {
action: 'drain',
enabled: !!normanAction.drain,
icon: 'icon icon-fw icon-dot-open',
label: this.t('drainNode.action'),
bulkable: true,
bulkAction: 'drain'
};
const stopDrain = {
action: 'stopDrain',
enabled: !!normanAction.stopDrain,
icon: 'icon icon-fw icon-x',
label: this.t('drainNode.actionStop'),
bulkable: true,
};
const openSsh = {
action: 'openSsh',
enabled: !!this.provisionedMachine?.links?.shell,
icon: 'icon icon-fw icon-chevron-right',
label: 'SSH Shell',
};
const downloadKeys = {
action: 'downloadKeys',
enabled: !!this.provisionedMachine?.links?.sshkeys,
icon: 'icon icon-fw icon-download',
label: this.t('node.actions.downloadSSHKey'),
};
return [
openSsh,
downloadKeys,
{ divider: true },
cordon,
uncordon,
drain,
stopDrain,
{ divider: true },
...super._availableActions
];
}
openSsh() {
this.provisionedMachine.openSsh();
}
downloadKeys() {
this.provisionedMachine.downloadKeys();
}
get showDetailStateBadge() {
return true;
}
get name() {
return this.metadata.name;
}
get internalIp() {
const addresses = this.status?.addresses || [];
return findLast(addresses, address => address.type === 'InternalIP')?.address;
}
get externalIp() {
const addresses = this.status?.addresses || [];
const annotationAddress = this.metadata.annotations[RKE.EXTERNAL_IP];
const statusAddress = findLast(addresses, address => address.type === 'ExternalIP')?.address;
return statusAddress || annotationAddress;
}
get labels() {
return this.metadata?.labels || {};
}
get isWorker() {
return this.managementNode ? this.managementNode.isWorker : `${ this.labels[NODE_ROLES.WORKER] }` === 'true';
}
get isControlPlane() {
if (this.managementNode) {
return this.managementNode.isControlPlane;
} else if (
`${ this.labels[NODE_ROLES.CONTROL_PLANE] }` === 'true' ||
`${ this.labels[NODE_ROLES.CONTROL_PLANE_OLD] }` === 'true'
) {
return true;
}
return false;
}
get isEtcd() {
return this.managementNode ? this.managementNode.isEtcd : `${ this.labels[NODE_ROLES.ETCD] }` === 'true';
}
get hasARole() {
const roleLabelKeys = Object.values(NODE_ROLES);
return Object.keys(this.labels)
.some((labelKey) => {
const hasRoleLabel = roleLabelKeys.includes(labelKey);
const isExpectedValue = `${ this.labels[labelKey] }` === 'true';
return hasRoleLabel && isExpectedValue;
});
}
get roles() {
const { isControlPlane, isWorker, isEtcd } = this;
return listNodeRoles(isControlPlane, isWorker, isEtcd, this.t('generic.all'));
}
get version() {
return this.status.nodeInfo.kubeletVersion;
}
get cpuUsage() {
return parseSi(this.$rootGetters['cluster/byId'](METRIC.NODE, this.id)?.usage?.cpu || '0');
}
get cpuCapacity() {
return parseSi(this.status.allocatable.cpu);
}
get cpuUsagePercentage() {
return ((this.cpuUsage * 100) / this.cpuCapacity).toString();
}
get ramUsage() {
return parseSi(this.$rootGetters['cluster/byId'](METRIC.NODE, this.id)?.usage?.memory || '0');
}
get ramCapacity() {
return parseSi(this.status.capacity.memory);
}
get ramUsagePercentage() {
return ((this.ramUsage * 100) / this.ramCapacity).toString();
}
get podUsage() {
return calculatePercentage(this.status.allocatable.pods, this.status.capacity.pods);
}
get podConsumedUsage() {
return ((this.podConsumed / this.podCapacity) * 100).toString();
}
get podCapacity() {
return Number.parseInt(this.status.capacity.pods);
}
get podConsumed() {
return this.runningPods.length;
}
get isPidPressureOk() {
return this.isCondition('PIDPressure', 'False');
}
get isDiskPressureOk() {
return this.isCondition('DiskPressure', 'False');
}
get isMemoryPressureOk() {
return this.isCondition('MemoryPressure', 'False');
}
get isKubeletOk() {
return this.isCondition('Ready');
}
get isCordoned() {
return !!this.spec.unschedulable;
}
get drainedState() {
const sNodeCondition = this.managementNode?.status.conditions.find(c => c.type === 'Drained');
if (sNodeCondition) {
if (sNodeCondition.status === 'True') {
return 'drained';
}
if (sNodeCondition.transitioning) {
return 'draining';
}
}
return null;
}
get containerRuntimeVersion() {
return this.status.nodeInfo.containerRuntimeVersion.replace('docker://', '');
}
get containerRuntimeIcon() {
if ( this.status.nodeInfo.containerRuntimeVersion.includes('docker') ) {
return 'icon-docker';
}
return '';
}
async cordon(resources) {
const safeResources = Array.isArray(resources) ? resources : [this];
await Promise.all(safeResources.map((node) => {
return node.norman?.doAction('cordon');
}));
}
async uncordon(resources) {
const safeResources = Array.isArray(resources) ? resources : [this];
await Promise.all(safeResources.map((node) => {
return node.norman?.doAction('uncordon');
}));
}
get clusterId() {
const parts = this.links.self.split('/');
return parts[parts.length - 4];
}
get normanNodeId() {
const managementNode = (this.$rootGetters['management/all'](MANAGEMENT.NODE) || []).find((n) => {
return n.id.startsWith(this.clusterId) && n.status.nodeName === this.name;
});
if (managementNode) {
return managementNode.id.replace('/', ':');
}
return null;
}
get norman() {
return this.$rootGetters['rancher/byId'](NORMAN.NODE, this.normanNodeId);
}
get managementNode() {
return this.$rootGetters['management/all'](MANAGEMENT.NODE).find((mNode) => {
return mNode.id.startsWith(this.clusterId) && mNode.status.nodeName === this.id;
});
}
drain(resources) {
this.$dispatch('promptModal', { component: 'DrainNode', resources: [resources || [this], this.normanNodeId] });
}
async stopDrain(resources) {
const safeResources = Array.isArray(resources) ? resources : [this];
await Promise.all(safeResources.map((node) => {
return node.norman?.doAction('stopDrain');
}));
}
get state() {
if (this.drainedState) {
return this.drainedState;
}
if ( this.isCordoned ) {
return 'cordoned';
}
return this.metadata?.state?.name || 'unknown';
}
get details() {
const details = [
{
label: this.t('node.detail.detailTop.version'),
content: this.version
},
{
label: this.t('node.detail.detailTop.os'),
content: this.status.nodeInfo.osImage
},
{
label: this.t('node.detail.detailTop.containerRuntime'),
formatter: 'IconText',
formatterOpts: { iconClass: this.containerRuntimeIcon },
content: this.containerRuntimeVersion
}];
if (this.internalIp) {
details.unshift({
label: this.t('node.detail.detailTop.internalIP'),
formatter: 'CopyToClipboard',
content: this.internalIp
});
}
if (this.externalIp) {
details.unshift({
label: this.t('node.detail.detailTop.externalIP'),
formatter: 'CopyToClipboard',
content: this.externalIp
});
}
return details;
}
get pods() {
const allPods = this.$rootGetters['cluster/all'](POD);
return allPods.filter(pod => pod.spec.nodeName === this.name);
}
get runningPods() {
return this.pods.filter(pod => pod.isRunning);
}
get confirmRemove() {
return true;
}
get canClone() {
return false;
}
get canDelete() {
const provider = this.$rootGetters['currentCluster'].provisioner.toLowerCase();
const cloudProviders = [
'aks', 'azureaks', 'azurekubernetesservice',
'eks', 'amazoneks',
'gke', 'googlegke'
];
return !cloudProviders.includes(provider);
}
// You need to preload CAPI.MACHINEs to use this
get provisionedMachine() {
const namespace = this.metadata?.annotations?.[CAPI_ANNOTATIONS.CLUSTER_NAMESPACE];
const name = this.metadata?.annotations?.[CAPI_ANNOTATIONS.MACHINE_NAME];
if ( namespace && name ) {
return this.$rootGetters['management/byId'](CAPI.MACHINE, `${ namespace }/${ name }`);
}
return null;
}
}
function calculatePercentage(allocatable, capacity) {
const c = Number.parseFloat(capacity);
const a = Number.parseFloat(allocatable);
const percent = (((c - a) / c) * 100);
return formatPercent(percent);
}
export function listNodeRoles(isControlPlane, isWorker, isEtcd, allString) {
const res = [];
if (isControlPlane) {
res.push('Control Plane');
}
if (isWorker) {
res.push('Worker');
}
if (isEtcd) {
res.push('Etcd');
}
if (res.length === 3 || res.length === 0) {
return allString;
}
return res.join(', ');
}