mirror of https://github.com/rancher/dashboard.git
489 lines
12 KiB
JavaScript
489 lines
12 KiB
JavaScript
import { formatPercent } from '@shell/utils/string';
|
|
import { CAPI as CAPI_ANNOTATIONS, NODE_ROLES, RKE, SYSTEM_LABELS } from '@shell/config/labels-annotations.js';
|
|
import {
|
|
CAPI, MANAGEMENT, METRIC, NORMAN, POD
|
|
} from '@shell/config/types';
|
|
import { parseSi } from '@shell/utils/units';
|
|
import findLast from 'lodash/findLast';
|
|
|
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
import { LOCAL } from '@shell/config/query-params';
|
|
|
|
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() {
|
|
// Pass in the name of the node, so we display that rather than the name of the provisioned machine
|
|
this.provisionedMachine.openSsh(this.nameDisplay);
|
|
}
|
|
|
|
downloadKeys() {
|
|
this.provisionedMachine.downloadKeys();
|
|
}
|
|
|
|
get showDetailStateBadge() {
|
|
return true;
|
|
}
|
|
|
|
get name() {
|
|
return this.metadata.name;
|
|
}
|
|
|
|
get addresses() {
|
|
return this.status?.addresses || [];
|
|
}
|
|
|
|
get internalIp() {
|
|
return findLast(this.addresses, (address) => address.type === 'InternalIP')?.address;
|
|
}
|
|
|
|
get externalIp() {
|
|
const annotationAddress = this.metadata.annotations[RKE.EXTERNAL_IP];
|
|
const statusAddress = findLast(this.addresses, (address) => address.type === 'ExternalIP')?.address;
|
|
|
|
return statusAddress || annotationAddress;
|
|
}
|
|
|
|
get labels() {
|
|
return this.metadata?.labels || {};
|
|
}
|
|
|
|
get customLabelCount() {
|
|
return this.customLabels.length;
|
|
}
|
|
|
|
get customLabels() {
|
|
const parsedLabels = [];
|
|
|
|
if (this.labels) {
|
|
for (const k in this.labels) {
|
|
const [prefix] = k.split('/');
|
|
|
|
if (!SYSTEM_LABELS.includes(prefix)) {
|
|
parsedLabels.push(`${ k }=${ this.labels[k] }`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return parsedLabels;
|
|
}
|
|
|
|
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() {
|
|
/*
|
|
With EKS nodes that have been migrated from norman,
|
|
cpu/memory usage is by the annotation `management.cattle.io/pod-requests`
|
|
*/
|
|
if ( this.isFromNorman && this.provider === 'eks' ) {
|
|
return parseSi(this.podRequests.cpu || '0');
|
|
}
|
|
|
|
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() {
|
|
if ( this.isFromNorman && this.provider === 'eks' ) {
|
|
return parseSi(this.podRequests.memory || '0');
|
|
}
|
|
|
|
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 ramReserved() {
|
|
return parseSi(this.status?.allocatable?.memory);
|
|
}
|
|
|
|
get ramReservedPercentage() {
|
|
return ((this.ramUsage * 100) / this.ramReserved).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 parseSi(this.status.capacity?.pods);
|
|
}
|
|
|
|
get podConsumed() {
|
|
const runningPods = this.pods.filter((pod) => pod.state === 'running');
|
|
|
|
return runningPods.length || 0;
|
|
}
|
|
|
|
get podRequests() {
|
|
return JSON.parse(this.metadata.annotations['management.cattle.io/pod-requests'] || '{}');
|
|
}
|
|
|
|
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');
|
|
}));
|
|
}
|
|
|
|
/**
|
|
*Find the node's cluster id from it's url
|
|
*/
|
|
get clusterId() {
|
|
const parts = this.links.self.split('/');
|
|
|
|
// Local cluster url links omit `/k8s/clusters/<cluster id>`
|
|
// `/v1/nodes` vs `k8s/clusters/c-m-274kcrc4/v1/nodes`
|
|
// Be safe when determining this, so work back through the url from a known point
|
|
if (parts.length > 6 && parts[parts.length - 6] === 'k8s' && parts[parts.length - 5] === 'clusters') {
|
|
return parts[parts.length - 4];
|
|
}
|
|
|
|
return LOCAL;
|
|
}
|
|
|
|
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',
|
|
componentProps: {
|
|
kubeNodes: resources || [this],
|
|
normanNodeId: 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() {
|
|
// This fetches all pods that are in the store, rather than all pods in the cluster
|
|
const allPods = this.$rootGetters['cluster/all'](POD);
|
|
|
|
return allPods.filter((pod) => pod.spec.nodeName === this.name);
|
|
}
|
|
|
|
get confirmRemove() {
|
|
return true;
|
|
}
|
|
|
|
get canClone() {
|
|
return false;
|
|
}
|
|
|
|
get canDelete() {
|
|
const cloudProviders = [
|
|
'aks', 'azureaks', 'azurekubernetesservice',
|
|
'eks', 'amazoneks',
|
|
'gke', 'googlegke'
|
|
];
|
|
|
|
return !cloudProviders.includes(this.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;
|
|
}
|
|
|
|
get isFromNorman() {
|
|
return (this.$rootGetters['currentCluster'].metadata.labels || {})['cattle.io/creator'] === 'norman';
|
|
}
|
|
|
|
get provider() {
|
|
return this.$rootGetters['currentCluster'].provisioner.toLowerCase();
|
|
}
|
|
|
|
get displayTaintsAndLabels() {
|
|
return !!this.spec.taints?.length || !!this.customLabelCount;
|
|
}
|
|
}
|
|
|
|
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(', ');
|
|
}
|