feat(forklift): small fixes after Copilot

Signed-off-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
This commit is contained in:
Marcelo Fukumoto 2026-05-28 22:57:38 +02:00
parent 5c7dd1953d
commit 2deb0aa254
No known key found for this signature in database
GPG Key ID: 1CA12189625C2543
8 changed files with 126 additions and 216 deletions

11
.vscode/settings.json vendored
View File

@ -5,5 +5,14 @@
".eslintcache": true, ".eslintcache": true,
".nyc_output": true, ".nyc_output": true,
"yarn-error.log": true "yarn-error.log": true
} },
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
]
} }

View File

@ -159,9 +159,17 @@ watch(selectedProvider, (val) => {
); );
if (secret?.data) { if (secret?.data) {
username.value = atob(secret.data.user || ''); const decode = (val) => {
password.value = atob(secret.data.password || ''); try {
skipTlsVerify.value = atob(secret.data.insecureSkipVerify || '') === 'true'; return atob(val || '');
} catch (e) {
return '';
}
};
username.value = decode(secret.data.user);
password.value = decode(secret.data.password);
skipTlsVerify.value = decode(secret.data.insecureSkipVerify) === 'true';
} }
} }
@ -195,6 +203,74 @@ watch(testing, (val) => {
emit('testing', val); emit('testing', val);
}, { immediate: true }); }, { immediate: true });
const pollProviderReady = async(name) => {
const inStore = store.getters['currentProduct'].inStore;
const namespace = FORKLIFT_NAMESPACE;
const maxAttempts = 15;
let attempts = 0;
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
const refreshed = await store.dispatch(`${ inStore }/find`, {
type: HCI.FORKLIFT_PROVIDER,
id: `${ namespace }/${ name }`,
opt: { force: true }
});
const conditions = refreshed?.status?.conditions || [];
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
const readyCondition = conditions.find((c) => c.type === 'Ready');
if (connectionCondition) {
if (connectionCondition.status === 'True') {
return { connected: true };
}
return { connected: false, errorMsg: connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed') };
}
if (readyCondition) {
if (readyCondition.status === 'True') {
return { connected: true };
} else if (readyCondition.status === 'False') {
return { connected: false, errorMsg: readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady') };
}
}
}
return { connected: false, errorMsg: '' };
};
const handlePollResult = ({ connected, errorMsg }, buttonCb) => {
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);
}
};
const cleanupCreatedResources = async() => {
if (createdProvider.value) {
try {
await createdProvider.value.remove();
} catch (e) {}
createdProvider.value = null;
createdSecret.value = null;
} else if (createdSecret.value) {
try {
await createdSecret.value.remove();
} catch (e) {}
createdSecret.value = null;
}
};
const testConnection = async(buttonCb) => { const testConnection = async(buttonCb) => {
testResult.value = null; testResult.value = null;
testError.value = null; testError.value = null;
@ -213,8 +289,6 @@ const testConnection = async(buttonCb) => {
// Edit mode: update existing provider URL + secret, then poll // Edit mode: update existing provider URL + secret, then poll
if (props.editMode && createdProvider.value) { if (props.editMode && createdProvider.value) {
try { try {
const namespace = FORKLIFT_NAMESPACE;
createdProvider.value.spec.url = url.value; createdProvider.value.spec.url = url.value;
await createdProvider.value.save(); await createdProvider.value.save();
@ -230,56 +304,7 @@ const testConnection = async(buttonCb) => {
await createdSecret.value.save(); await createdSecret.value.save();
} }
const maxAttempts = 15; handlePollResult(await pollProviderReady(providerName.value), buttonCb);
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 || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
if (readyCondition) {
if (readyCondition.status === 'True') {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
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) { } catch (err) {
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed'); testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
testing.value = false; testing.value = false;
@ -292,57 +317,7 @@ const testConnection = async(buttonCb) => {
// For existing providers, just poll for Ready/ConnectionTestSucceeded status // For existing providers, just poll for Ready/ConnectionTestSucceeded status
if (isExistingProvider.value) { if (isExistingProvider.value) {
try { try {
const namespace = FORKLIFT_NAMESPACE; handlePollResult(await pollProviderReady(providerName.value), buttonCb);
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 || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
if (readyCondition) {
if (readyCondition.status === 'True') {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
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) { } catch (err) {
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed'); testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
testing.value = false; testing.value = false;
@ -354,20 +329,11 @@ const testConnection = async(buttonCb) => {
// For new providers, create provider + secret then poll // For new providers, create provider + secret then poll
try { try {
// Delete previous provider (cascades to secret via ownerReferences) await cleanupCreatedResources();
if (createdProvider.value) {
await createdProvider.value.remove();
createdProvider.value = null;
createdSecret.value = null;
} else if (createdSecret.value) {
await createdSecret.value.remove();
createdSecret.value = null;
}
const namespace = FORKLIFT_NAMESPACE; const namespace = FORKLIFT_NAMESPACE;
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`; const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
// Create Provider first so we have its UID for the ownerReference on the Secret
const provider = await store.dispatch(`${ inStore }/create`, { const provider = await store.dispatch(`${ inStore }/create`, {
type: HCI.FORKLIFT_PROVIDER, type: HCI.FORKLIFT_PROVIDER,
metadata: { metadata: {
@ -387,7 +353,6 @@ const testConnection = async(buttonCb) => {
await provider.save(); await provider.save();
createdProvider.value = provider; createdProvider.value = provider;
// Create Secret with ownerReference already set (avoids an extra PUT)
const newSecret = await store.dispatch(`${ inStore }/create`, { const newSecret = await store.dispatch(`${ inStore }/create`, {
type: SECRET, type: SECRET,
metadata: { metadata: {
@ -417,79 +382,15 @@ const testConnection = async(buttonCb) => {
await newSecret.save(); await newSecret.save();
createdSecret.value = newSecret; createdSecret.value = newSecret;
const maxAttempts = 15; const result = await pollProviderReady(providerName.value);
let attempts = 0;
let connected = false;
let errorMsg = '';
while (attempts < maxAttempts) { if (!result.connected) {
await new Promise((resolve) => setTimeout(resolve, 2000)); await cleanupCreatedResources();
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 || t('harvester.addons.vmMigration.errors.connectionFailed');
break;
}
}
if (readyCondition) {
if (readyCondition.status === 'True') {
connected = true;
break;
} else if (readyCondition.status === 'False') {
errorMsg = readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady');
break;
}
}
} }
if (connected) { handlePollResult(result, buttonCb);
testPassed.value = true;
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
testing.value = false;
buttonCb(true);
} else {
if (createdProvider.value) {
await createdProvider.value.remove();
createdProvider.value = null;
createdSecret.value = null;
} else if (createdSecret.value) {
await createdSecret.value.remove();
createdSecret.value = null;
}
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
testing.value = false;
buttonCb(false);
}
} catch (err) { } catch (err) {
if (createdProvider.value) { await cleanupCreatedResources();
try {
await createdProvider.value.remove();
} catch (e) {}
createdProvider.value = null;
}
if (createdSecret.value) {
try {
await createdSecret.value.remove();
} catch (e) {}
createdSecret.value = null;
}
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed'); testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
testing.value = false; testing.value = false;
buttonCb(false); buttonCb(false);

View File

@ -15,8 +15,6 @@ const props = defineProps({
providerName: { type: String, default: '' }, providerName: { type: String, default: '' },
provider: { type: Object, default: null }, provider: { type: Object, default: null },
selectedVms: { type: Array, default: () => [] }, selectedVms: { type: Array, default: () => [] },
networkMapName: { type: String, default: '' },
storageMapName: { type: String, default: '' },
mappingEntries: { type: Object, default: null }, mappingEntries: { type: Object, default: null },
stepData: { type: Object, required: true }, stepData: { type: Object, required: true },
}); });

View File

@ -406,8 +406,7 @@ init();
</script> </script>
<template> <template>
<Loading v-if="refreshing" /> <Loading v-if="loading || refreshing" />
<Loading v-if="loading" />
<div <div
v-else v-else
class="select-vms-step" class="select-vms-step"

View File

@ -1821,6 +1821,7 @@ harvester:
failedLoadVms: Failed to load virtual machines failedLoadVms: Failed to load virtual machines
failedRefreshVms: Failed to refresh virtual machines failedRefreshVms: Failed to refresh virtual machines
failedLoadProviders: Failed to load providers failedLoadProviders: Failed to load providers
failedLoadProvider: "Failed to load provider: {error}"
failedResolveDetails: Failed to resolve network and datastore details failedResolveDetails: Failed to resolve network and datastore details
failedDecodeCredentials: Failed to decode provider credentials failedDecodeCredentials: Failed to decode provider credentials
connectionFailed: Connection failed connectionFailed: Connection failed
@ -1921,6 +1922,10 @@ harvester:
failed: Failed failed: Failed
migration: Migration migration: Migration
initializingMigration: Initializing migration initializingMigration: Initializing migration
finishedSuccessfully: Finished Successfully
step: "Step {index}"
vmCount: "{count} VMs"
vmId: "id: {id}"
configureMappings: configureMappings:
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

View File

@ -69,17 +69,17 @@ const rows = computed(() => {
overallProgress += (stepPct / 100) * stepWeight; overallProgress += (stepPct / 100) * stepWeight;
if (!currentStep) { if (!currentStep) {
currentStep = step.name || `Step ${ idx + 1 }`; currentStep = step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 });
} }
if (step.error && !errorMsg) { if (step.error && !errorMsg) {
const reasons = (step.error.reasons || []).join('; ') || t('harvester.addons.vmMigration.plan.states.error'); const reasons = (step.error.reasons || []).join('; ') || t('harvester.addons.vmMigration.plan.states.error');
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ reasons }`; errorMsg = `${ step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 }) }: ${ reasons }`;
} }
if (step.phase === 'Failed' && !errorMsg) { if (step.phase === 'Failed' && !errorMsg) {
errorMsg = `${ step.name || `Step ${ idx + 1 }` }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`; errorMsg = `${ step.name || t('harvester.addons.vmMigration.dashboard.progress.step', { index: idx + 1 }) }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
} }
} }
}); });
@ -215,7 +215,7 @@ init();
</div> </div>
</template> </template>
<template #cell:vmCount="{ row }"> <template #cell:vmCount="{ row }">
{{ (row.spec.vms || []).length }} VMs {{ t('harvester.addons.vmMigration.dashboard.progress.vmCount', { count: (row.spec.vms || []).length }) }}
</template> </template>
<template #cell:progress="{ row }"> <template #cell:progress="{ row }">
<div <div
@ -230,7 +230,7 @@ init();
<div class="vm-progress-header"> <div class="vm-progress-header">
<div class="vm-name-block"> <div class="vm-name-block">
<span class="vm-name">{{ vm.vmName }}</span> <span class="vm-name">{{ vm.vmName }}</span>
<span class="text-muted vm-id">id: {{ vm.vmId }}</span> <span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
</div> </div>
</div> </div>
<div class="vm-pct-block"> <div class="vm-pct-block">
@ -246,7 +246,7 @@ init();
v-if="vm.progress >= 100" v-if="vm.progress >= 100"
class="step-label text-muted" class="step-label text-muted"
> >
Finished Successfully {{ t('harvester.addons.vmMigration.dashboard.progress.finishedSuccessfully') }}
</div> </div>
<div <div
v-else-if="vm.errorMsg" v-else-if="vm.errorMsg"
@ -258,7 +258,7 @@ init();
v-else-if="vm.canceled" v-else-if="vm.canceled"
class="step-label text-muted" class="step-label text-muted"
> >
Canceled {{ t('harvester.addons.vmMigration.plan.states.canceled') }}
</div> </div>
<div <div
v-else-if="vm.currentStep" v-else-if="vm.currentStep"

View File

@ -203,13 +203,17 @@ const init = async() => {
); );
if (secret?.data) { if (secret?.data) {
try { const decode = (val) => {
stepData.provider.username = atob(secret.data.user || ''); try {
stepData.provider.password = atob(secret.data.password || ''); return atob(val || '');
stepData.provider.skipTlsVerify = atob(secret.data.insecureSkipVerify || '') === 'true'; } catch (e) {
} catch (e) { return '';
errors.value = [t('harvester.addons.vmMigration.errors.failedDecodeCredentials')]; }
} };
stepData.provider.username = decode(secret.data.user);
stepData.provider.password = decode(secret.data.password);
stepData.provider.skipTlsVerify = decode(secret.data.insecureSkipVerify) === 'true';
stepData.provider.createdSecret = secret; stepData.provider.createdSecret = secret;
} }
} }
@ -230,7 +234,7 @@ const init = async() => {
// Maps may not exist yet // Maps may not exist yet
} }
} catch (err) { } catch (err) {
errors.value = [`Failed to load provider: ${ err.message || err }`]; errors.value = [t('harvester.addons.vmMigration.errors.failedLoadProvider', { error: err.message || err })];
} }
initialLoading.value = false; initialLoading.value = false;

View File

@ -15,14 +15,11 @@ const { t } = useI18n(store);
const cruRef = ref(null); const cruRef = ref(null);
const providerStepRef = ref(null); const providerStepRef = ref(null);
const mappingsStepRef = ref(null);
const reviewStepRef = ref(null); const reviewStepRef = ref(null);
const providerName = ref(''); const providerName = ref('');
const provider = ref(null); const provider = ref(null);
const selectedVMs = ref([]); const selectedVMs = ref([]);
const networkMapName = ref('');
const storageMapName = ref('');
const errors = ref([]); const errors = ref([]);
const providerReady = ref(false); const providerReady = ref(false);
@ -263,7 +260,6 @@ const onCancel = () => {
</template> </template>
<template #configure-mappings> <template #configure-mappings>
<ConfigureMappingsStep <ConfigureMappingsStep
ref="mappingsStepRef"
:provider-name="providerName" :provider-name="providerName"
:provider="provider" :provider="provider"
:selected-vms="selectedVMs" :selected-vms="selectedVMs"
@ -277,8 +273,6 @@ const onCancel = () => {
:provider-name="providerName" :provider-name="providerName"
:provider="provider" :provider="provider"
:selected-vms="selectedVMs" :selected-vms="selectedVMs"
:network-map-name="networkMapName"
:storage-map-name="storageMapName"
:mapping-entries="stepData.mappings" :mapping-entries="stepData.mappings"
:step-data="stepData.review" :step-data="stepData.review"
@ready="onReviewReady" @ready="onReviewReady"