Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add encrypt image and decrypt image actions #1165

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions pkg/harvester/detail/harvesterhci.io.virtualmachineimage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 '-';
Expand All @@ -80,7 +113,7 @@ export default {
},

imageName() {
return this.value?.metadata?.annotations?.[HCI.IMAGE_NAME] || '-';
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
},
}
};
Expand Down Expand Up @@ -144,7 +177,7 @@ export default {
</div>
</div>

<div v-if="value.isEncrypted" class="row">
<div v-if="value.isEncrypted" class="row mb-20">
<div class="col span-12">
<div class="text-label">
{{ t('harvester.image.encryptionSecret') }}
Expand All @@ -161,6 +194,23 @@ export default {
</div>
</div>

<div v-if="isEncryptedOrDecrypted" class="row mb-20">
<div class="col span-12">
<div class="text-label">
{{ t('harvester.image.sourceImage') }}
</div>
<n-link v-if="sourceImageId && sourceImageLink" :to="sourceImageLink">
{{ sourceImageId }}
</n-link>
<span v-else-if="sourceImageId">
{{ sourceImageId }}
</span>
<span v-else class="text-muted">
&mdash;
</span>
</div>
</div>

<div v-if="errorMessage !== '-'" class="row">
<div class="col span-12">
<div>
Expand Down
54 changes: 35 additions & 19 deletions pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,40 @@ 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) {
this.value.metadata.generateName = 'image-';
}

return {
selectedImage: null,
selectedImage: image || null,
images: [],
url: this.value.spec.url,
files: [],
Expand Down Expand Up @@ -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;
}
Expand All @@ -184,15 +209,6 @@ export default {
},

watch: {
'value.metadata.namespace'(neu) {
a110605 marked this conversation as resolved.
Show resolved Hide resolved
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();

Expand Down Expand Up @@ -448,7 +464,7 @@ export default {

<LabeledSelect
v-if="value.spec.sourceType === 'clone'"
v-model="sourceImageName"
v-model="sourceImage"
:options="sourceImageOptions"
:label="t('harvester.image.sourceImage')"
:mode="mode"
Expand Down
6 changes: 4 additions & 2 deletions pkg/harvester/l10n/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ harvester:
warning: Warning
error: Error
action:
createVM: Create a Virtual Machine
createVM: Create Virtual Machine
start: Start
restart: Restart
softreboot: Soft Reboot
Expand All @@ -135,6 +135,8 @@ harvester:
deepClone: Clone
shallowClone: Clone Template
unpause: Unpause
encryptImage: Encrypt Image
decryptImage: Decrypt Image
ejectCDROM: Eject CD-ROM
editVMQuota: Edit VM Quota
launchFormTemplate: Launch instance from template
Expand Down Expand Up @@ -734,7 +736,7 @@ harvester:
urlTip: 'Supports the <code>raw</code> and <code>qcow2</code> image formats which are supported by <a href="https://www.qemu.org/docs/master/system/images.html#disk-image-file-formats" target="_blank">qemu</a>. Bootable ISO images can also be used and are treated like <code>raw</code> images.'
fileName: File Name
uploadFile: Upload File
source: Source
source: Source Type
sourceType:
download: URL
upload: File
Expand Down
48 changes: 48 additions & 0 deletions pkg/harvester/models/harvesterhci.io.virtualmachineimage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]: '' });
Expand Down Expand Up @@ -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);
Expand Down
Loading