diff --git a/pkg/harvester/config/harvester-map.js b/pkg/harvester/config/harvester-map.js index 02cbfe25063..39e01ad49ac 100644 --- a/pkg/harvester/config/harvester-map.js +++ b/pkg/harvester/config/harvester-map.js @@ -69,3 +69,12 @@ export const ADD_ONS = { RANCHER_MONITORING: 'rancher-monitoring', VM_IMPORT_CONTROLLER: 'vm-import-controller', }; + +export const CSI_SECRETS = { + CSI_PROVISIONER_SECRET_NAME: 'csi.storage.k8s.io/provisioner-secret-name', + CSI_PROVISIONER_SECRET_NAMESPACE: 'csi.storage.k8s.io/provisioner-secret-namespace', + CSI_NODE_PUBLISH_SECRET_NAME: 'csi.storage.k8s.io/node-publish-secret-name', + CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace', + CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name', + CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace', +}; diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index fa0c42bfcb4..b580f4f0026 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -8,7 +8,6 @@ export const IMAGE_DOWNLOAD_SIZE = { labelKey: 'tableHeaders.size', value: 'downSize', sort: 'status.size', - width: 120 }; export const IMAGE_VIRTUAL_SIZE = { @@ -16,7 +15,6 @@ export const IMAGE_VIRTUAL_SIZE = { labelKey: 'harvester.tableHeaders.virtualSize', value: 'virtualSize', sort: 'status.virtualSize', - width: 120 }; export const IMAGE_PROGRESS = { diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue index 98677d4e27c..0776d940ebe 100644 --- a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue +++ b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue @@ -7,8 +7,9 @@ import Tabbed from '@shell/components/Tabbed'; import Tab from '@shell/components/Tabbed/Tab'; import { findBy } from '@shell/utils/array'; import { get } from '@shell/utils/object'; - +import { ucFirst } from '@shell/utils/string'; import Storage from './Storage'; +import { SECRET } from '@shell/config/types'; export default { components: { @@ -26,8 +27,14 @@ export default { }, }, + async fetch() { + const inStore = this.$store.getters['currentProduct'].inStore; + + this.secrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }); + }, + data() { - return {}; + return { secrets: [] }; }, computed: { @@ -57,6 +64,21 @@ export default { return this.value?.spec?.sourceType === 'upload'; }, + encryptionSecret() { + if (!this.value.isEncrypted) { + return '-'; + } + + return this.value.encryptionSecret; + }, + secretLink() { + return this.secrets.find(sc => sc.id === this.value.encryptionSecret)?.detailLocation; + }, + + isEncryptedString() { + return ucFirst(String(this.value.isEncrypted)); + }, + imageName() { return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-'; }, @@ -116,6 +138,29 @@ export default { +
+
+ +
+
+ +
+
+
+ {{ t('harvester.image.encryptionSecret') }} +
+ + {{ encryptionSecret }} + + + {{ encryptionSecret }} + + + — + +
+
+
diff --git a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue index 8846f9f87e8..2845918efbd 100644 --- a/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue +++ b/pkg/harvester/detail/kubevirt.io.virtualmachine/VirtualMachineTabs/VirtualMachineBasics.vue @@ -63,10 +63,7 @@ export default { imageName() { const imageList = this.$store.getters['harvester/all'](HCI.IMAGE) || []; - - const image = imageList.find( (I) => { - return this.value.rootImageId === I.id; - }); + const image = imageList.find( I => this.value.rootImageId === I.id); return image?.spec?.displayName || 'N/A'; }, diff --git a/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue b/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue index 329f61d2bfc..aef2cc02961 100644 --- a/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue +++ b/pkg/harvester/edit/harvesterhci.io.storage/provisioners/driver.longhorn.io.vue @@ -3,21 +3,34 @@ import KeyValue from '@shell/components/form/KeyValue'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import { LabeledInput } from '@components/Form/LabeledInput'; import RadioGroup from '@components/Form/Radio/RadioGroup'; - +import { SECRET, NAMESPACE, LONGHORN } from '@shell/config/types'; import { _CREATE, _VIEW } from '@shell/config/query-params'; -import { LONGHORN } from '@shell/config/types'; +import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map'; import { clone } from '@shell/utils/object'; import { uniq } from '@shell/utils/array'; +// UI components for Longhorn storage class parameters const DEFAULT_PARAMETERS = [ 'numberOfReplicas', 'staleReplicaTimeout', 'diskSelector', 'nodeSelector', 'migratable', + 'encrypted', ]; +const { + CSI_PROVISIONER_SECRET_NAME, + CSI_PROVISIONER_SECRET_NAMESPACE, + CSI_NODE_PUBLISH_SECRET_NAME, + CSI_NODE_PUBLISH_SECRET_NAMESPACE, + CSI_NODE_STAGE_SECRET_NAME, + CSI_NODE_STAGE_SECRET_NAMESPACE +} = CSI_SECRETS; + export default { + name: 'DriverLonghornIO', + components: { KeyValue, LabeledSelect, @@ -40,6 +53,16 @@ export default { }, }, + async fetch() { + const inStore = this.$store.getters['currentProduct'].inStore; + + await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }); + + const allSecrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }); + + // only show non-system secret to user to select + this.secrets = allSecrets.filter(secret => secret.isSystem === false); + }, data() { if (this.realMode === _CREATE) { this.$set(this.value, 'parameters', { @@ -47,13 +70,13 @@ export default { staleReplicaTimeout: '30', diskSelector: null, nodeSelector: null, + encrypted: 'false', migratable: 'true', }); } - return {}; + return { secrets: [] }; }, - computed: { longhornNodes() { const inStore = this.$store.getters['currentProduct'].inStore; @@ -97,11 +120,29 @@ export default { }]; }, + secretOptions() { + return this.secrets.map(secret => secret.id); + }, + + volumeEncryptionOptions() { + return [{ + label: this.t('generic.yes'), + value: 'true' + }, { + label: this.t('generic.no'), + value: 'false' + }]; + }, + parameters: { get() { const parameters = clone(this.value?.parameters) || {}; - DEFAULT_PARAMETERS.map((key) => { + DEFAULT_PARAMETERS.forEach((key) => { + delete parameters[key]; + }); + + Object.values(CSI_SECRETS).forEach((key) => { delete parameters[key]; }); @@ -113,6 +154,46 @@ export default { } }, + volumeEncryption: { + set(neu) { + this.$set(this.value, 'parameters', { + ...this.value.parameters, + encrypted: neu + }); + }, + + get() { + return this.value?.parameters?.encrypted || 'false'; + } + }, + + secret: { + get() { + const selectedNs = this.value.parameters[CSI_PROVISIONER_SECRET_NAMESPACE]; + const selectedName = this.value.parameters[CSI_PROVISIONER_SECRET_NAME]; + + if (selectedNs && selectedName) { + return `${ selectedNs }/${ selectedName }`; + } + + return ''; + }, + + set(selectedSecret) { + const [namespace, name] = selectedSecret.split('/'); + + this.$set(this.value, 'parameters', { + ...this.value.parameters, + [CSI_PROVISIONER_SECRET_NAME]: name, + [CSI_NODE_PUBLISH_SECRET_NAME]: name, + [CSI_NODE_STAGE_SECRET_NAME]: name, + [CSI_PROVISIONER_SECRET_NAMESPACE]: namespace, + [CSI_NODE_PUBLISH_SECRET_NAMESPACE]: namespace, + [CSI_NODE_STAGE_SECRET_NAMESPACE]: namespace + }); + } + }, + nodeSelector: { get() { const nodeSelector = this.value?.parameters?.nodeSelector; @@ -221,14 +302,31 @@ export default {
-
+
+ +
+
+ +
+
-
diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue index 88c5dda9c1a..7a3c8c5e2ad 100644 --- a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue +++ b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue @@ -8,7 +8,6 @@ import NameNsDescription from '@shell/components/form/NameNsDescription'; import { RadioGroup } from '@components/Form/Radio'; import Select from '@shell/components/form/Select'; import LabeledSelect from '@shell/components/form/LabeledSelect'; - import CreateEditView from '@shell/mixins/create-edit-view'; import { OS } from '../mixins/harvester-vm'; import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image'; @@ -18,6 +17,9 @@ import { allHash } from '@shell/utils/promise'; import { STORAGE_CLASS } from '@shell/config/types'; import { HCI } from '../types'; +const ENCRYPT = 'encrypt'; +const DECRYPT = 'decrypt'; +const CLONE = 'clone'; const DOWNLOAD = 'download'; const UPLOAD = 'upload'; const rawORqcow2 = 'raw_qcow2'; @@ -57,6 +59,8 @@ export default { const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find(s => s.isDefault); this.$set(this, 'storageClassName', this.storageClassName || defaultStorage?.metadata?.name || 'longhorn'); + this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE); + this.selectedImage = this.images.find(i => i.name === this.value.name) || null; }, data() { @@ -69,12 +73,14 @@ export default { } return { - url: this.value.spec.url, - files: [], - resource: '', - headers: {}, - fileUrl: '', - file: '', + selectedImage: null, + images: [], + url: this.value.spec.url, + files: [], + resource: '', + headers: {}, + fileUrl: '', + file: '', }; }, @@ -92,9 +98,16 @@ export default { }, showEditAsYaml() { - return this.value.spec.sourceType === DOWNLOAD; + return this.value.spec.sourceType === DOWNLOAD || this.value.spec.sourceType === CLONE; + }, + radioGroupOptions() { + return [ + DOWNLOAD, + UPLOAD, + ENCRYPT, + DECRYPT + ]; }, - storageClassOptions() { const inStore = this.$store.getters['currentProduct'].inStore; const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); @@ -120,9 +133,66 @@ export default { this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue; } }, + sourceImageOptions() { + let options = []; + + if (this.value.spec.sourceType !== CLONE) { + return options; + } + if (this.value.spec.securityParameters.cryptoOperation === ENCRYPT) { + options = this.images.filter(image => !image.isEncrypted); + } else { + options = this.images.filter(image => image.isEncrypted); + } + + return options.map(image => image.spec.displayName); + }, + sourceImageName: { + get() { + return this.selectedImage?.spec.displayName; + }, + set(imageDisplayName) { + this.selectedImage = this.images.find(i => i.spec.displayName === imageDisplayName); + // sourceImageName should bring the name of the image + this.value.spec.securityParameters.sourceImageName = this.selectedImage?.metadata.name || ''; + } + }, + sourceType: { + get() { + if (this.value.spec.sourceType === CLONE) { + return this.value.spec.securityParameters.cryptoOperation; + } else { + return this.value.spec.sourceType; + } + }, + + set(neu) { + if (neu === DECRYPT || neu === ENCRYPT) { + this.value.spec.sourceType = CLONE; + this.$set(this.value.spec, 'securityParameters', { + cryptoOperation: neu, + sourceImageName: '', + sourceImageNamespace: this.value.metadata.namespace + }); + this.selectedImage = null; + } else { + this.$delete(this.value.spec, 'securityParameters'); + this.value.spec.sourceType = neu; + } + } + } }, watch: { + 'value.metadata.namespace'(neu) { + if (this.value.spec.sourceType === CLONE) { + this.$set(this.value.spec, 'securityParameters', { + cryptoOperation: this.value.spec.securityParameters.cryptoOperation, + sourceImageName: '', + sourceImageNamespace: neu + }); + } + }, 'value.spec.url'(neu) { const url = neu.trim(); @@ -297,15 +367,14 @@ export default { > @@ -331,7 +400,7 @@ export default { :tooltip="t('harvester.image.urlTip', {}, true)" /> -
+
+ +
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue index 4a992224ecc..a2565eacfdf 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/container.vue @@ -111,7 +111,7 @@ export default {
c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => { return { @@ -270,7 +281,7 @@ export default {
+
+ +
diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue index 8cd29f05f15..58fe7a5a95a 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/volume.vue @@ -8,11 +8,14 @@ import LabeledSelect from '@shell/components/form/LabeledSelect'; import { PVC, STORAGE_CLASS } from '@shell/config/types'; import { formatSi, parseSi } from '@shell/utils/units'; import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map'; +import { _VIEW } from '@shell/config/query-params'; +import LabelValue from '@shell/components/LabelValue'; +import { ucFirst } from '@shell/utils/string'; export default { name: 'HarvesterEditVolume', components: { - InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, + InputOrDisplay, Loading, LabeledInput, LabeledSelect, UnitInput, LabelValue }, props: { @@ -58,6 +61,13 @@ export default { }, computed: { + encryptionValue() { + return ucFirst(String(this.value.isEncrypted)); + }, + + isView() { + return this.mode === _VIEW; + }, pvcsResource() { const allPVCs = this.$store.getters['harvester/all'](PVC) || []; @@ -212,7 +222,7 @@ export default {
+
+ +
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 8c8fc52c740..c639a01ab49 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -166,6 +166,7 @@ harvester: reboot: Reboot forceStop: Force Stop tableHeaders: + imageEncryption: Encryption size: Size virtualSize: Virtual Size progress: Progress @@ -551,6 +552,10 @@ harvester: addContainer: Add Container setFirst: Set as root volume saveVolume: Update Volume + encryption: Encryption + lockTooltip: + all: All volumes are encrypted. + partial: Some volumes are encrypted. title: vmImage: Image Volume existingVolume: Existing Volume @@ -723,6 +728,8 @@ harvester: basics: Basics url: URL size: Size + isEncryption: Encryption + encryptionSecret: Encryption Secret virtualSize: Virtual Size urlTip: 'Supports the raw and qcow2 image formats which are supported by qemu. Bootable ISO images can also be used and are treated like raw images.' fileName: File Name @@ -731,6 +738,11 @@ harvester: sourceType: download: URL upload: File + clone: Clone + encrypt: Encrypt + decrypt: Decrypt + sourceImage: Source Image + cryptoOperation: Crypto Operation warning: uploading: |- {count, plural, @@ -1056,6 +1068,8 @@ harvester: storage: label: Storage useDefault: Use the default storage + volumeEncryption: Volume Encryption + secret: Secret migratable: label: Migratable numberOfReplicas: diff --git a/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue index 13dad01f333..0a49d1e7451 100644 --- a/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue +++ b/pkg/harvester/list/harvesterhci.io.virtualmachineimage.vue @@ -75,6 +75,22 @@ export default { +
diff --git a/pkg/harvester/list/harvesterhci.io.volume.vue b/pkg/harvester/list/harvesterhci.io.volume.vue index d261af9ee87..04f82a71213 100644 --- a/pkg/harvester/list/harvesterhci.io.volume.vue +++ b/pkg/harvester/list/harvesterhci.io.volume.vue @@ -158,6 +158,22 @@ export default { + diff --git a/pkg/harvester/list/kubevirt.io.virtualmachine.vue b/pkg/harvester/list/kubevirt.io.virtualmachine.vue index 9e556fc2781..8e63db59dd1 100644 --- a/pkg/harvester/list/kubevirt.io.virtualmachine.vue +++ b/pkg/harvester/list/kubevirt.io.virtualmachine.vue @@ -1,13 +1,10 @@ @@ -194,11 +207,21 @@ export default { @@ -215,6 +238,14 @@ export default { } } +.green-icon { + color: var(--success); +} + +.yellow-icon { + color: var(--warning); +} + .name-console { display: flex; align-items: center; diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index 2ffefb25c58..5d00739d0c5 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -420,8 +420,10 @@ export default { let size = '10Gi'; const imageResource = this.images.find( I => this.imageId === I.id); + const isIsoImage = /iso$/i.test(imageResource?.imageSuffix); const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize); + const isEncrypted = imageResource?.isEncrypted || false; if (isIsoImage) { bus = 'sata'; @@ -449,6 +451,7 @@ export default { storageClassName: '', image: this.imageId, volumeMode: 'Block', + isEncrypted }); } else { out = _disks.map( (DISK, index) => { @@ -525,7 +528,11 @@ export default { minExponent: 3, }); - const volumeStatus = this.pvcs.find(P => P.id === `${ this.value.metadata.namespace }/${ volumeName }`)?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR]; + const pvc = this.pvcs.find(P => P.id === `${ this.value.metadata.namespace }/${ volumeName }`); + + const volumeStatus = pvc?.relatedPV?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_ERROR]; + + const isEncrypted = pvc?.isEncrypted || false; return { id: randomStr(5), @@ -545,7 +552,8 @@ export default { hotpluggable, volumeStatus, dataSource, - namespace + namespace, + isEncrypted }; }); } diff --git a/pkg/harvester/models/harvester/persistentvolumeclaim.js b/pkg/harvester/models/harvester/persistentvolumeclaim.js index 141fb3eedfb..d960d7673a1 100644 --- a/pkg/harvester/models/harvester/persistentvolumeclaim.js +++ b/pkg/harvester/models/harvester/persistentvolumeclaim.js @@ -40,7 +40,7 @@ export default class HciPv extends HarvesterResource { return [ { action: 'exportImage', - enabled: this.hasAction('export'), + enabled: this.hasAction('export') && !this.isEncrypted, icon: 'icon icon-copy', label: this.t('harvester.action.exportImage') }, @@ -216,6 +216,10 @@ export default class HciPv extends HarvesterResource { return false; } + get isEncrypted() { + return Boolean(this.relatedPV?.spec.csi.volumeAttributes.encrypted) || false; + } + get longhornVolume() { const inStore = this.$rootGetters['currentProduct'].inStore; diff --git a/pkg/harvester/models/harvester/secret.js b/pkg/harvester/models/harvester/secret.js index 0c8536ddd5b..febb0a58761 100644 --- a/pkg/harvester/models/harvester/secret.js +++ b/pkg/harvester/models/harvester/secret.js @@ -2,6 +2,7 @@ import { clone } from '@shell/utils/object'; import { HCI } from '../../types'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester'; import Secret from '@shell/models/secret'; +import { NAMESPACE } from '@shell/config/types'; export default class HciSecret extends Secret { get _detailLocation() { @@ -52,6 +53,14 @@ export default class HciSecret extends Secret { return this.doneOverride; } + get isSystem() { + const inStore = this.$rootGetters['currentProduct'].inStore; + + const systemNs = this.$rootGetters[`${ inStore }/all`](NAMESPACE).filter(ns => ns.isSystem === true).map(ns => ns.metadata.name); + + return systemNs.includes(this.metadata.namespace); + } + get details() { const out = [ { diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index 4e16e4eb78a..5f9e44a16cd 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -12,6 +12,12 @@ import { stateDisplay, colorForState } from '@shell/plugins/dashboard-store/reso import { _CLONE } from '@shell/config/query-params'; import HarvesterResource from './harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; +import { CSI_SECRETS } from '@pkg/harvester/config/harvester-map'; + +const { + CSI_PROVISIONER_SECRET_NAME, + CSI_PROVISIONER_SECRET_NAMESPACE, +} = CSI_SECRETS; function isReady() { function getStatusConditionOfType(type, defaultValue = []) { @@ -128,6 +134,24 @@ export default class HciVmImage extends HarvesterResource { return stateDisplay(this.metadata.state.name); } + get encryptionSecret() { + const secretNS = this.spec.storageClassParameters[CSI_PROVISIONER_SECRET_NAMESPACE]; + const secretName = this.spec.storageClassParameters[CSI_PROVISIONER_SECRET_NAME]; + + if (secretNS && secretName) { + return `${ secretNS }/${ secretName }`; + } + + return ''; + } + + get isEncrypted() { + return this.spec.sourceType === 'clone' && + this.spec.securityParameters?.cryptoOperation === 'encrypt' && + !!this.spec.securityParameters?.sourceImageName && + !!this.spec.securityParameters?.sourceImageNamespace; + } + get imageMessage() { if (this.uploadError) { return ucFirst(this.uploadError); diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index 3adef26c56e..d0d35af2b03 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -575,6 +575,22 @@ export default class VirtVm extends HarvesterResource { return vmis.find(VMI => VMI.id === this.id); } + get encryptedVolumeType() { + const inStore = this.productInStore; + const pvcs = this.$rootGetters[`${ inStore }/all`](PVC); + + const volumeClaimNames = this.spec.template.spec.volumes?.map(v => v.persistentVolumeClaim?.claimName).filter(v => !!v) || []; + const volumes = pvcs.filter(pvc => volumeClaimNames.includes(pvc.metadata.name)); + + if (volumes.every(vol => vol.isEncrypted)) { + return 'all'; + } else if (volumes.some(vol => vol.isEncrypted)) { + return 'partial'; + } else { + return 'none'; + } + } + get isError() { const conditions = get(this.vmi, 'status.conditions'); const vmiFailureCond = findBy(conditions, 'type', 'Failure');