mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-08-17 05:09:51 +00:00
feat(forklift): Changed based on review
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
parent
4d230ebd42
commit
d075871afc
@ -1468,7 +1468,7 @@ export function init($plugin, store) {
|
||||
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
|
||||
virtualType({
|
||||
name: 'forklift-create',
|
||||
labelKey: 'harvester.addons.forklift.labels.plan',
|
||||
labelKey: 'harvester.addons.forklift.labels.dashboard',
|
||||
group: 'forklift',
|
||||
namespaced: true,
|
||||
route: {
|
||||
|
||||
@ -1809,7 +1809,7 @@ harvester:
|
||||
|
||||
forklift:
|
||||
labels:
|
||||
dashboard: Create
|
||||
dashboard: Migrations
|
||||
provider: Providers
|
||||
networkMap: Network Maps
|
||||
storageMap: Storage Maps
|
||||
@ -1822,6 +1822,8 @@ harvester:
|
||||
title: Configure Provider
|
||||
description: Connect to your VMware vCenter or ESXi host to discover virtual machines for migration
|
||||
connectionDetails: Connection Details
|
||||
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."
|
||||
name: Name
|
||||
@ -1836,6 +1838,7 @@ harvester:
|
||||
testMissingFields: Please fill in all required fields before testing
|
||||
testTimeout: Connection test timed out — the provider did not report a status in time
|
||||
save: Save Provider and Continue
|
||||
saveExisting: Check Provider and Continue
|
||||
selectVms:
|
||||
title: Select Virtual Machines
|
||||
discovered: "{count} VMs discovered from"
|
||||
@ -1872,17 +1875,22 @@ harvester:
|
||||
title: Set Mappings
|
||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||
save: Save Mappings and Continue
|
||||
noTemplate: Start from scratch
|
||||
networkMapping:
|
||||
title: Network Mapping
|
||||
description: Map VMware port groups to Harvester networks
|
||||
placeholder: Choose a Harvester network...
|
||||
template: Use existing network mapping as template
|
||||
storageMapping:
|
||||
title: Storage Mapping
|
||||
description: Map VMware datastores to Harvester storage classes
|
||||
placeholder: Choose a Longhorn Storage Volume
|
||||
template: Use existing storage mapping as template
|
||||
reviewMigration:
|
||||
title: Review Migration Plan
|
||||
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
|
||||
totalVms: Total VMs
|
||||
vcpu: vCPU
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import Loading from '@shell/components/Loading';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
@ -25,11 +25,17 @@ const schema = {
|
||||
const store = useStore();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const NO_TEMPLATE = '__none__';
|
||||
|
||||
const vms = ref([]);
|
||||
const harvesterNetworks = ref([]);
|
||||
const storageClasses = ref([]);
|
||||
const networkEntries = ref([]);
|
||||
const storageEntries = ref([]);
|
||||
const allNetworkMaps = ref([]);
|
||||
const allStorageMaps = ref([]);
|
||||
const selectedNetworkTemplate = ref(NO_TEMPLATE);
|
||||
const selectedStorageTemplate = ref(NO_TEMPLATE);
|
||||
const errors = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
@ -66,6 +72,118 @@ const storageClassOptions = computed(() => {
|
||||
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 allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
|
||||
|
||||
@ -159,15 +277,7 @@ const formatStorageDetail = (entry) => {
|
||||
return parts.join(' \u2022 ');
|
||||
};
|
||||
|
||||
const cancel = async() => {
|
||||
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();
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
@ -274,19 +384,7 @@ const saveMappings = async(buttonCb) => {
|
||||
|
||||
await storageMap.save();
|
||||
|
||||
// Navigate to review page with all mapping data
|
||||
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,
|
||||
}));
|
||||
|
||||
// Navigate to review page
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift-review-migration`,
|
||||
params: {
|
||||
@ -294,12 +392,10 @@ const saveMappings = async(buttonCb) => {
|
||||
cluster: store.getters['clusterId'],
|
||||
},
|
||||
query: {
|
||||
provider: providerName.value,
|
||||
vms: currentRoute().query.vms,
|
||||
networkMap: networkMap.metadata.name,
|
||||
storageMap: storageMap.metadata.name,
|
||||
networkMappings: JSON.stringify(networkMappingsData),
|
||||
storageMappings: JSON.stringify(storageMappingsData),
|
||||
provider: providerName.value,
|
||||
vms: currentRoute().query.vms,
|
||||
networkMap: networkMap.metadata.name,
|
||||
storageMap: storageMap.metadata.name,
|
||||
}
|
||||
});
|
||||
|
||||
@ -325,6 +421,18 @@ const init = async() => {
|
||||
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 {
|
||||
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||
} catch (e) {
|
||||
@ -436,6 +544,14 @@ init();
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="selectedNetworkTemplate"
|
||||
:label="t('harvester.addons.forklift.configureMappings.networkMapping.template')"
|
||||
:options="networkTemplateOptions"
|
||||
:reduce="(opt) => opt.value"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in networkEntries"
|
||||
:id="entry._key"
|
||||
@ -483,6 +599,14 @@ init();
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="selectedStorageTemplate"
|
||||
:label="t('harvester.addons.forklift.configureMappings.storageMapping.template')"
|
||||
:options="storageTemplateOptions"
|
||||
:reduce="(opt) => opt.value"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in storageEntries"
|
||||
:id="entry._key"
|
||||
|
||||
@ -7,6 +7,7 @@ import { Checkbox } from '@components/Form/Checkbox';
|
||||
import { Banner } from '@components/Banner';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { SCHEMA, SECRET } from '@shell/config/types';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
@ -24,10 +25,14 @@ const schema = {
|
||||
metadata: { name: HCI.FORKLIFT_PROVIDER },
|
||||
};
|
||||
|
||||
const CREATE_NEW = '__create_new__';
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n(store);
|
||||
|
||||
const allProviders = ref([]);
|
||||
const allSecrets = ref([]);
|
||||
const selectedProvider = ref(CREATE_NEW);
|
||||
const providerName = ref('');
|
||||
const url = ref('');
|
||||
const username = ref('');
|
||||
@ -43,23 +48,75 @@ const testPassed = ref(false);
|
||||
const testing = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const isExistingProvider = computed(() => selectedProvider.value !== CREATE_NEW);
|
||||
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||
|
||||
// Reset test state when any field changes
|
||||
watch([providerName, url, username, password], () => {
|
||||
testPassed.value = false;
|
||||
testResult.value = null;
|
||||
const providerOptions = computed(() => {
|
||||
const options = [
|
||||
{ label: t('harvester.addons.forklift.configureProvider.createNew'), value: CREATE_NEW }
|
||||
];
|
||||
|
||||
allProviders.value.forEach((p) => {
|
||||
options.push({
|
||||
label: p.metadata.name,
|
||||
value: p.metadata.name,
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const cancel = async() => {
|
||||
if (createdProvider.value) {
|
||||
await createdProvider.value.remove();
|
||||
createdProvider.value = null;
|
||||
} else if (createdSecret.value) {
|
||||
await createdSecret.value.remove();
|
||||
createdSecret.value = null;
|
||||
}
|
||||
// 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;
|
||||
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({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
@ -84,6 +141,70 @@ const testConnection = async(buttonCb) => {
|
||||
|
||||
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 {
|
||||
// Delete previous provider (cascades to secret via ownerReferences)
|
||||
if (createdProvider.value) {
|
||||
@ -124,10 +245,7 @@ const testConnection = async(buttonCb) => {
|
||||
metadata: {
|
||||
name: secretName,
|
||||
namespace,
|
||||
labels: {
|
||||
createdForProviderType: 'vsphere',
|
||||
createdForResourceType: 'providers',
|
||||
},
|
||||
labels: { 'ui.forklift/created-for-resource-type': 'forklift.konveyor.io.provider' },
|
||||
ownerReferences: [
|
||||
{
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
@ -268,7 +386,12 @@ const saveProvider = async(buttonCb) => {
|
||||
const init = async() => {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -308,6 +431,18 @@ init();
|
||||
</Banner>
|
||||
|
||||
<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
|
||||
v-model:value="providerName"
|
||||
:label="t('harvester.addons.forklift.configureProvider.name')"
|
||||
@ -320,6 +455,7 @@ init();
|
||||
v-model:value="url"
|
||||
:label="t('harvester.addons.forklift.configureProvider.urlLabel')"
|
||||
:placeholder="t('harvester.addons.forklift.configureProvider.urlPlaceholder')"
|
||||
:disabled="isExistingProvider"
|
||||
required
|
||||
/>
|
||||
<p class="text-muted mt-5">
|
||||
@ -332,6 +468,7 @@ init();
|
||||
<LabeledInput
|
||||
v-model:value="username"
|
||||
:label="t('harvester.addons.forklift.fields.username')"
|
||||
:disabled="isExistingProvider"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -340,6 +477,7 @@ init();
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
:label="t('harvester.addons.forklift.fields.password')"
|
||||
:disabled="isExistingProvider"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -349,6 +487,7 @@ init();
|
||||
<Checkbox
|
||||
v-model:value="skipTlsVerify"
|
||||
:label="t('harvester.addons.forklift.configureProvider.skipSsl')"
|
||||
:disabled="isExistingProvider"
|
||||
/>
|
||||
<p class="text-muted ml-20">
|
||||
{{ t('harvester.addons.forklift.configureProvider.skipSslHint') }}
|
||||
@ -407,10 +546,10 @@ init();
|
||||
</button>
|
||||
<AsyncButton
|
||||
:disabled="!isFormValid || testing || saving"
|
||||
:action-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:waiting-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:success-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:error-label="t('harvester.addons.forklift.configureProvider.save')"
|
||||
:action-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
||||
:waiting-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
||||
:success-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
||||
:error-label="isExistingProvider ? t('harvester.addons.forklift.configureProvider.saveExisting') : t('harvester.addons.forklift.configureProvider.save')"
|
||||
@click="saveProvider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -200,6 +200,8 @@ init();
|
||||
:groupable="false"
|
||||
:table-actions="false"
|
||||
:search="false"
|
||||
default-sort-by="state"
|
||||
:default-sort-descending="true"
|
||||
key-field="_key"
|
||||
>
|
||||
<template #header-left>
|
||||
|
||||
@ -6,6 +6,7 @@ import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { RcItemCard } from '@components/RcItemCard';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import MappingsCell from '../../../../components/MappingsCell';
|
||||
import { SCHEMA } from '@shell/config/types';
|
||||
import { useI18n } from '@shell/composables/useI18n';
|
||||
@ -29,6 +30,7 @@ const { t } = useI18n(store);
|
||||
const vms = ref([]);
|
||||
const networkMappings = ref([]);
|
||||
const storageMappings = ref([]);
|
||||
const planName = ref('');
|
||||
const errors = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
@ -89,15 +91,7 @@ const vmCards = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const cancel = async() => {
|
||||
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();
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
@ -111,7 +105,51 @@ const startMigration = async(buttonCb) => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
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 = {
|
||||
provider: {
|
||||
@ -132,13 +170,13 @@ const startMigration = async(buttonCb) => {
|
||||
network: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'NetworkMap',
|
||||
name: networkMapName.value,
|
||||
name: finalNetworkMapName,
|
||||
namespace: NAMESPACE,
|
||||
},
|
||||
storage: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'StorageMap',
|
||||
name: storageMapName.value,
|
||||
name: finalStorageMapName,
|
||||
namespace: NAMESPACE,
|
||||
},
|
||||
},
|
||||
@ -153,7 +191,7 @@ const startMigration = async(buttonCb) => {
|
||||
const plan = await store.dispatch(`${ inStore }/create`, {
|
||||
type: HCI.FORKLIFT_PLAN,
|
||||
metadata: {
|
||||
name: planName,
|
||||
name: planName.value,
|
||||
namespace: NAMESPACE,
|
||||
},
|
||||
spec: planSpec,
|
||||
@ -171,7 +209,7 @@ const startMigration = async(buttonCb) => {
|
||||
};
|
||||
|
||||
// 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`, {
|
||||
type: HCI.FORKLIFT_MIGRATION,
|
||||
@ -184,7 +222,7 @@ const startMigration = async(buttonCb) => {
|
||||
plan: {
|
||||
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||
kind: 'Plan',
|
||||
name: planName,
|
||||
name: planName.value,
|
||||
namespace: NAMESPACE,
|
||||
},
|
||||
},
|
||||
@ -192,16 +230,21 @@ const startMigration = async(buttonCb) => {
|
||||
|
||||
await migration.save();
|
||||
|
||||
// Set Plan as owner of Provider (Provider already owns NetworkMap, StorageMap, Secret)
|
||||
const allProviders = store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [];
|
||||
const provider = allProviders.find((p) => p.metadata.name === providerName.value && p.metadata.namespace === NAMESPACE);
|
||||
// Set Plan as owner of the newly created NetworkMap and StorageMap
|
||||
const updatedNetworkMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_NETWORK_MAP) || [];
|
||||
const updatedStorageMaps = store.getters[`${ inStore }/all`](HCI.FORKLIFT_STORAGE_MAP) || [];
|
||||
|
||||
if (provider) {
|
||||
provider.metadata.ownerReferences = [
|
||||
...(provider.metadata.ownerReferences || []),
|
||||
planOwnerRef,
|
||||
];
|
||||
await provider.save();
|
||||
const newNetworkMap = updatedNetworkMaps.find((nm) => nm.metadata.name === finalNetworkMapName && nm.metadata.namespace === NAMESPACE);
|
||||
const newStorageMap = updatedStorageMaps.find((sm) => sm.metadata.name === finalStorageMapName && sm.metadata.namespace === NAMESPACE);
|
||||
|
||||
if (newNetworkMap) {
|
||||
newNetworkMap.metadata.ownerReferences = [planOwnerRef];
|
||||
await newNetworkMap.save();
|
||||
}
|
||||
|
||||
if (newStorageMap) {
|
||||
newStorageMap.metadata.ownerReferences = [planOwnerRef];
|
||||
await newStorageMap.save();
|
||||
}
|
||||
|
||||
currentRouter().push({
|
||||
@ -223,6 +266,8 @@ const init = async() => {
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
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 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) {
|
||||
try {
|
||||
networkMappings.value = JSON.parse(networkMappingsParam);
|
||||
} catch (e) {
|
||||
networkMappings.value = [];
|
||||
}
|
||||
const networkMap = allNetworkMaps.find((nm) => nm.metadata.name === networkMapName.value && nm.metadata.namespace === NAMESPACE);
|
||||
const storageMap = allStorageMaps.find((sm) => sm.metadata.name === storageMapName.value && sm.metadata.namespace === NAMESPACE);
|
||||
|
||||
if (networkMap?.spec?.map) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return {
|
||||
source, target, usedBy
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const storageMappingsParam = currentRoute().query.storageMappings;
|
||||
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 || '';
|
||||
|
||||
if (storageMappingsParam) {
|
||||
try {
|
||||
storageMappings.value = JSON.parse(storageMappingsParam);
|
||||
} catch (e) {
|
||||
storageMappings.value = [];
|
||||
}
|
||||
// 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;
|
||||
@ -295,6 +369,15 @@ init();
|
||||
</template>
|
||||
</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 -->
|
||||
<div class="migration-details">
|
||||
<h3 class="section-title">
|
||||
@ -412,6 +495,7 @@ init();
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
:disabled="!planName"
|
||||
:action-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
:waiting-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
:success-label="t('harvester.addons.forklift.reviewMigration.startMigration')"
|
||||
|
||||
@ -147,15 +147,7 @@ const onSelect = (rows) => {
|
||||
selectedVMs.value = rows.map((r) => r._original);
|
||||
};
|
||||
|
||||
const cancel = async() => {
|
||||
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();
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
currentRouter().push({
|
||||
name: `${ PRODUCT_NAME }-c-cluster-forklift`,
|
||||
params: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user