mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-05 05:36:05 +08:00
feat(forklift): added the provider wizard
Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
@@ -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();
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="selectedNetworkTemplate"
|
||||
:label="t('harvester.addons.vmMigration.configureMappings.networkMapping.template')"
|
||||
:options="networkTemplateOptions"
|
||||
:reduce="(opt) => opt.value"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in networkEntries"
|
||||
:id="entry._key"
|
||||
@@ -551,7 +544,7 @@ init();
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!useAllProviderData && entry.usedBy.length">
|
||||
<span class="used-by">
|
||||
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||
</span>
|
||||
@@ -572,14 +565,6 @@ init();
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LabeledSelect
|
||||
v-model:value="selectedStorageTemplate"
|
||||
:label="t('harvester.addons.vmMigration.configureMappings.storageMapping.template')"
|
||||
:options="storageTemplateOptions"
|
||||
:reduce="(opt) => opt.value"
|
||||
class="mb-10"
|
||||
/>
|
||||
|
||||
<RcItemCard
|
||||
v-for="entry in storageEntries"
|
||||
:id="entry._key"
|
||||
@@ -610,7 +595,7 @@ init();
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="!useAllProviderData && entry.usedBy.length">
|
||||
<span class="used-by">
|
||||
Used by: <b>{{ entry.usedBy.join(', ') }}</b>
|
||||
</span>
|
||||
@@ -626,9 +611,22 @@ init();
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.mappings-columns {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
|
||||
gap: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.mapping-column {
|
||||
@@ -650,7 +648,7 @@ init();
|
||||
.card-content-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -664,13 +662,23 @@ init();
|
||||
.mapping-source {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 160px;
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
.source-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.source-detail {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.used-by {
|
||||
@@ -686,7 +694,8 @@ init();
|
||||
}
|
||||
|
||||
.mapping-target {
|
||||
flex: 1;
|
||||
flex: 3;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ import { HCI } from '../../types';
|
||||
|
||||
const CREATE_NEW = '__create_new__';
|
||||
|
||||
const props = defineProps({ stepData: { type: Object, required: true } });
|
||||
const props = defineProps({
|
||||
stepData: { type: Object, required: true },
|
||||
createOnly: { type: Boolean, default: false },
|
||||
editMode: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['complete', 'ready', 'form-valid', 'testing']);
|
||||
|
||||
@@ -205,6 +209,85 @@ const testConnection = async(buttonCb) => {
|
||||
|
||||
const inStore = store.getters['currentProduct'].inStore;
|
||||
|
||||
// Edit mode: update existing provider URL + secret, then poll
|
||||
if (props.editMode && createdProvider.value) {
|
||||
try {
|
||||
const namespace = 'forklift';
|
||||
|
||||
createdProvider.value.spec.url = url.value;
|
||||
await createdProvider.value.save();
|
||||
|
||||
const secretRef = createdProvider.value.spec?.secret;
|
||||
|
||||
if (secretRef && createdSecret.value) {
|
||||
createdSecret.value.data = {
|
||||
user: btoa(username.value),
|
||||
password: btoa(password.value),
|
||||
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||
url: btoa(url.value),
|
||||
};
|
||||
await createdSecret.value.save();
|
||||
}
|
||||
|
||||
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.vmMigration.configureProvider.testSuccess');
|
||||
testing.value = false;
|
||||
buttonCb(true);
|
||||
} else {
|
||||
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||
testing.value = false;
|
||||
buttonCb(false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
||||
if (isExistingProvider.value) {
|
||||
try {
|
||||
@@ -456,7 +539,7 @@ defineExpose({ testConnection, clickTestButton });
|
||||
</Banner>
|
||||
|
||||
<div class="configure-provider-step-form">
|
||||
<div>
|
||||
<div v-if="!createOnly && !editMode">
|
||||
<LabeledSelect
|
||||
v-model:value="selectedProvider"
|
||||
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
|
||||
@@ -466,11 +549,12 @@ defineExpose({ testConnection, clickTestButton });
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isExistingProvider"
|
||||
v-if="!isExistingProvider || editMode"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="providerName"
|
||||
:label="t('harvester.addons.vmMigration.configureProvider.name')"
|
||||
:disabled="editMode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -480,7 +564,7 @@ defineExpose({ testConnection, clickTestButton });
|
||||
v-model:value="url"
|
||||
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
||||
:placeholder="t('harvester.addons.vmMigration.configureProvider.urlPlaceholder')"
|
||||
:disabled="isExistingProvider"
|
||||
:disabled="isExistingProvider && !editMode"
|
||||
required
|
||||
/>
|
||||
<p class="text-muted mt-5">
|
||||
@@ -493,7 +577,7 @@ defineExpose({ testConnection, clickTestButton });
|
||||
<LabeledInput
|
||||
v-model:value="username"
|
||||
:label="t('harvester.addons.vmMigration.fields.username')"
|
||||
:disabled="isExistingProvider"
|
||||
:disabled="isExistingProvider && !editMode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -502,7 +586,7 @@ defineExpose({ testConnection, clickTestButton });
|
||||
v-model:value="password"
|
||||
type="password"
|
||||
:label="t('harvester.addons.vmMigration.fields.password')"
|
||||
:disabled="isExistingProvider"
|
||||
:disabled="isExistingProvider && !editMode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -512,7 +596,7 @@ defineExpose({ testConnection, clickTestButton });
|
||||
<Checkbox
|
||||
v-model:value="skipTlsVerify"
|
||||
:label="t('harvester.addons.vmMigration.configureProvider.skipSsl')"
|
||||
:disabled="isExistingProvider"
|
||||
:disabled="isExistingProvider && !editMode"
|
||||
/>
|
||||
<p class="text-muted ml-20">
|
||||
{{ t('harvester.addons.vmMigration.configureProvider.skipSslHint') }}
|
||||
|
||||
@@ -395,8 +395,7 @@ defineExpose({ startMigration: startMigrationAction });
|
||||
display: grid;
|
||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px 128px;
|
||||
width: 747px;
|
||||
gap: 12px clamp(64px, 8vw, 128px);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user