From c12d4b5fba540ab7b695fdf32fc08c23abaae94f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:33:01 +0800 Subject: [PATCH] feat: support advance option and creation from DataVolume (#776) (#777) * feat: support advance option and creation from DataVolume - advance option would let user change the accessMode/volumeMode - creation from DataVolume could supports various scenario with 3rd-party storage * feat: add data migration action on volume page * refactor: use show advanced options link instead of checkbox * feat: add feature flag * feat: add feature flag for dataMigration action --------- (cherry picked from commit 566e79eda5c08e84e431d5e048e4bb581e7d9aaf) Signed-off-by: Vicente Cheng Signed-off-by: Andy Lee Co-authored-by: freeze <1615081+Vicente-Cheng@users.noreply.github.com> Co-authored-by: Andy Lee --- pkg/harvester/config/feature-flags.js | 1 + pkg/harvester/config/labels-annotations.js | 1 + pkg/harvester/config/types.js | 5 + .../dialog/HarvesterDataMigrationDialog.vue | 183 ++++++++++++++++++ pkg/harvester/edit/harvesterhci.io.volume.vue | 152 +++++++++++++-- pkg/harvester/l10n/en-us.yaml | 15 ++ pkg/harvester/list/harvesterhci.io.volume.vue | 9 +- .../models/harvester/persistentvolumeclaim.js | 26 ++- 8 files changed, 372 insertions(+), 20 deletions(-) create mode 100644 pkg/harvester/dialog/HarvesterDataMigrationDialog.vue diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js index b6e0d944..36b85f62 100644 --- a/pkg/harvester/config/feature-flags.js +++ b/pkg/harvester/config/feature-flags.js @@ -65,6 +65,7 @@ const FEATURE_FLAGS = { 'vGPUAsPCIDevice', 'instanceManagerResourcesSetting', 'rwxNetworkSetting', + 'createPVCWithDataVolume' ], }; diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js index 98c28c3c..9f2f039a 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -80,4 +80,5 @@ export const HCI = { VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass', MAC_ADDRESS: 'harvesterhci.io/mac-address', NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map', + CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind', }; diff --git a/pkg/harvester/config/types.js b/pkg/harvester/config/types.js index 472ec034..71fef7a9 100644 --- a/pkg/harvester/config/types.js +++ b/pkg/harvester/config/types.js @@ -41,3 +41,8 @@ export const VMIMPORT_SOURCE_KINDS = { OPENSTACK: 'OpenstackSource', OVA: 'OvaSource', }; + +export const CDI_POPULATOR_KIND = { + VOLUME_IMPORT_SOURCE: 'VolumeImportSource', + VOLUME_CLONE_SOURCE: 'VolumeCloneSource', +}; diff --git a/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue b/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue new file mode 100644 index 00000000..c0c14f9c --- /dev/null +++ b/pkg/harvester/dialog/HarvesterDataMigrationDialog.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/pkg/harvester/edit/harvesterhci.io.volume.vue b/pkg/harvester/edit/harvesterhci.io.volume.vue index 5bdb5e47..285cdfab 100644 --- a/pkg/harvester/edit/harvesterhci.io.volume.vue +++ b/pkg/harvester/edit/harvesterhci.io.volume.vue @@ -9,8 +9,11 @@ import { LabeledInput } from '@components/Form/LabeledInput'; import NameNsDescription from '@shell/components/form/NameNsDescription'; import Conditions from '@shell/components/form/Conditions'; import { Banner } from '@components/Banner'; +import { Checkbox } from '@components/Form/Checkbox'; +import jsyaml from 'js-yaml'; +import { exceptionToErrorsArray } from '@shell/utils/error'; import { allHash } from '@shell/utils/promise'; -import { get } from '@shell/utils/object'; +import { clone, get } from '@shell/utils/object'; import { STORAGE_CLASS, LONGHORN, PV } from '@shell/config/types'; import { sortBy } from '@shell/utils/sort'; import { saferDump } from '@shell/utils/create-yaml'; @@ -33,6 +36,7 @@ export default { components: { Banner, + Checkbox, Tab, UnitInput, CruResource, @@ -90,14 +94,34 @@ export default { source, storage, imageId, - snapshots: [], - images: [], + showAdvanced: false, + createWithDataVolume: false, + snapshots: [], + images: [], GIBIBYTE }; }, created() { this.registerBeforeHook(this.willSave, 'willSave'); + + if (this.mode === _CREATE) { + const originalSaveYaml = this.value.saveYaml?.bind(this.value); + + this.value.saveYaml = async(yaml) => { + if (this.createWithDataVolume && this.isBlank) { + const parsed = jsyaml.load(yaml); + const dvObj = { ...parsed, type: 'cdi.kubevirt.io.datavolume' }; + const dataVolume = await this.$store.dispatch('harvester/create', dvObj); + + await dataVolume.save(); + + return dataVolume; + } + + return originalSaveYaml(yaml); + }; + } }, computed: { @@ -135,6 +159,10 @@ export default { return Object.values(VOLUME_MODE); }, + accessModeOptions() { + return ['ReadWriteOnce', 'ReadWriteMany', 'ReadOnlyMany']; + }, + imageOption() { return sortBy( this.images @@ -275,6 +303,10 @@ export default { return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion'); }, + isCreatePVCWithDataVolumeFeatureEnabled() { + return this.$store.getters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume'); + }, + isResizeDisabled() { return ( !this.isLHV2VolExpansionFeatureEnabled && @@ -341,6 +373,58 @@ export default { return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany']; }, + buildDataVolumeObj() { + const storage = { + storageClassName: this.value.spec.storageClassName, + resources: { requests: { storage: this.storage } }, + }; + + if (this.showAdvanced && this.value.spec.accessModes?.length > 0) { + storage.accessModes = this.value.spec.accessModes; + } + + if (this.showAdvanced && this.value.spec.volumeMode) { + storage.volumeMode = this.value.spec.volumeMode; + } + + return { + type: 'cdi.kubevirt.io.datavolume', + apiVersion: 'cdi.kubevirt.io/v1beta1', + kind: 'DataVolume', + metadata: { + name: this.value.metadata.name, + namespace: this.value.metadata.namespace, + annotations: this.value.metadata.annotations || {}, + labels: this.value.metadata.labels || {}, + }, + spec: { + source: { blank: {} }, + storage, + } + }; + }, + + async save(buttonDone) { + if (this.isCreate && this.isBlank && this.createWithDataVolume) { + try { + this.update(); + const dvObj = this.buildDataVolumeObj(); + const dataVolume = await this.$store.dispatch('harvester/create', dvObj); + + await dataVolume.save(); + buttonDone(true); + this.done(); + } catch (err) { + const error = err?.data || err; + + this['errors'] = exceptionToErrorsArray(error); + buttonDone(false); + } + } else { + await CreateEditView.methods.save.call(this, buttonDone); + } + }, + willSave() { this.update(); }, @@ -383,9 +467,17 @@ export default { this.update(); }, generateYaml() { - const out = saferDump(this.value); + this.update(); - return out; + if (this.isCreate && this.isBlank && this.createWithDataVolume) { + return saferDump(this.buildDataVolumeObj()); + } + + const plain = clone(this.value); + + delete plain.saveYaml; + + return saferDump(plain); }, } }; @@ -458,18 +550,6 @@ export default { @update:value="update" /> - - {{ t('harvester.volume.longhorn.disableResize') }} + +
+ +
+ + {{ showAdvanced ? t('harvester.volume.hideAdvanced') : t('harvester.volume.showAdvanced') }} + + + + + !pvc?.isGoldenImageVolume); + return this.rows.filter((pvc) => { + if (pvc?.isGoldenImageVolume || pvc?.isCDIPopulatorVolume) { + return false; + } + + return true; + }); }, headers() { return [ diff --git a/pkg/harvester/models/harvester/persistentvolumeclaim.js b/pkg/harvester/models/harvester/persistentvolumeclaim.js index 44f751f4..2e0ee11b 100644 --- a/pkg/harvester/models/harvester/persistentvolumeclaim.js +++ b/pkg/harvester/models/harvester/persistentvolumeclaim.js @@ -9,6 +9,7 @@ import { colorForState } from '@shell/plugins/dashboard-store/resource-class'; import { HCI, VOLUME_SNAPSHOT } from '../../types'; import HarvesterResource from '../harvester'; import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester'; +import { CDI_POPULATOR_KIND } from '../../config/types'; import { LVM_DRIVER } from './storage.k8s.io.storageclass'; const DEGRADED_ERRORS = ['replica scheduling failed', 'precheck new replica failed']; @@ -44,7 +45,7 @@ export default class HciPv extends HarvesterResource { const exportImageAction = { action: 'exportImage', enabled: this.hasAction('export') && !this.isEncrypted, - icon: 'icon icon-copy', + icon: 'icon icon-external-link', label: this.t('harvester.action.exportImage') }; const takeSnapshotAction = { @@ -77,10 +78,23 @@ export default class HciPv extends HarvesterResource { icon: 'icon icon-backup', label: this.t('harvester.action.cancelExpand') }, + { + action: 'dataMigration', + enabled: this.hasAction('dataMigration') && this.createPVCWithDataVolumeFeatureEnabled, + icon: 'icon icon-copy', + label: this.t('harvester.action.dataMigration') + }, ...out ]; } + dataMigration(resources = this) { + this.$dispatch('promptModal', { + resources, + component: 'HarvesterDataMigrationDialog' + }); + } + exportImage(resources = this) { this.$dispatch('promptModal', { resources, @@ -339,10 +353,20 @@ export default class HciPv extends HarvesterResource { return this?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true'; } + get isCDIPopulatorVolume() { + const kind = this?.metadata?.annotations?.[HCI_ANNOTATIONS.CDI_POPULATOR_KIND]; + + return kind === CDI_POPULATOR_KIND.VOLUME_IMPORT_SOURCE || kind === CDI_POPULATOR_KIND.VOLUME_CLONE_SOURCE; + } + get thirdPartyStorageFeatureEnabled() { return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage'); } + get createPVCWithDataVolumeFeatureEnabled() { + return this.$rootGetters['harvester-common/getFeatureEnabled']('createPVCWithDataVolume'); + } + get resourceExternalLink() { const host = window.location.host; const { params } = this.currentRoute();