From 40794d89a031e556d1be5d4d09d284b0426ae55c Mon Sep 17 00:00:00 2001 From: "andy.lee" Date: Tue, 18 Feb 2025 00:03:43 +0800 Subject: [PATCH] add third party storage UI change Signed-off-by: andy.lee --- pkg/harvester/config/harvester-cluster.js | 4 ++- pkg/harvester/config/labels-annotations.js | 1 + pkg/harvester/config/table-headers.js | 10 ++++++ .../harvesterhci.io.virtualmachineimage.vue | 31 +++++++++++------ .../VirtualMachineVolume/type/existing.vue | 4 +++ .../VirtualMachineVolume/type/vmImage.vue | 34 ++++++++++++++++++- .../edit/kubevirt.io.virtualmachine/index.vue | 6 ++++ pkg/harvester/l10n/en-us.yaml | 1 + pkg/harvester/list/harvesterhci.io.volume.vue | 5 ++- .../harvester/storage.k8s.io.storageclass.js | 4 +++ .../harvesterhci.io.virtualmachineimage.js | 12 ++++--- 11 files changed, 95 insertions(+), 17 deletions(-) diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 82e16199..731e1295 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -31,7 +31,8 @@ import { FINGERPRINT, IMAGE_PROGRESS, SNAPSHOT_TARGET_VOLUME, - IMAGE_VIRTUAL_SIZE + IMAGE_VIRTUAL_SIZE, + IMAGE_STORAGE_CLASS } from './table-headers'; const TEMPLATE = HCI.VM_VERSION; @@ -221,6 +222,7 @@ export function init($plugin, store) { STATE, NAME_COL, NAMESPACE_COL, + IMAGE_STORAGE_CLASS, IMAGE_PROGRESS, IMAGE_DOWNLOAD_SIZE, IMAGE_VIRTUAL_SIZE, diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js index a38071d4..0dd01da0 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -15,6 +15,7 @@ export const HCI = { TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName', CREATOR: 'harvesterhci.io/creator', OS: 'harvesterhci.io/os', + GOLDEN_IMAGE: 'harvesterhci.io/goldenImage', NETWORK_TYPE: 'network.harvesterhci.io/type', VM_NAME: 'harvesterhci.io/vmName', VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix', diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index 2c57035b..dc5234f6 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -88,3 +88,13 @@ export const MACHINE_POOLS = { align: 'center', width: 100, }; + +// The STORAGE_CLASS column in VM image list page +export const IMAGE_STORAGE_CLASS = { + name: 'imageStorageClass', + labelKey: 'harvester.tableHeaders.storageClass', + sort: 'imageStorageClass', + value: 'imageStorageClass', + align: 'left', + width: 200, +}; diff --git a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue index 4eb8a9d7..094dafc8 100644 --- a/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue +++ b/pkg/harvester/edit/harvesterhci.io.virtualmachineimage.vue @@ -16,7 +16,6 @@ import { STORAGE_CLASS } from '@shell/config/types'; import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image'; import { OS } from '../mixins/harvester-vm'; import { HCI } from '../types'; -import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass'; const ENCRYPT = 'encrypt'; const DECRYPT = 'decrypt'; @@ -61,9 +60,12 @@ export default { images: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.IMAGE }), storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }), }); + this['storageClassName'] = this.storageClassName || this.defaultStorageClassName(); this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE); + this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS); + const { securityParameters } = this.value.spec; // edit and view mode should show the source image @@ -98,14 +100,15 @@ export default { } return { - selectedImage: null, - images: [], - url: this.value.spec.url, - files: [], - resource: '', - headers: {}, - fileUrl: '', - file: '', + selectedImage: null, + storageClasses: [], + images: [], + url: this.value.spec.url, + files: [], + resource: '', + headers: {}, + fileUrl: '', + file: '', }; }, @@ -160,7 +163,7 @@ export default { const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses; return storages - .filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported. + .filter((s) => !s.parameters?.backingImage) .map((s) => { const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name; @@ -178,6 +181,7 @@ export default { set(nue) { this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue; + this.value.spec.targetStorageClassName = nue; } }, sourceImageOptions() { @@ -256,6 +260,13 @@ export default { } else { // URL / FILE / DECRYPT should use default storage class this.storageClassName = this.defaultStorageClassName(); } + }, + 'storageClassName'(neu) { + const storageClass = this.storages.find((s) => s.id === neu); + + if (storageClass) { + this.value.spec.backend = storageClass.isLonghornV1 ? 'backingimage' : 'cdi'; + } } }, diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue index cfbde927..8db5ef9b 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/existing.vue @@ -109,6 +109,10 @@ export default { return false; } + if (pvc.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true') { + return false; + } + if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) { isBeingUsed = false; } else if (pvc.attachVM) { diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue index cc5263a7..a6980b18 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineVolume/type/vmImage.vue @@ -90,7 +90,7 @@ export default { imagesOption() { return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => { return { - label: `${ I.metadata.namespace }/${ I.spec.displayName }`, + label: `${ I.metadata.namespace }/${ I.spec.displayName } (${ I.imageStorageClass } / ${ I.virtualSize })`, value: I.id }; }); @@ -118,6 +118,32 @@ export default { isLonghornV2() { return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2; + }, + + selectedImage() { + return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id); + }, + + imageVirtualSize() { + return this.selectedImage?.virtualSize; + }, + + diskSize() { + const size = this.value?.size || '0'; + + return `${ size.replace('Gi', '') } GiB`; + }, + + imageVirtualSizeInByte() { + return Math.max(this.selectedImage?.status?.size, this.selectedImage?.status?.virtualSize); + }, + + diskSizeInByte() { + return parseSi(this.value?.size || '0'); + }, + + showDiskTooSmallError() { + return this.imageVirtualSizeInByte > this.diskSizeInByte; } }, @@ -159,6 +185,7 @@ export default { methods: { update() { + this.value.hasDiskError = this.showDiskTooSmallError; this.$emit('update'); }, @@ -341,5 +368,10 @@ export default { class="mb-20" :label="value.volumeBackups.error.message" /> + diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/index.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/index.vue index e7f2bfd0..d30c0d31 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/index.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/index.vue @@ -195,7 +195,12 @@ export default { return true; }, + isValidationPassed() { + // check if any disk hasDiskError is true + const hasError = this.diskRows.some((disk) => disk.hasDiskError === true); + return !hasError; + }, showCpuPinningBanner() { if (!this.value.cpuPinningFeatureEnabled) { return false; @@ -489,6 +494,7 @@ export default { :resource="value" :cancel-event="true" :mode="mode" + :validation-passed="isValidationPassed" :can-yaml="isSingle ? true : false" :errors="errors" :generate-yaml="generateYaml" diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 2aae4357..6e727501 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -630,6 +630,7 @@ harvester: setFirst: Set as root volume saveVolume: Update Volume encryption: Encryption + vmImageVolumeTip: Disk size ({diskSize}) should greater than selected image virtual size ({imageVirtualSize}) lockTooltip: all: All volumes are encrypted. partial: Some volumes are encrypted. diff --git a/pkg/harvester/list/harvesterhci.io.volume.vue b/pkg/harvester/list/harvesterhci.io.volume.vue index 6a0fb0f4..356dceeb 100644 --- a/pkg/harvester/list/harvesterhci.io.volume.vue +++ b/pkg/harvester/list/harvesterhci.io.volume.vue @@ -9,6 +9,7 @@ import HarvesterVolumeState from '../formatters/HarvesterVolumeState'; import { allSettled } from '../utils/promise'; import { HCI, VOLUME_SNAPSHOT } from '../types'; +import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; const schema = { id: HCI.VOLUME, @@ -58,8 +59,10 @@ export default { if (!pvcSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post')) { this.$store.dispatch('type-map/configureType', { match: HCI.VOLUME, isCreatable: false }); } + // we only show the non golden image PVCs in the list + const pvcs = hash.pvcs.filter((pvc) => pvc.metadata.annotations[HCI_ANNOTATIONS.GOLDEN_IMAGE] !== 'true'); - this.rows = hash.pvcs; + this.rows = pvcs; }, data() { diff --git a/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js b/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js index 372f7a5f..65fafd54 100644 --- a/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js +++ b/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js @@ -62,6 +62,10 @@ export default class HciStorageClass extends StorageClass { return this.parameters?.encrypted === 'true'; } + get isLonghornV1() { + return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V1; + } + get isLonghornV2() { return this.provisioner === LONGHORN_DRIVER && this.longhornVersion === DATA_ENGINE_V2; } diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index ccc2126a..89935ccc 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -199,6 +199,10 @@ export default class HciVmImage extends HarvesterResource { return `${ this.metadata.namespace }/${ this.spec.displayName }`; } + get imageStorageClass() { + return this?.metadata?.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] || ''; + } + get imageMessage() { if (this.uploadError) { return ucFirst(this.uploadError); @@ -239,8 +243,8 @@ export default class HciVmImage extends HarvesterResource { return formatSi(size, { increment: 1024, maxPrecision: 2, - suffix: 'B', - firstSuffix: 'B', + suffix: 'iB', + firstSuffix: 'iB', }); } @@ -254,8 +258,8 @@ export default class HciVmImage extends HarvesterResource { return formatSi(virtualSize, { increment: 1024, maxPrecision: 2, - suffix: 'B', - firstSuffix: 'B', + suffix: 'iB', + firstSuffix: 'iB', }); }