From de7598b7af6f6586d43c7c92314d1da86397c7ee Mon Sep 17 00:00:00 2001
From: Marcelo Fukumoto
Date: Thu, 21 May 2026 18:13:43 +0200
Subject: [PATCH] feat(forklift): added the provider wizard
Signed-off-by: Marcelo Fukumoto
---
.../vm-migration/ConfigureMappingsStep.vue | 401 +++++++++---------
.../vm-migration/ConfigureProviderStep.vue | 98 ++++-
.../vm-migration/ReviewMigrationStep.vue | 3 +-
pkg/harvester/config/harvester-cluster.js | 6 +-
.../edit/forklift.konveyor.io.provider.vue | 24 ++
pkg/harvester/l10n/en-us.yaml | 5 +-
.../models/forklift.konveyor.io.plan.js | 2 +-
.../models/forklift.konveyor.io.provider.js | 22 +
.../_cluster/vm-migration/provider-wizard.vue | 281 ++++++++++++
.../vm-migration/vm-migration-wizard.vue | 10 +-
pkg/harvester/routing/harvester-routing.js | 5 +
11 files changed, 640 insertions(+), 217 deletions(-)
create mode 100644 pkg/harvester/edit/forklift.konveyor.io.provider.vue
create mode 100644 pkg/harvester/pages/c/_cluster/vm-migration/provider-wizard.vue
diff --git a/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue b/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue
index 54f1bbb6..e2bbb3a1 100644
--- a/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue
+++ b/pkg/harvester/components/vm-migration/ConfigureMappingsStep.vue
@@ -9,10 +9,13 @@ import { useI18n } from '@shell/composables/useI18n';
import { HCI } from '../../types';
const props = defineProps({
- providerName: { type: String, default: '' },
- provider: { type: Object, default: null },
- selectedVms: { type: Array, default: () => [] },
- stepData: { type: Object, required: true },
+ providerName: { type: String, default: '' },
+ provider: { type: Object, default: null },
+ selectedVms: { type: Array, default: () => [] },
+ stepData: { type: Object, required: true },
+ useAllProviderData: { type: Boolean, default: false },
+ existingNetworkMap: { type: Object, default: null },
+ existingStorageMap: { type: Object, default: null },
});
const emit = defineEmits(['ready']);
@@ -20,8 +23,6 @@ const emit = defineEmits(['ready']);
const store = useStore();
const { t } = useI18n(store);
-const NO_TEMPLATE = '__none__';
-
const vms = ref([]);
const harvesterNetworks = ref([]);
const storageClasses = ref([]);
@@ -29,8 +30,6 @@ const networkEntries = ref([]);
const storageEntries = ref([]);
const allNetworkMaps = ref([]);
const allStorageMaps = ref([]);
-const selectedNetworkTemplate = ref(NO_TEMPLATE);
-const selectedStorageTemplate = ref(NO_TEMPLATE);
const loading = ref(true);
// Restore from stepData
@@ -40,8 +39,6 @@ if (props.stepData.networkEntries.length > 0) {
if (props.stepData.storageEntries.length > 0) {
storageEntries.value = props.stepData.storageEntries;
}
-selectedNetworkTemplate.value = props.stepData.selectedNetworkTemplate;
-selectedStorageTemplate.value = props.stepData.selectedStorageTemplate;
const NAMESPACE = 'forklift';
@@ -75,75 +72,13 @@ const storageClassOptions = computed(() => {
return options;
});
-const networkTemplateOptions = computed(() => {
- const options = [
- { label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE }
- ];
-
- const currentIds = new Set(networkEntries.value.map((e) => e.id).filter(Boolean));
-
- const sorted = [...allNetworkMaps.value].sort(
- (a, b) => new Date(b.metadata.creationTimestamp) - new Date(a.metadata.creationTimestamp)
- );
-
- sorted.forEach((nm) => {
- const mapSources = (nm.spec?.map || []).map((m) => m.source?.id).filter(Boolean);
- const hasOverlap = mapSources.some((id) => currentIds.has(id));
-
- if (hasOverlap) {
- options.push({
- label: nm.metadata.name,
- value: nm.metadata.name,
- });
- }
- });
-
- return options;
-});
-
-const storageTemplateOptions = computed(() => {
- const options = [
- { label: t('harvester.addons.vmMigration.configureMappings.noTemplate'), value: NO_TEMPLATE }
- ];
-
- const currentIds = new Set(storageEntries.value.map((e) => e.id).filter(Boolean));
-
- const sorted = [...allStorageMaps.value].sort(
- (a, b) => new Date(b.metadata.creationTimestamp) - new Date(a.metadata.creationTimestamp)
- );
-
- sorted.forEach((sm) => {
- const mapSources = (sm.spec?.map || []).map((m) => m.source?.id).filter(Boolean);
- const hasOverlap = mapSources.some((id) => currentIds.has(id));
-
- if (hasOverlap) {
- options.push({
- label: sm.metadata.name,
- value: sm.metadata.name,
- });
- }
- });
-
- return options;
-});
-
-watch(selectedNetworkTemplate, (val) => {
- if (val === NO_TEMPLATE) {
- networkEntries.value.forEach((e) => {
- e.target = '';
- });
-
- return;
- }
-
- const template = allNetworkMaps.value.find((nm) => nm.metadata.name === val);
-
- if (!template?.spec?.map) {
+const applyNetworkMapTargets = (mapSpec) => {
+ if (!mapSpec) {
return;
}
networkEntries.value.forEach((entry) => {
- const match = template.spec.map.find(
+ const match = mapSpec.find(
(m) => m.source?.id === entry.id || m.source?.name === entry.name
);
@@ -159,25 +94,15 @@ watch(selectedNetworkTemplate, (val) => {
}
}
});
-});
+};
-watch(selectedStorageTemplate, (val) => {
- if (val === NO_TEMPLATE) {
- storageEntries.value.forEach((e) => {
- e.target = '';
- });
-
- return;
- }
-
- const template = allStorageMaps.value.find((sm) => sm.metadata.name === val);
-
- if (!template?.spec?.map) {
+const applyStorageMapTargets = (mapSpec) => {
+ if (!mapSpec) {
return;
}
storageEntries.value.forEach((entry) => {
- const match = template.spec.map.find(
+ const match = mapSpec.find(
(m) => m.source?.id === entry.id || m.source?.name === entry.name
);
@@ -185,12 +110,32 @@ watch(selectedStorageTemplate, (val) => {
entry.target = match.destination.storageClass;
}
});
-});
+};
+
+const applyDefaultNetworkMap = () => {
+ const defaultMap = allNetworkMaps.value.find(
+ (nm) => nm.metadata.name === `${ props.providerName }-network-map-default`
+ );
+
+ applyNetworkMapTargets(defaultMap?.spec?.map);
+};
+
+const applyDefaultStorageMap = () => {
+ const defaultMap = allStorageMaps.value.find(
+ (sm) => sm.metadata.name === `${ props.providerName }-storage-map-default`
+ );
+
+ applyStorageMapTargets(defaultMap?.spec?.map);
+};
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target));
const allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
const canSave = computed(() => {
+ if (props.useAllProviderData) {
+ return true;
+ }
+
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
allNetworksMapped.value && allStorageMapped.value;
});
@@ -211,13 +156,6 @@ watch(networkEntries, (val) => {
watch(storageEntries, (val) => {
props.stepData.storageEntries = val;
}, { deep: true });
-watch(selectedNetworkTemplate, (val) => {
- props.stepData.selectedNetworkTemplate = val;
-});
-watch(selectedStorageTemplate, (val) => {
- props.stepData.selectedStorageTemplate = val;
-});
-
const buildNetworkEntries = () => {
const networkMap = {};
@@ -285,6 +223,29 @@ const buildStorageEntries = () => {
storageEntries.value = Object.values(datastoreMap);
};
+const buildNetworkEntriesFromProvider = (networksData) => {
+ networkEntries.value = (Array.isArray(networksData) ? networksData : []).map((net) => ({
+ name: net.name || net.id,
+ id: net.id || '',
+ vlanId: net.vlanId || '',
+ target: '',
+ usedBy: [],
+ _key: `net-${ net.id || net.name }`,
+ }));
+};
+
+const buildStorageEntriesFromProvider = (datastoresData) => {
+ storageEntries.value = (Array.isArray(datastoresData) ? datastoresData : []).map((ds) => ({
+ name: ds.name || ds.id,
+ id: ds.id || '',
+ type: ds.type || '',
+ capacity: ds.capacity || 0,
+ target: '',
+ usedBy: [],
+ _key: `stor-${ ds.id || ds.name }`,
+ }));
+};
+
const formatStorageDetail = (entry) => {
const parts = [];
@@ -305,26 +266,8 @@ const formatStorageDetail = (entry) => {
return parts.join(' • ');
};
-const saveAndReturn = async() => {
- const inStore = store.getters['currentProduct'].inStore;
-
- const providerRef = {
- source: {
- apiVersion: 'forklift.konveyor.io/v1beta1',
- kind: 'Provider',
- name: props.providerName,
- namespace: NAMESPACE,
- },
- destination: {
- apiVersion: 'forklift.konveyor.io/v1beta1',
- kind: 'Provider',
- name: 'host',
- namespace: NAMESPACE,
- },
- };
-
- // Create NetworkMap
- const networkMapSpec = {
+const buildNetworkMapSpec = (providerRef) => {
+ return {
map: networkEntries.value.map((entry) => {
if (entry.target === 'pod') {
return {
@@ -355,6 +298,49 @@ const saveAndReturn = async() => {
}),
provider: providerRef,
};
+};
+
+const buildStorageMapSpec = (providerRef) => {
+ return {
+ map: storageEntries.value.map((entry) => ({
+ source: { name: entry.name, id: entry.id },
+ destination: { storageClass: entry.target },
+ })),
+ provider: providerRef,
+ };
+};
+
+const saveAndReturn = async() => {
+ const inStore = store.getters['currentProduct'].inStore;
+
+ const providerRef = {
+ source: {
+ apiVersion: 'forklift.konveyor.io/v1beta1',
+ kind: 'Provider',
+ name: props.providerName,
+ namespace: NAMESPACE,
+ },
+ destination: {
+ apiVersion: 'forklift.konveyor.io/v1beta1',
+ kind: 'Provider',
+ name: 'host',
+ namespace: NAMESPACE,
+ },
+ };
+
+ // Update existing maps in place (edit mode)
+ if (props.existingNetworkMap && props.existingStorageMap) {
+ props.existingNetworkMap.spec = buildNetworkMapSpec(providerRef);
+ await props.existingNetworkMap.save();
+
+ props.existingStorageMap.spec = buildStorageMapSpec(providerRef);
+ await props.existingStorageMap.save();
+
+ return {
+ networkMapName: props.existingNetworkMap.metadata.name,
+ storageMapName: props.existingStorageMap.metadata.name,
+ };
+ }
const providerOwnerRef = props.provider ? [{
apiVersion: 'forklift.konveyor.io/v1beta1',
@@ -367,32 +353,23 @@ const saveAndReturn = async() => {
const networkMap = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_NETWORK_MAP,
metadata: {
- name: `${ props.providerName }-network-map-${ Math.random().toString(36).substring(2, 7) }`,
+ name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
namespace: NAMESPACE,
ownerReferences: providerOwnerRef,
},
- spec: networkMapSpec,
+ spec: buildNetworkMapSpec(providerRef),
});
await networkMap.save();
- // Create StorageMap
- const storageMapSpec = {
- map: storageEntries.value.map((entry) => ({
- source: { name: entry.name, id: entry.id },
- destination: { storageClass: entry.target },
- })),
- provider: providerRef,
- };
-
const storageMap = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_STORAGE_MAP,
metadata: {
- name: `${ props.providerName }-storage-map-${ Math.random().toString(36).substring(2, 7) }`,
+ name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : Math.random().toString(36).substring(2, 7) }`,
namespace: NAMESPACE,
ownerReferences: providerOwnerRef,
},
- spec: storageMapSpec,
+ spec: buildStorageMapSpec(providerRef),
});
await storageMap.save();
@@ -429,9 +406,7 @@ const init = async() => {
allStorageMaps.value = [];
}
- vms.value = props.selectedVms;
-
- if (networkEntries.value.length === 0) {
+ if (networkEntries.value.length === 0 || storageEntries.value.length === 0) {
try {
const providerUid = props.provider?.metadata?.uid;
const providerType = props.provider?.spec?.type || 'vsphere';
@@ -442,53 +417,79 @@ const init = async() => {
fetch(`${ baseUrl }/datastores?detail=1`).then((r) => r.json()).catch(() => []),
]);
- const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
- map[n.id] = n.name;
-
- return map;
- }, {});
- const datastoreInfoMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => {
- map[d.id] = { name: d.name, type: d.type || '' };
-
- return map;
- }, {});
-
- vms.value = vms.value.map((vm) => {
- const resolved = { ...vm };
-
- if (resolved.networks) {
- resolved.networks = resolved.networks.map((n) => ({
- ...n,
- name: n.name || networkNameMap[n.id] || n.id,
- }));
+ if (props.useAllProviderData) {
+ if (networkEntries.value.length === 0) {
+ buildNetworkEntriesFromProvider(networksData);
}
-
- if (resolved.disks) {
- resolved.disks = resolved.disks.map((d) => {
- const dsInfo = d.datastore ? datastoreInfoMap[d.datastore.id] : null;
-
- return {
- ...d,
- datastore: d.datastore ? {
- ...d.datastore,
- name: d.datastore.name || dsInfo?.name || d.datastore.id,
- type: dsInfo?.type || '',
- } : d.datastore,
- };
- });
+ if (storageEntries.value.length === 0) {
+ buildStorageEntriesFromProvider(datastoresData);
}
+ } else {
+ vms.value = props.selectedVms;
- return resolved;
- });
+ const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
+ map[n.id] = n.name;
+
+ return map;
+ }, {});
+ const datastoreInfoMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => {
+ map[d.id] = { name: d.name, type: d.type || '' };
+
+ return map;
+ }, {});
+
+ vms.value = vms.value.map((vm) => {
+ const resolved = { ...vm };
+
+ if (resolved.networks) {
+ resolved.networks = resolved.networks.map((n) => ({
+ ...n,
+ name: n.name || networkNameMap[n.id] || n.id,
+ }));
+ }
+
+ if (resolved.disks) {
+ resolved.disks = resolved.disks.map((d) => {
+ const dsInfo = d.datastore ? datastoreInfoMap[d.datastore.id] : null;
+
+ return {
+ ...d,
+ datastore: d.datastore ? {
+ ...d.datastore,
+ name: d.datastore.name || dsInfo?.name || d.datastore.id,
+ type: dsInfo?.type || '',
+ } : d.datastore,
+ };
+ });
+ }
+
+ return resolved;
+ });
+
+ if (networkEntries.value.length === 0) {
+ buildNetworkEntries();
+ }
+ if (storageEntries.value.length === 0) {
+ buildStorageEntries();
+ }
+ }
} catch (e) {
- // Name resolution failed — entries will use IDs as fallback
+ // Data resolution failed — entries will use IDs as fallback
}
+ }
- buildNetworkEntries();
+ if (props.existingNetworkMap?.spec?.map) {
+ applyNetworkMapTargets(props.existingNetworkMap.spec.map);
+ } else {
+ applyDefaultNetworkMap();
}
- if (storageEntries.value.length === 0) {
- buildStorageEntries();
+
+ if (props.existingStorageMap?.spec?.map) {
+ applyStorageMapTargets(props.existingStorageMap.spec.map);
+ } else {
+ applyDefaultStorageMap();
}
+
loading.value = false;
};
@@ -516,14 +517,6 @@ init();
-
-
-
+
Used by: {{ entry.usedBy.join(', ') }}
@@ -572,14 +565,6 @@ init();
-
-
-
+
Used by: {{ entry.usedBy.join(', ') }}
@@ -626,9 +611,22 @@ init();