diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue index 0776d940ebe..2e952cd1e5d 100644 --- a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue +++ b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue @@ -2,7 +2,8 @@ import CopyToClipboardText from '@shell/components/CopyToClipboardText'; import LabelValue from '@shell/components/LabelValue'; import { DESCRIPTION } from '@shell/config/labels-annotations'; -import { HCI } from '@pkg/harvester/config/labels-annotations'; +import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; +import { HCI } from '../../types'; import Tabbed from '@shell/components/Tabbed'; import Tab from '@shell/components/Tabbed/Tab'; import { findBy } from '@shell/utils/array'; @@ -31,10 +32,14 @@ export default { const inStore = this.$store.getters['currentProduct'].inStore; this.secrets = await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET }); + this.images = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }); }, data() { - return { secrets: [] }; + return { + secrets: [], + images: [] + }; }, computed: { @@ -64,6 +69,34 @@ export default { return this.value?.spec?.sourceType === 'upload'; }, + sourceImage() { + const { sourceImageName, sourceImageNamespace } = this.value?.spec?.securityParameters || {}; + + if (sourceImageNamespace && sourceImageName) { + const imageId = `${ sourceImageNamespace }/${ sourceImageName }`; + + return this.images.find(image => image.id === imageId); + } + + return null; + }, + + sourceImageLink() { + return this.sourceImage?.detailLocation; + }, + + sourceImageId() { + if (this.sourceImage) { + return this.sourceImage.displayNameWithNamespace; + } + + return ''; + }, + + isEncryptedOrDecrypted() { + return ['encrypt', 'decrypt'].includes(this.value?.spec?.securityParameters?.cryptoOperation); + }, + encryptionSecret() { if (!this.value.isEncrypted) { return '-'; @@ -80,7 +113,7 @@ export default { }, imageName() { - return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-'; + return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-'; }, } }; @@ -144,7 +177,7 @@ export default { -
+
{{ t('harvester.image.encryptionSecret') }} @@ -161,6 +194,23 @@ export default {
+
+
+
+ {{ t('harvester.image.sourceImage') }} +
+ + {{ sourceImageId }} + + + {{ sourceImageId }} + + + — + +
+
+
diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue index 7a3c8c5e2ad..f44cae4d9ca 100644 --- a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue +++ b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue @@ -60,12 +60,32 @@ export default { 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; + + const { securityParameters } = this.value.spec; + + // edit and view mode should show the source image + if (securityParameters) { + // image ns/name = image.id + const sourceImage = `${ securityParameters.sourceImageNamespace }/${ securityParameters.sourceImageName }`; + + this.selectedImage = this.images.find(image => image.id === sourceImage); + } }, data() { + // pass from Encrypt Image / Decrypt Image actions + const { image, sourceType, cryptoOperation } = this.$route.query || {}; + if ( !this.value.spec ) { - this.$set(this.value, 'spec', { sourceType: DOWNLOAD }); + this.$set(this.value, 'spec', { sourceType: sourceType || DOWNLOAD }); + } + + if (image && cryptoOperation) { + this.$set(this.value.spec, 'securityParameters', { + cryptoOperation, + sourceImageName: image.metadata.name, + sourceImageNamespace: image.metadata.namespace + }); } if (!this.value.metadata.name) { @@ -73,7 +93,7 @@ export default { } return { - selectedImage: null, + selectedImage: image || null, images: [], url: this.value.spec.url, files: [], @@ -145,22 +165,27 @@ export default { options = this.images.filter(image => image.isEncrypted); } - return options.map(image => image.spec.displayName); + return options.map(image => image.displayNameWithNamespace); }, - sourceImageName: { + sourceImage: { get() { - return this.selectedImage?.spec.displayName; + if (this.selectedImage) { + return this.selectedImage.displayNameWithNamespace; + } + + return ''; }, - set(imageDisplayName) { - this.selectedImage = this.images.find(i => i.spec.displayName === imageDisplayName); + set(neu) { + this.selectedImage = this.images.find(i => i.displayNameWithNamespace === neu); // sourceImageName should bring the name of the image this.value.spec.securityParameters.sourceImageName = this.selectedImage?.metadata.name || ''; + this.value.spec.securityParameters.sourceImageNamespace = this.selectedImage?.metadata.namespace || ''; } }, sourceType: { get() { if (this.value.spec.sourceType === CLONE) { - return this.value.spec.securityParameters.cryptoOperation; + return this.value.spec?.securityParameters?.cryptoOperation; } else { return this.value.spec.sourceType; } @@ -184,15 +209,6 @@ export default { }, 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(); @@ -448,7 +464,7 @@ export default { 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 uploadFile: Upload File - source: Source + source: Source Type sourceType: download: URL upload: File diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index 5f9e44a16cd..6fcb32dcd62 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -58,6 +58,20 @@ export default class HciVmImage extends HarvesterResource { label: this.t('harvester.action.createVM'), disabled: !this.isReady, }, + { + action: 'encryptImage', + enabled: !this.isEncrypted, + icon: 'icon icon-lock', + label: this.t('harvester.action.encryptImage'), + disabled: !this.isReady, + }, + { + action: 'decryptImage', + enabled: this.isEncrypted, + icon: 'icon icon-unlock', + label: this.t('harvester.action.decryptImage'), + disabled: !this.isReady, + }, { action: 'download', enabled: this.links?.download, @@ -68,6 +82,36 @@ export default class HciVmImage extends HarvesterResource { ]; } + encryptImage() { + const router = this.currentRouter(); + + router.push({ + name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, + params: { resource: HCI.IMAGE }, + query: { + image: this, + fromPage: HCI.IMAGE, + sourceType: 'clone', + cryptoOperation: 'encrypt' + } + }); + } + + decryptImage() { + const router = this.currentRouter(); + + router.push({ + name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`, + params: { resource: HCI.IMAGE }, + query: { + image: this, + fromPage: HCI.IMAGE, + sourceType: 'clone', + cryptoOperation: 'decrypt' + } + }); + } + applyDefaults(resources = this, realMode) { if (realMode !== _CLONE) { Vue.set(this.metadata, 'labels', { [HCI_ANNOTATIONS.OS_TYPE]: '', [HCI_ANNOTATIONS.IMAGE_SUFFIX]: '' }); @@ -152,6 +196,10 @@ export default class HciVmImage extends HarvesterResource { !!this.spec.securityParameters?.sourceImageNamespace; } + get displayNameWithNamespace() { + return `${ this.metadata.namespace }/${ this.spec.displayName }`; + } + get imageMessage() { if (this.uploadError) { return ucFirst(this.uploadError);