@@ -85,7 +81,7 @@ const selectOptions = computed(() => {
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index bf1a8a9a..05f503b8 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -1893,7 +1893,7 @@ harvester:
providerSelect: Provider
createNew: Create new provider
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 Harvester Node."
name: Name
urlLabel: vCenter/ESXi URL
urlPlaceholder: "https://vcenter.example.com/sdk"
@@ -1965,7 +1965,7 @@ harvester:
description: Map VMware networks and datastores to Harvester and Longhorn target resources
save: Save Mappings and Continue
noTemplate: Start from scratch
- removeMap: Remove Map
+ removeMap: Remove mapping
networkMapping:
title: Network Mapping
description: Map VMware port groups to Harvester networks
@@ -1977,7 +1977,7 @@ harvester:
storageMapping:
title: Storage Mapping
description: Map VMware datastores to Harvester storage classes
- placeholder: Choose a Longhorn Storage Volume
+ placeholder: Choose a Harvester Storage Class
template: Use existing storage mapping as template
reviewMigration:
title: Review Migration Plan
diff --git a/pkg/harvester/pages/c/_cluster/vm-migration/index.vue b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue
index 10f7d789..0774d2af 100644
--- a/pkg/harvester/pages/c/_cluster/vm-migration/index.vue
+++ b/pkg/harvester/pages/c/_cluster/vm-migration/index.vue
@@ -179,8 +179,10 @@ const rows = computed(() => {
overallProgress = Math.round(overallProgress * 10) / 10;
- // If no step-level error but the plan itself is failed, surface it
- if (!errorMsg && plan.planFailed) {
+ // If no step-level error but the plan itself is failed, surface it.
+ // Skip VMs that already reached 100% (they finished successfully before
+ // the plan failed) so their bar stays green instead of turning red.
+ if (!errorMsg && plan.planFailed && overallProgress < 100) {
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
}
@@ -190,7 +192,11 @@ const rows = computed(() => {
const cluster = routeParams.cluster || store.getters['clusterId'];
const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean);
const targetVm = allVMs.value.find((item) => vmNameCandidates.includes(item.metadata?.name) && (!vmNamespace || item.metadata?.namespace === vmNamespace));
- const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planFailed && !plan.planCanceled && !!targetVm && !!product && !!cluster;
+ // A VM that individually reached 100% without error should be navigable
+ // even when the overall plan failed (other VMs in the plan may have
+ // failed); `overallProgress >= 100 && !errorMsg && targetVm` already
+ // guarantees this VM migrated successfully.
+ const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planCanceled && !!targetVm && !!product && !!cluster;
const vmDetailLocation = canNavigateToVm ? {
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
params: {
diff --git a/pkg/harvester/promptRemove/forklift.konveyor.io.plan.vue b/pkg/harvester/promptRemove/forklift.konveyor.io.plan.vue
index 87408672..e6ce207d 100644
--- a/pkg/harvester/promptRemove/forklift.konveyor.io.plan.vue
+++ b/pkg/harvester/promptRemove/forklift.konveyor.io.plan.vue
@@ -68,40 +68,28 @@ export default defineComponent({
return (this.value || []).map((plan) => {
const planName = plan?.metadata?.name || '-';
const namespace = plan?.metadata?.namespace || '';
- const networkMap = plan?.spec?.map?.network;
- const storageMap = plan?.spec?.map?.storage;
- const history = plan?.status?.migration?.history || [];
- const migrationsMap = new Map();
+ const migrations = existing[HCI.FORKLIFT_MIGRATION]
+ .filter((resource) => this.ownedByPlan(resource, plan))
+ .map((resource) => ({
+ name: resource?.metadata?.name,
+ namespace: resource?.metadata?.namespace || namespace,
+ }));
- history
- .map((entry) => ({
- name: entry?.migration?.name,
- namespace: entry?.migration?.namespace || namespace,
- }))
- .filter((entry) => !!entry.name)
- .forEach((entry) => {
- const matched = existing[HCI.FORKLIFT_MIGRATION].find((resource) => resource?.metadata?.name === entry.name && resource?.metadata?.namespace === entry.namespace);
-
- if (matched) {
- migrationsMap.set(`${ entry.namespace }/${ entry.name }`, entry);
- }
- });
-
- const matchedNetworkMap = networkMap?.name && existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => resource?.metadata?.name === networkMap.name && resource?.metadata?.namespace === (networkMap.namespace || namespace));
- const matchedStorageMap = storageMap?.name && existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => resource?.metadata?.name === storageMap.name && resource?.metadata?.namespace === (storageMap.namespace || namespace));
+ const matchedNetworkMap = existing[HCI.FORKLIFT_NETWORK_MAP].find((resource) => this.ownedByPlan(resource, plan));
+ const matchedStorageMap = existing[HCI.FORKLIFT_STORAGE_MAP].find((resource) => this.ownedByPlan(resource, plan));
return {
planName,
namespace,
- migrations: [...migrationsMap.values()],
+ migrations,
networkMap: matchedNetworkMap ? {
- namespace: networkMap.namespace || namespace,
- name: networkMap.name,
+ namespace: matchedNetworkMap?.metadata?.namespace || namespace,
+ name: matchedNetworkMap?.metadata?.name,
} : null,
storageMap: matchedStorageMap ? {
- namespace: storageMap.namespace || namespace,
- name: storageMap.name,
+ namespace: matchedStorageMap?.metadata?.namespace || namespace,
+ name: matchedStorageMap?.metadata?.name,
} : null,
};
});
@@ -125,53 +113,57 @@ export default defineComponent({
methods: {
resourceNames,
+ /**
+ * Determine whether a related resource (migration / network map / storage map)
+ * is owned by the given plan by inspecting its `metadata.ownerReferences`.
+ * Matches on `kind: Plan` and the plan name; when both sides expose a uid it
+ * must match too, to disambiguate same-named plans.
+ */
+ ownedByPlan(resource, plan) {
+ const planName = plan?.metadata?.name;
+ const planUid = plan?.metadata?.uid;
+ const owners = resource?.metadata?.ownerReferences || [];
+
+ if (!planName) {
+ return false;
+ }
+
+ return owners.some((owner) => owner?.kind === 'Plan' &&
+ owner?.name === planName &&
+ (!planUid || !owner?.uid || owner.uid === planUid));
+ },
+
buildDeleteTargets() {
+ const existing = this.existingRelatedResources;
const targets = new Map();
+ const addTarget = (type, resource) => {
+ const name = resource?.metadata?.name;
+ const ns = resource?.metadata?.namespace;
+
+ if (!name) {
+ return;
+ }
+
+ const key = `${ type }|${ ns }|${ name }`;
+
+ targets.set(key, {
+ type, name, namespace: ns
+ });
+ };
+
for (const plan of this.value || []) {
- const namespace = plan?.metadata?.namespace || '';
- const networkMap = plan?.spec?.map?.network;
- const storageMap = plan?.spec?.map?.storage;
- const history = plan?.status?.migration?.history || [];
+ existing[HCI.FORKLIFT_NETWORK_MAP]
+ .filter((resource) => this.ownedByPlan(resource, plan))
+ .forEach((resource) => addTarget(HCI.FORKLIFT_NETWORK_MAP, resource));
- if (networkMap?.name) {
- const ns = networkMap.namespace || namespace;
- const key = `${ HCI.FORKLIFT_NETWORK_MAP }|${ ns }|${ networkMap.name }`;
+ existing[HCI.FORKLIFT_STORAGE_MAP]
+ .filter((resource) => this.ownedByPlan(resource, plan))
+ .forEach((resource) => addTarget(HCI.FORKLIFT_STORAGE_MAP, resource));
- targets.set(key, {
- type: HCI.FORKLIFT_NETWORK_MAP,
- name: networkMap.name,
- namespace: ns,
- });
- }
-
- if (storageMap?.name) {
- const ns = storageMap.namespace || namespace;
- const key = `${ HCI.FORKLIFT_STORAGE_MAP }|${ ns }|${ storageMap.name }`;
-
- targets.set(key, {
- type: HCI.FORKLIFT_STORAGE_MAP,
- name: storageMap.name,
- namespace: ns,
- });
- }
-
- for (const entry of history) {
- const migrationName = entry?.migration?.name;
-
- if (!migrationName) {
- continue;
- }
-
- const migrationNs = entry?.migration?.namespace || namespace;
- const key = `${ HCI.FORKLIFT_MIGRATION }|${ migrationNs }|${ migrationName }`;
-
- targets.set(key, {
- type: HCI.FORKLIFT_MIGRATION,
- name: migrationName,
- namespace: migrationNs,
- });
- }
+ existing[HCI.FORKLIFT_MIGRATION]
+ .filter((resource) => this.ownedByPlan(resource, plan))
+ .forEach((resource) => addTarget(HCI.FORKLIFT_MIGRATION, resource));
}
return [...targets.values()];