andy.lee db21093670
show volume encryption in vm detail volume tab
Signed-off-by: andy.lee <andy.lee@suse.com>
2024-10-23 17:01:31 +02:00

310 lines
8.7 KiB
JavaScript

import { _CLONE } from '@shell/config/query-params';
import pick from 'lodash/pick';
import { PV, LONGHORN } from '@shell/config/types';
import { DESCRIPTION } from '@shell/config/labels-annotations';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { findBy } from '@shell/utils/array';
import { get, clone } from '@shell/utils/object';
import { colorForState } from '@shell/plugins/dashboard-store/resource-class';
import { HCI, VOLUME_SNAPSHOT } from '../../types';
import HarvesterResource from '../harvester';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
const DEGRADED_ERROR = 'replica scheduling failed';
export default class HciPv extends HarvesterResource {
applyDefaults(_, realMode) {
const accessModes = realMode === _CLONE ? this.spec.accessModes : [];
const storage =
realMode === _CLONE ? this.spec.resources.requests.storage : null;
const storageClassName =
realMode === _CLONE ? this.spec.storageClassName : '';
this['spec'] = {accessModes,
storageClassName,
volumeName: '',
resources: { requests: { storage } }};
}
get availableActions() {
const out = super._availableActions;
const clone = out.find(action => action.action === 'goToClone');
if (clone) {
clone.action = 'goToCloneVolume';
}
return [
{
action: 'exportImage',
enabled: this.hasAction('export') && !this.isEncrypted,
icon: 'icon icon-copy',
label: this.t('harvester.action.exportImage')
},
{
action: 'cancelExpand',
enabled: this.hasAction('cancelExpand'),
icon: 'icon icon-backup',
label: this.t('harvester.action.cancelExpand')
},
{
action: 'snapshot',
enabled: this.hasAction('snapshot'),
icon: 'icon icon-backup',
label: this.t('harvester.action.snapshot'),
},
...out
];
}
exportImage(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'HarvesterExportImageDialog'
});
}
cancelExpand(resources = this) {
this.doActionGrowl('cancelExpand', {});
}
snapshot(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'SnapshotDialog'
});
}
goToCloneVolume(resources = this) {
this.$dispatch('promptModal', {
resources,
component: 'VolumeCloneDialog'
});
}
cleanForNew() {
this.$dispatch(`cleanForNew`, this);
delete this.metadata.finalizers;
const keys = [HCI_ANNOTATIONS.IMAGE_ID, DESCRIPTION];
this.metadata.annotations = pick(this.metadata.annotations, keys);
}
get canUpdate() {
return this.hasLink('update');
}
get stateDisplay() {
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
const degradedVolume = volumeError === DEGRADED_ERROR;
const status = this?.status?.phase === 'Bound' && !volumeError && this.isLonghornVolumeReady ? 'Ready' : 'Not Ready';
const conditions = this?.status?.conditions || [];
if (findBy(conditions, 'type', 'Resizing')?.status === 'True') {
return 'Resizing';
} else if (!!this.attachVM && !volumeError) {
return 'In-use';
} else if (degradedVolume) {
return 'Degraded';
} else {
return status;
}
}
// state is similar with stateDisplay, the reason we keep this property is the status of In-use should not be displayed on vm detail page
get state() {
const volumeError = this.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR];
const degradedVolume = volumeError === DEGRADED_ERROR;
let status = this?.status?.phase === 'Bound' && !volumeError ? 'Ready' : 'Not Ready';
const conditions = this?.status?.conditions || [];
if (degradedVolume) {
status = 'Degraded';
}
if (findBy(conditions, 'type', 'Resizing')?.status === 'True') {
status = 'Resizing';
}
return status;
}
get stateColor() {
const state = this.stateDisplay;
return colorForState(state);
}
get stateDescription() {
return (
super.stateDescription
);
}
get detailLocation() {
const detailLocation = clone(this._detailLocation);
detailLocation.params.resource = HCI.VOLUME;
return detailLocation;
}
get doneOverride() {
const detailLocation = clone(this._detailLocation);
delete detailLocation.params.namespace;
delete detailLocation.params.id;
detailLocation.params.resource = HCI.VOLUME;
detailLocation.name = `${ HARVESTER_PRODUCT }-c-cluster-resource`;
return detailLocation;
}
get parentNameOverride() {
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.VOLUME }"`, { count: 1 }).trim();
}
get parentLocationOverride() {
return this.doneOverride;
}
get phaseState() {
return this.status?.phase || 'N/A';
}
get attachVM() {
const allVMs = this.$rootGetters['harvester/all'](HCI.VM) || [];
const findAttachVM = (vm) => {
const attachVolumes = vm.spec.template?.spec?.volumes || [];
if (vm.namespace === this.namespace && attachVolumes.length > 0) {
return attachVolumes.find(vol => vol.persistentVolumeClaim?.claimName === this.name);
}
return null;
};
return allVMs.find(findAttachVM);
}
get isAvailable() {
const unAvailable = ['Resizing', 'Not Ready'];
return !unAvailable.includes(this.stateDisplay);
}
get volumeSort() {
const volume = this.spec?.resources?.requests?.storage || 0;
return parseInt(volume);
}
get isSystemResource() {
const systemNamespaces = this.$rootGetters['systemNamespaces'];
if (systemNamespaces.includes(this.metadata?.namespace)) {
return true;
}
return false;
}
get isEncrypted() {
return Boolean(this.relatedPV?.spec.csi.volumeAttributes.encrypted) || false;
}
get longhornVolume() {
const inStore = this.$rootGetters['currentProduct'].inStore;
return this.$rootGetters[`${ inStore }/all`](LONGHORN.VOLUMES).find(v => v.metadata?.name === this.spec?.volumeName);
}
get longhornEngine() {
const inStore = this.$rootGetters['currentProduct'].inStore;
return this.$rootGetters[`${ inStore }/all`](LONGHORN.ENGINES).find(v => v.spec?.volumeName === this.spec?.volumeName);
}
// https://github.com/longhorn/longhorn-manager/blob/master/api/model.go#L1151
get isLonghornVolumeReady() {
let ready = true;
const longhornVolume = this.longhornVolume || {};
const scheduledCondition = (longhornVolume?.status?.conditions || []).find(c => c.type === 'Scheduled' || c.type === 'scheduled') || {};
if ((longhornVolume?.spec?.nodeID === '' && longhornVolume?.status?.state !== 'detached') ||
(longhornVolume?.status?.state === 'detached' && scheduledCondition.status !== 'True') ||
longhornVolume?.status?.robustness === 'faulted' ||
longhornVolume?.status?.restoreRequired ||
longhornVolume?.status?.cloneStatus?.state === 'failed'
) {
ready = false;
}
return ready;
}
get relatedVolumeSnapshotCounts() {
const snapshots = this.$rootGetters['harvester/all'](VOLUME_SNAPSHOT);
return snapshots.filter((snapshot) => {
const volumeId = `${ snapshot.metadata?.namespace }/${ snapshot.spec?.source?.persistentVolumeClaimName }`;
const kind = snapshot.metadata?.ownerReferences?.[0]?.kind;
return volumeId === this.id && kind === 'PersistentVolumeClaim';
});
}
get originalSnapshot() {
if (this.spec?.dataSource) {
return this.$rootGetters['harvester/all'](VOLUME_SNAPSHOT).find(V => V.metadata?.name === this.spec.dataSource.name);
} else {
return null;
}
}
get source() {
const imageId = get(this, `metadata.annotations."${ HCI_ANNOTATIONS.IMAGE_ID }"`);
return imageId ? 'image' : 'data';
}
get warnDeletionMessage() {
return this.t('harvester.volume.promptRemove.tips');
}
get relatedPV() {
return this.$rootGetters['harvester/all'](PV).find(pv => pv.metadata?.name === this.spec?.volumeName);
}
get resourceExternalLink() {
const host = window.location.host;
const { params } = this.currentRoute();
const volumeName = this.spec?.volumeName;
if (!volumeName) {
return null;
}
return {
tipsKey: 'harvester.volume.externalLink.tips',
url: `https://${ host }/k8s/clusters/${ params.cluster }/api/v1/namespaces/longhorn-system/services/http:longhorn-frontend:80/proxy/#/volume/${ volumeName }`
};
}
get customValidationRules() {
return [
{
nullable: false,
path: 'spec.resources.requests.storage',
required: true,
validators: ['volumeSize']
},
];
}
}