From f0929100f008a6aacc123604df78562060e44358 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:46:52 +0800 Subject: [PATCH] feat: validate migration plan name against existing providers and plans (#1053) (#1054) Show an error banner and disable the Create and Start action when the migration plan name matches an existing provider or migration plan name. (cherry picked from commit 790cade04d1d99d5bfd1b78f8c7b81b8d5703162) Signed-off-by: Andy Lee Co-authored-by: Andy Lee --- .../vm-migration/ReviewMigrationStep.vue | 46 ++++++++++++++++++- pkg/harvester/l10n/en-us.yaml | 2 + 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue index 5311ddee..8bdb0a23 100644 --- a/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue +++ b/pkg/harvester/components/vm-migration/ReviewMigrationStep.vue @@ -35,6 +35,8 @@ const storageMappings = ref([]); const planName = ref(''); const targetNamespace = ref(''); const namespaceOptions = ref([]); +const existingProviderNames = ref([]); +const existingPlanNames = ref([]); const errors = ref([]); const loading = ref(true); @@ -44,6 +46,24 @@ targetNamespace.value = props.stepData.targetNamespace || ''; const NS = FORKLIFT_NAMESPACE; +const nameConflictError = computed(() => { + const name = (planName.value || '').trim(); + + if (!name) { + return ''; + } + + if (existingProviderNames.value.includes(name)) { + return t('harvester.addons.vmMigration.reviewMigration.planNameProviderConflict', { name }); + } + + if (existingPlanNames.value.includes(name)) { + return t('harvester.addons.vmMigration.reviewMigration.planNamePlanConflict', { name }); + } + + return ''; +}); + watch(planName, (val) => { props.stepData.planName = val; }, { immediate: true }); @@ -52,8 +72,8 @@ watch(targetNamespace, (val) => { props.stepData.targetNamespace = val; }); -watch([planName, targetNamespace], ([name, namespace]) => { - emit('ready', !!name && !!namespace); +watch([planName, targetNamespace, nameConflictError], () => { + emit('ready', !!planName.value && !!targetNamespace.value && !nameConflictError.value); }, { immediate: true }); const formatNetworkTarget = (target = '') => { @@ -282,6 +302,22 @@ const init = async() => { namespaceOptions.value = []; } + try { + await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }); + } catch (e) {} + + existingProviderNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || []) + .map((p) => p.metadata?.name) + .filter(Boolean); + + try { + await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PLAN }); + } catch (e) {} + + existingPlanNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PLAN) || []) + .map((p) => p.metadata?.name) + .filter(Boolean); + if (props.mappingEntries) { networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({ source: entry.name || entry.id, @@ -313,6 +349,12 @@ defineExpose({ startMigration: startMigrationAction });
+ +
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index fc6fc06f..6cc9d993 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -1984,6 +1984,8 @@ harvester: description: Confirm your migration settings before starting the transfer of VMs to the target cluster. planName: Name planNamePlaceholder: "e.g. my-migration-plan" + planNameProviderConflict: 'The name “{name}” is already used by an existing provider. Please input a different plan name.' + planNamePlanConflict: 'The name “{name}” is already used by an existing migration plan. Please input a different plan name.' migrationDetails: Migration Details migrationDetailsDescription: Name this plan and choose the namespace where migrated VMs will be created. totalVms: Total VMs