From 228369ad88e7b9696f00dc3dcde99c9c1dfe6e03 Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Tue, 24 Sep 2024 12:39:22 +0800 Subject: [PATCH] add encrypt image and decrypt image actions Signed-off-by: andy.lee --- .../index.vue | 58 +++++++++++++++++-- .../harvesterhci.io.virtualmachineimage.vue | 54 +++++++++++------ pkg/harvester/l10n/en-us.yaml | 6 +- .../harvesterhci.io.virtualmachineimage.js | 48 +++++++++++++++ 4 files changed, 141 insertions(+), 25 deletions(-) diff --git a/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue b/pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue index d67f2102..50e5cecd 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 4419f102..9755331f 100644 --- a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue +++ b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue @@ -62,12 +62,32 @@ export default { 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.value['spec'] = { sourceType: DOWNLOAD }; + this.value['spec'] = { sourceType: sourceType || DOWNLOAD }; + } + + if (image && cryptoOperation) { + this.value.spec.securityParameters = { + cryptoOperation, + sourceImageName: image.metadata.name, + sourceImageNamespace: image.metadata.namespace + } } if (!this.value.metadata.name) { @@ -75,7 +95,7 @@ export default { } return { - selectedImage: null, + selectedImage: image || null, images: [], url: this.value.spec.url, files: [], @@ -147,22 +167,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; } @@ -186,15 +211,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(); @@ -451,7 +467,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 7a5ce6a3..eb2dac17 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -57,6 +57,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, @@ -67,6 +81,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) { this.metadata['labels'] = { [HCI_ANNOTATIONS.OS_TYPE]: '', [HCI_ANNOTATIONS.IMAGE_SUFFIX]: '' }; @@ -151,6 +195,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);