feat(forklift): Changed based on review

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
Marcelo Fukumoto 2026-05-15 17:04:03 +02:00
parent 4d230ebd42
commit d075871afc
No known key found for this signature in database
GPG Key ID: 1CA12189625C2543
7 changed files with 448 additions and 99 deletions

View File

@ -1468,7 +1468,7 @@ export function init($plugin, store) {
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] }); configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
virtualType({ virtualType({
name: 'forklift-create', name: 'forklift-create',
labelKey: 'harvester.addons.forklift.labels.plan', labelKey: 'harvester.addons.forklift.labels.dashboard',
group: 'forklift', group: 'forklift',
namespaced: true, namespaced: true,
route: { route: {

View File

@ -1809,7 +1809,7 @@ harvester:
forklift: forklift:
labels: labels:
dashboard: Create dashboard: Migrations
provider: Providers provider: Providers
networkMap: Network Maps networkMap: Network Maps
storageMap: Storage Maps storageMap: Storage Maps
@ -1822,6 +1822,8 @@ harvester:
title: Configure Provider title: Configure Provider
description: Connect to your VMware vCenter or ESXi host to discover virtual machines for migration description: Connect to your VMware vCenter or ESXi host to discover virtual machines for migration
connectionDetails: Connection Details connectionDetails: Connection Details
providerSelect: Provider
createNew: Create new provider
requirementsTitle: Requirements requirementsTitle: Requirements
requirementsText: "To migrate virtual machines from VMWare, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Rancher." requirementsText: "To migrate virtual machines from VMWare, you will need a vCenter Server running version 6.5 or higher with a user account that has at least read permissions, and the source environment must be network-accessible from Rancher."
name: Name name: Name
@ -1836,6 +1838,7 @@ harvester:
testMissingFields: Please fill in all required fields before testing testMissingFields: Please fill in all required fields before testing
testTimeout: Connection test timed out — the provider did not report a status in time testTimeout: Connection test timed out — the provider did not report a status in time
save: Save Provider and Continue save: Save Provider and Continue
saveExisting: Check Provider and Continue
selectVms: selectVms:
title: Select Virtual Machines title: Select Virtual Machines
discovered: "{count} VMs discovered from" discovered: "{count} VMs discovered from"
@ -1872,17 +1875,22 @@ harvester:
title: Set Mappings title: Set Mappings
description: Map VMware networks and datastores to Harvester and Longhorn target resources description: Map VMware networks and datastores to Harvester and Longhorn target resources
save: Save Mappings and Continue save: Save Mappings and Continue
noTemplate: Start from scratch
networkMapping: networkMapping:
title: Network Mapping title: Network Mapping
description: Map VMware port groups to Harvester networks description: Map VMware port groups to Harvester networks
placeholder: Choose a Harvester network... placeholder: Choose a Harvester network...
template: Use existing network mapping as template
storageMapping: storageMapping:
title: Storage Mapping title: Storage Mapping
description: Map VMware datastores to Harvester storage classes description: Map VMware datastores to Harvester storage classes
placeholder: Choose a Longhorn Storage Volume placeholder: Choose a Longhorn Storage Volume
template: Use existing storage mapping as template
reviewMigration: reviewMigration:
title: Review Migration Plan title: Review Migration Plan
description: Confirm your migration settings before starting the transfer of VMs to the target cluster. description: Confirm your migration settings before starting the transfer of VMs to the target cluster.
planName: Name
planNamePlaceholder: "e.g. my-migration-plan"
migrationDetails: Migration Details migrationDetails: Migration Details
totalVms: Total VMs totalVms: Total VMs
vcpu: vCPU vcpu: vCPU

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import Loading from '@shell/components/Loading'; import Loading from '@shell/components/Loading';
import Masthead from '@shell/components/ResourceList/Masthead'; import Masthead from '@shell/components/ResourceList/Masthead';
@ -25,11 +25,17 @@ const schema = {
const store = useStore(); const store = useStore();
const { t } = useI18n(store); const { t } = useI18n(store);
const NO_TEMPLATE = '__none__';
const vms = ref([]); const vms = ref([]);
const harvesterNetworks = ref([]); const harvesterNetworks = ref([]);
const storageClasses = ref([]); const storageClasses = ref([]);
const networkEntries = ref([]); const networkEntries = ref([]);
const storageEntries = ref([]); const storageEntries = ref([]);
const allNetworkMaps = ref([]);
const allStorageMaps = ref([]);
const selectedNetworkTemplate = ref(NO_TEMPLATE);
const selectedStorageTemplate = ref(NO_TEMPLATE);
const errors = ref([]); const errors = ref([]);
const loading = ref(true); const loading = ref(true);
@ -66,6 +72,118 @@ const storageClassOptions = computed(() => {
return options; return options;
}); });
const networkTemplateOptions = computed(() => {
const options = [
{ label: t('harvester.addons.forklift.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.forklift.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) {
return;
}
networkEntries.value.forEach((entry) => {
const match = template.spec.map.find(
(m) => m.source?.id === entry.id || m.source?.name === entry.name
);
if (match?.destination) {
const dest = match.destination;
if (dest.type === 'pod') {
entry.target = 'pod';
} else if (dest.type === 'ignored') {
entry.target = 'ignored';
} else if (dest.type === 'multus' && dest.name) {
entry.target = dest.namespace ? `${ dest.namespace }/${ dest.name }` : dest.name;
}
}
});
});
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) {
return;
}
storageEntries.value.forEach((entry) => {
const match = template.spec.map.find(
(m) => m.source?.id === entry.id || m.source?.name === entry.name
);
if (match?.destination?.storageClass) {
entry.target = match.destination.storageClass;
}
});
});
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target)); 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 allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
@ -159,15 +277,7 @@ const formatStorageDetail = (entry) => {
return parts.join(' \u2022 '); return parts.join(' \u2022 ');
}; };
const cancel = async() => { const cancel = () => {
const inStore = store.getters['currentProduct'].inStore;
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
if (provider) {
await provider.remove();
}
currentRouter().push({ currentRouter().push({
name: `${ PRODUCT_NAME }-c-cluster-forklift`, name: `${ PRODUCT_NAME }-c-cluster-forklift`,
params: { params: {
@ -274,19 +384,7 @@ const saveMappings = async(buttonCb) => {
await storageMap.save(); await storageMap.save();
// Navigate to review page with all mapping data // Navigate to review page
const networkMappingsData = networkEntries.value.map((entry) => ({
source: entry.name,
target: entry.target === 'pod' ? 'Pod Networking' : entry.target === 'ignored' ? 'Ignored' : entry.target,
usedBy: entry.usedBy,
}));
const storageMappingsData = storageEntries.value.map((entry) => ({
source: entry.name,
target: entry.target,
usedBy: entry.usedBy,
}));
currentRouter().push({ currentRouter().push({
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`, name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
params: { params: {
@ -298,8 +396,6 @@ const saveMappings = async(buttonCb) => {
vms: currentRoute().query.vms, vms: currentRoute().query.vms,
networkMap: networkMap.metadata.name, networkMap: networkMap.metadata.name,
storageMap: storageMap.metadata.name, storageMap: storageMap.metadata.name,
networkMappings: JSON.stringify(networkMappingsData),
storageMappings: JSON.stringify(storageMappingsData),
} }
}); });
@ -325,6 +421,18 @@ const init = async() => {
storageClasses.value = []; storageClasses.value = [];
} }
try {
allNetworkMaps.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
} catch (e) {
allNetworkMaps.value = [];
}
try {
allStorageMaps.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
} catch (e) {
allStorageMaps.value = [];
}
try { try {
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }); await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
} catch (e) { } catch (e) {
@ -436,6 +544,14 @@ init();
</p> </p>
</div> </div>
<LabeledSelect
v-model:value="selectedNetworkTemplate"
:label="t('harvester.addons.forklift.configureMappings.networkMapping.template')"
:options="networkTemplateOptions"
:reduce="(opt) => opt.value"
class="mb-10"
/>
<RcItemCard <RcItemCard
v-for="entry in networkEntries" v-for="entry in networkEntries"
:id="entry._key" :id="entry._key"
@ -483,6 +599,14 @@ init();
</p> </p>
</div> </div>
<LabeledSelect
v-model:value="selectedStorageTemplate"
:label="t('harvester.addons.forklift.configureMappings.storageMapping.template')"
:options="storageTemplateOptions"
:reduce="(opt) => opt.value"
class="mb-10"
/>
<RcItemCard <RcItemCard
v-for="entry in storageEntries" v-for="entry in storageEntries"
:id="entry._key" :id="entry._key"

View File

@ -7,6 +7,7 @@ import { Checkbox } from '@components/Form/Checkbox';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import Masthead from '@shell/components/ResourceList/Masthead'; import Masthead from '@shell/components/ResourceList/Masthead';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { SCHEMA, SECRET } from '@shell/config/types'; import { SCHEMA, SECRET } from '@shell/config/types';
import { randomStr } from '@shell/utils/string'; import { randomStr } from '@shell/utils/string';
import { useI18n } from '@shell/composables/useI18n'; import { useI18n } from '@shell/composables/useI18n';
@ -24,10 +25,14 @@ const schema = {
metadata: { name: HCI.FORKLIFT_PROVIDER }, metadata: { name: HCI.FORKLIFT_PROVIDER },
}; };
const CREATE_NEW = '__create_new__';
const store = useStore(); const store = useStore();
const { t } = useI18n(store); const { t } = useI18n(store);
const allProviders = ref([]);
const allSecrets = ref([]); const allSecrets = ref([]);
const selectedProvider = ref(CREATE_NEW);
const providerName = ref(''); const providerName = ref('');
const url = ref(''); const url = ref('');
const username = ref(''); const username = ref('');
@ -43,23 +48,75 @@ const testPassed = ref(false);
const testing = ref(false); const testing = ref(false);
const saving = ref(false); const saving = ref(false);
const isExistingProvider = computed(() => selectedProvider.value !== CREATE_NEW);
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value); const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
// Reset test state when any field changes const providerOptions = computed(() => {
watch([providerName, url, username, password], () => { const options = [
testPassed.value = false; { label: t('harvester.addons.forklift.configureProvider.createNew'), value: CREATE_NEW }
testResult.value = null; ];
allProviders.value.forEach((p) => {
options.push({
label: p.metadata.name,
value: p.metadata.name,
});
}); });
const cancel = async() => { return options;
if (createdProvider.value) { });
await createdProvider.value.remove();
// When the provider selection changes, populate or clear the fields
watch(selectedProvider, (val) => {
testPassed.value = false;
testResult.value = null;
testError.value = null;
if (val === CREATE_NEW) {
providerName.value = '';
url.value = '';
username.value = '';
password.value = '';
skipTlsVerify.value = false;
createdProvider.value = null; createdProvider.value = null;
} else if (createdSecret.value) {
await createdSecret.value.remove();
createdSecret.value = null; createdSecret.value = null;
} else {
const provider = allProviders.value.find(
(p) => p.metadata.name === val && p.metadata.namespace === 'forklift'
);
if (provider) {
providerName.value = provider.metadata.name;
url.value = provider.spec?.url || '';
const secretRef = provider.spec?.secret;
if (secretRef) {
const secret = allSecrets.value.find(
(s) => s.metadata.name === secretRef.name && s.metadata.namespace === secretRef.namespace
);
if (secret?.data) {
username.value = atob(secret.data.user || '');
password.value = atob(secret.data.password || '');
skipTlsVerify.value = atob(secret.data.insecureSkipVerify || '') === 'true';
}
} }
createdProvider.value = provider;
}
}
});
// Reset test state when any field changes (only for create new)
watch([providerName, url, username, password], () => {
if (!isExistingProvider.value) {
testPassed.value = false;
testResult.value = null;
}
});
const cancel = () => {
currentRouter().push({ currentRouter().push({
name: `${ PRODUCT_NAME }-c-cluster-forklift`, name: `${ PRODUCT_NAME }-c-cluster-forklift`,
params: { params: {
@ -84,6 +141,70 @@ const testConnection = async(buttonCb) => {
const inStore = store.getters['currentProduct'].inStore; const inStore = store.getters['currentProduct'].inStore;
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
if (isExistingProvider.value) {
try {
const namespace = 'forklift';
const maxAttempts = 15;
let attempts = 0;
let connected = false;
let errorMsg = '';
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
const refreshed = await store.dispatch(`${ inStore }/find`, {
type: HCI.FORKLIFT_PROVIDER,
id: `${ namespace }/${ providerName.value }`,
opt: { force: true }
});
const conditions = refreshed?.status?.conditions || [];
const readyCondition = conditions.find((c) => c.type === 'Ready');
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
if (connectionCondition) {
if (connectionCondition.status === 'True') {
connected = true;
break;
} else {
errorMsg = connectionCondition.message || 'Connection failed';
break;
}
}
if (readyCondition) {
if (readyCondition.status === 'True') {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || 'Provider not ready';
break;
}
}
}
if (connected) {
testPassed.value = true;
testResult.value = t('harvester.addons.forklift.configureProvider.testSuccess');
testing.value = false;
buttonCb(true);
} else {
testError.value = errorMsg || t('harvester.addons.forklift.configureProvider.testTimeout');
testing.value = false;
buttonCb(false);
}
} catch (err) {
testError.value = err.message || t('harvester.addons.forklift.configureProvider.testFailed');
testing.value = false;
buttonCb(false);
}
return;
}
// For new providers, create provider + secret then poll
try { try {
// Delete previous provider (cascades to secret via ownerReferences) // Delete previous provider (cascades to secret via ownerReferences)
if (createdProvider.value) { if (createdProvider.value) {
@ -124,10 +245,7 @@ const testConnection = async(buttonCb) => {
metadata: { metadata: {
name: secretName, name: secretName,
namespace, namespace,
labels: { labels: { 'ui.forklift/created-for-resource-type': 'forklift.konveyor.io.provider' },
createdForProviderType: 'vsphere',
createdForResourceType: 'providers',
},
ownerReferences: [ ownerReferences: [
{ {
apiVersion: 'forklift.konveyor.io/v1beta1', apiVersion: 'forklift.konveyor.io/v1beta1',
@ -268,7 +386,12 @@ const saveProvider = async(buttonCb) => {
const init = async() => { const init = async() => {
const inStore = store.getters['currentProduct'].inStore; const inStore = store.getters['currentProduct'].inStore;
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, { type: SECRET }); allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
type: SECRET,
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
});
loading.value = false; loading.value = false;
}; };
@ -308,6 +431,18 @@ init();
</Banner> </Banner>
<div class="mb-20"> <div class="mb-20">
<LabeledSelect
v-model:value="selectedProvider"
:label="t('harvester.addons.forklift.configureProvider.providerSelect')"
:options="providerOptions"
:reduce="(opt) => opt.value"
/>
</div>
<div
v-if="!isExistingProvider"
class="mb-20"
>
<LabeledInput <LabeledInput
v-model:value="providerName" v-model:value="providerName"
:label="t('harvester.addons.forklift.configureProvider.name')" :label="t('harvester.addons.forklift.configureProvider.name')"
@ -320,6 +455,7 @@ init();
v-model:value="url" v-model:value="url"
:label="t('harvester.addons.forklift.configureProvider.urlLabel')" :label="t('harvester.addons.forklift.configureProvider.urlLabel')"
:placeholder="t('harvester.addons.forklift.configureProvider.urlPlaceholder')" :placeholder="t('harvester.addons.forklift.configureProvider.urlPlaceholder')"
:disabled="isExistingProvider"
required required
/> />
<p class="text-muted mt-5"> <p class="text-muted mt-5">
@ -332,6 +468,7 @@ init();
<LabeledInput <LabeledInput
v-model:value="username" v-model:value="username"
:label="t('harvester.addons.forklift.fields.username')" :label="t('harvester.addons.forklift.fields.username')"
:disabled="isExistingProvider"
required required
/> />
</div> </div>
@ -340,6 +477,7 @@ init();
v-model:value="password" v-model:value="password"
type="password" type="password"
:label="t('harvester.addons.forklift.fields.password')" :label="t('harvester.addons.forklift.fields.password')"
:disabled="isExistingProvider"
required required
/> />
</div> </div>
@ -349,6 +487,7 @@ init();
<Checkbox <Checkbox
v-model:value="skipTlsVerify" v-model:value="skipTlsVerify"
:label="t('harvester.addons.forklift.configureProvider.skipSsl')" :label="t('harvester.addons.forklift.configureProvider.skipSsl')"
:disabled="isExistingProvider"
/> />
<p class="text-muted ml-20"> <p class="text-muted ml-20">
{{ t('harvester.addons.forklift.configureProvider.skipSslHint') }} {{ t('harvester.addons.forklift.configureProvider.skipSslHint') }}
@ -407,10 +546,10 @@ init();
</button> </button>
<AsyncButton <AsyncButton
:disabled="!isFormValid || testing || saving" :disabled="!isFormValid || testing || saving"
:action-label="t('harvester.addons.forklift.configureProvider.save')" :action-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
:waiting-label="t('harvester.addons.forklift.configureProvider.save')" :waiting-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
:success-label="t('harvester.addons.forklift.configureProvider.save')" :success-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
:error-label="t('harvester.addons.forklift.configureProvider.save')" :error-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
@click="saveProvider" @click="saveProvider"
/> />
</div> </div>

View File

@ -200,6 +200,8 @@ init();
:groupable="false" :groupable="false"
:table-actions="false" :table-actions="false"
:search="false" :search="false"
default-sort-by="state"
:default-sort-descending="true"
key-field="_key" key-field="_key"
> >
<template #header-left> <template #header-left>

View File

@ -6,6 +6,7 @@ import Masthead from '@shell/components/ResourceList/Masthead';
import AsyncButton from '@shell/components/AsyncButton'; import AsyncButton from '@shell/components/AsyncButton';
import { Banner } from '@components/Banner'; import { Banner } from '@components/Banner';
import { RcItemCard } from '@components/RcItemCard'; import { RcItemCard } from '@components/RcItemCard';
import { LabeledInput } from '@components/Form/LabeledInput';
import MappingsCell from '../../../../components/MappingsCell'; import MappingsCell from '../../../../components/MappingsCell';
import { SCHEMA } from '@shell/config/types'; import { SCHEMA } from '@shell/config/types';
import { useI18n } from '@shell/composables/useI18n'; import { useI18n } from '@shell/composables/useI18n';
@ -29,6 +30,7 @@ const { t } = useI18n(store);
const vms = ref([]); const vms = ref([]);
const networkMappings = ref([]); const networkMappings = ref([]);
const storageMappings = ref([]); const storageMappings = ref([]);
const planName = ref('');
const errors = ref([]); const errors = ref([]);
const loading = ref(true); const loading = ref(true);
@ -89,15 +91,7 @@ const vmCards = computed(() => {
}); });
}); });
const cancel = async() => { const cancel = () => {
const inStore = store.getters['currentProduct'].inStore;
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
if (provider) {
await provider.remove();
}
currentRouter().push({ currentRouter().push({
name: `${ PRODUCT_NAME }-c-cluster-forklift`, name: `${ PRODUCT_NAME }-c-cluster-forklift`,
params: { params: {
@ -111,7 +105,51 @@ const startMigration = async(buttonCb) => {
const inStore = store.getters['currentProduct'].inStore; const inStore = store.getters['currentProduct'].inStore;
try { try {
const planName = `${ providerName.value }`; // Rename NetworkMap and StorageMap to use the plan name
const allNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
const allStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
const currentNetworkMap = allNetworkMaps.find((nm) => nm.metadata.name === networkMapName.value && nm.metadata.namespace === NAMESPACE);
const currentStorageMap = allStorageMaps.find((sm) => sm.metadata.name === storageMapName.value && sm.metadata.namespace === NAMESPACE);
const newNetworkMapName = `${ planName.value }-network-map`;
const newStorageMapName = `${ planName.value }-storage-map`;
// Recreate NetworkMap with new name
let finalNetworkMapName = networkMapName.value;
if (currentNetworkMap) {
const networkMapData = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_NETWORK_MAP,
metadata: {
name: newNetworkMapName,
namespace: NAMESPACE,
},
spec: currentNetworkMap.spec,
});
await networkMapData.save();
await currentNetworkMap.remove();
finalNetworkMapName = newNetworkMapName;
}
// Recreate StorageMap with new name
let finalStorageMapName = storageMapName.value;
if (currentStorageMap) {
const storageMapData = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_STORAGE_MAP,
metadata: {
name: newStorageMapName,
namespace: NAMESPACE,
},
spec: currentStorageMap.spec,
});
await storageMapData.save();
await currentStorageMap.remove();
finalStorageMapName = newStorageMapName;
}
const planSpec = { const planSpec = {
provider: { provider: {
@ -132,13 +170,13 @@ const startMigration = async(buttonCb) => {
network: { network: {
apiVersion: 'forklift.konveyor.io/v1beta1', apiVersion: 'forklift.konveyor.io/v1beta1',
kind: 'NetworkMap', kind: 'NetworkMap',
name: networkMapName.value, name: finalNetworkMapName,
namespace: NAMESPACE, namespace: NAMESPACE,
}, },
storage: { storage: {
apiVersion: 'forklift.konveyor.io/v1beta1', apiVersion: 'forklift.konveyor.io/v1beta1',
kind: 'StorageMap', kind: 'StorageMap',
name: storageMapName.value, name: finalStorageMapName,
namespace: NAMESPACE, namespace: NAMESPACE,
}, },
}, },
@ -153,7 +191,7 @@ const startMigration = async(buttonCb) => {
const plan = await store.dispatch(`${ inStore }/create`, { const plan = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_PLAN, type: HCI.FORKLIFT_PLAN,
metadata: { metadata: {
name: planName, name: planName.value,
namespace: NAMESPACE, namespace: NAMESPACE,
}, },
spec: planSpec, spec: planSpec,
@ -171,7 +209,7 @@ const startMigration = async(buttonCb) => {
}; };
// Create Migration owned by Plan // Create Migration owned by Plan
const migrationName = `${ planName }-migration-${ Math.random().toString(36).substring(2, 7) }`; const migrationName = `${ planName.value }-migration-${ Math.random().toString(36).substring(2, 7) }`;
const migration = await store.dispatch(`${ inStore }/create`, { const migration = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_MIGRATION, type: HCI.FORKLIFT_MIGRATION,
@ -184,7 +222,7 @@ const startMigration = async(buttonCb) => {
plan: { plan: {
apiVersion: 'forklift.konveyor.io/v1beta1', apiVersion: 'forklift.konveyor.io/v1beta1',
kind: 'Plan', kind: 'Plan',
name: planName, name: planName.value,
namespace: NAMESPACE, namespace: NAMESPACE,
}, },
}, },
@ -192,16 +230,21 @@ const startMigration = async(buttonCb) => {
await migration.save(); await migration.save();
// Set Plan as owner of Provider (Provider already owns NetworkMap, StorageMap, Secret) // Set Plan as owner of the newly created NetworkMap and StorageMap
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || []; const updatedNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE); const updatedStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
if (provider) { const newNetworkMap = updatedNetworkMaps.find((nm) => nm.metadata.name === finalNetworkMapName && nm.metadata.namespace === NAMESPACE);
provider.metadata.ownerReferences = [ const newStorageMap = updatedStorageMaps.find((sm) => sm.metadata.name === finalStorageMapName && sm.metadata.namespace === NAMESPACE);
...(provider.metadata.ownerReferences || []),
planOwnerRef, if (newNetworkMap) {
]; newNetworkMap.metadata.ownerReferences = [planOwnerRef];
await provider.save(); await newNetworkMap.save();
}
if (newStorageMap) {
newStorageMap.metadata.ownerReferences = [planOwnerRef];
await newStorageMap.save();
} }
currentRouter().push({ currentRouter().push({
@ -223,6 +266,8 @@ const init = async() => {
const inStore = store.getters['currentProduct'].inStore; const inStore = store.getters['currentProduct'].inStore;
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }).catch(() => {}); await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER }).catch(() => {});
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }).catch(() => {});
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }).catch(() => {});
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || []; const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE); const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
@ -250,24 +295,53 @@ const init = async() => {
} }
} }
const networkMappingsParam = currentRoute().query.networkMappings; // Build mapping display data from the actual NetworkMap/StorageMap resources
const allNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
const allStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
if (networkMappingsParam) { const networkMap = allNetworkMaps.find((nm) => nm.metadata.name === networkMapName.value && nm.metadata.namespace === NAMESPACE);
try { const storageMap = allStorageMaps.find((sm) => sm.metadata.name === storageMapName.value && sm.metadata.namespace === NAMESPACE);
networkMappings.value = JSON.parse(networkMappingsParam);
} catch (e) { if (networkMap?.spec?.map) {
networkMappings.value = []; networkMappings.value = networkMap.spec.map.map((m) => {
} const source = m.source?.name || m.source?.id || 'Unknown';
let target = '';
if (m.destination?.type === 'pod') {
target = 'Pod Networking';
} else if (m.destination?.type === 'ignored') {
target = 'Ignored';
} else if (m.destination?.type === 'multus' && m.destination?.name) {
target = m.destination.namespace ? `${ m.destination.namespace }/${ m.destination.name }` : m.destination.name;
} }
const storageMappingsParam = currentRoute().query.storageMappings; // Find which VMs use this network
const sourceId = m.source?.id;
const usedBy = vms.value
.filter((vm) => vm.networks?.some((n) => n.id === sourceId || n.name === m.source?.name))
.map((vm) => vm.name || vm.id);
if (storageMappingsParam) { return {
try { source, target, usedBy
storageMappings.value = JSON.parse(storageMappingsParam); };
} catch (e) { });
storageMappings.value = [];
} }
if (storageMap?.spec?.map) {
storageMappings.value = storageMap.spec.map.map((m) => {
const source = m.source?.name || m.source?.id || 'Unknown';
const target = m.destination?.storageClass || '';
// Find which VMs use this datastore
const sourceId = m.source?.id;
const usedBy = vms.value
.filter((vm) => vm.disks?.some((d) => d.datastore?.id === sourceId || d.datastore?.name === m.source?.name))
.map((vm) => vm.name || vm.id);
return {
source, target, usedBy
};
});
} }
loading.value = false; loading.value = false;
@ -295,6 +369,15 @@ init();
</template> </template>
</Masthead> </Masthead>
<div class="mb-20">
<LabeledInput
v-model:value="planName"
:label="t('harvester.addons.forklift.reviewMigration.planName')"
:placeholder="t('harvester.addons.forklift.reviewMigration.planNamePlaceholder')"
required
/>
</div>
<!-- Migration Details Summary --> <!-- Migration Details Summary -->
<div class="migration-details"> <div class="migration-details">
<h3 class="section-title"> <h3 class="section-title">
@ -412,6 +495,7 @@ init();
{{ t('generic.cancel') }} {{ t('generic.cancel') }}
</button> </button>
<AsyncButton <AsyncButton
:disabled="!planName"
:action-label="t('harvester.addons.forklift.reviewMigration.startMigration')" :action-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
:waiting-label="t('harvester.addons.forklift.reviewMigration.startMigration')" :waiting-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
:success-label="t('harvester.addons.forklift.reviewMigration.startMigration')" :success-label="t('harvester.addons.forklift.reviewMigration.startMigration')"

View File

@ -147,15 +147,7 @@ const onSelect = (rows) => {
selectedVMs.value = rows.map((r) => r._original); selectedVMs.value = rows.map((r) => r._original);
}; };
const cancel = async() => { const cancel = () => {
const inStore = store.getters['currentProduct'].inStore;
const providers = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
const found = providers.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === 'forklift');
if (found) {
await found.remove();
}
currentRouter().push({ currentRouter().push({
name: `${ PRODUCT_NAME }-c-cluster-forklift`, name: `${ PRODUCT_NAME }-c-cluster-forklift`,
params: { params: {