diff --git a/k8s/migration/internal/controller/migrationplan_controller.go b/k8s/migration/internal/controller/migrationplan_controller.go index 9e8fbbe46..b0d289b6a 100644 --- a/k8s/migration/internal/controller/migrationplan_controller.go +++ b/k8s/migration/internal/controller/migrationplan_controller.go @@ -735,11 +735,6 @@ func (r *MigrationPlanReconciler) ReconcileMigrationPlanJob(ctx context.Context, if migrationtemplate.Spec.ProxyVMRef == nil { return ctrl.Result{}, errors.New("StorageCopyMethod is HotAdd but ProxyVMRef is not set in MigrationTemplate") } - if migrationplan.Spec.MigrationStrategy.Type == "hot" { - return ctrl.Result{}, errors.Errorf( - "StorageCopyMethod HotAdd does not support migration type 'hot' — use 'cold' or 'mock'", - ) - } proxyVM = &vjailbreakv1alpha1.ProxyVM{} if err := r.Get(ctx, types.NamespacedName{Name: migrationtemplate.Spec.ProxyVMRef.Name, Namespace: migrationtemplate.Namespace}, proxyVM); err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to get ProxyVM '%s'", migrationtemplate.Spec.ProxyVMRef.Name) diff --git a/ui/e2e/migration/helpers/migration.helpers.ts b/ui/e2e/migration/helpers/migration.helpers.ts index 56eaa4875..8cc6cf0fe 100644 --- a/ui/e2e/migration/helpers/migration.helpers.ts +++ b/ui/e2e/migration/helpers/migration.helpers.ts @@ -46,6 +46,7 @@ export const API = { podLogs: (namespace: string, podName: string) => `**/namespaces/${namespace}/pods/${podName}/log*`, rollingMigrationPlans: `**${V1A1}/rollingmigrationplans`, + proxyVMs: `**${V1A1}/proxyvms`, } export const ROUTES = { @@ -110,6 +111,13 @@ export async function selectPcdCluster(page: Page, clusterValue: string): Promis await page.getByRole('option', { name: clusterValue }).click() } +// Storage copy method radios (Standard / Storage Accelerated / vJailbreak Accelerated) +// live in NetworkAndStorageMappingStep. `label` matches the radio's accessible name, +// so callers can pass e.g. /vJailbreak Accelerated Copy/i for the Hot-Add option. +export async function selectStorageCopyMethod(page: Page, label: string | RegExp): Promise { + await page.getByRole('radio', { name: label }).check() +} + // ─── Route mocking helpers ──────────────────────────────────────────────────── type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' diff --git a/ui/e2e/migration/migration-options-toggle.spec.ts b/ui/e2e/migration/migration-options-toggle.spec.ts index 53f58e673..7452ca077 100644 --- a/ui/e2e/migration/migration-options-toggle.spec.ts +++ b/ui/e2e/migration/migration-options-toggle.spec.ts @@ -5,6 +5,7 @@ import { openMigrationDrawer, selectVmwareCluster, selectPcdCluster, + selectStorageCopyMethod, mockRoute, API, } from './helpers/migration.helpers' @@ -73,3 +74,52 @@ test.describe('MIGOPTS-001 — GH-2176 regression: Migration Options toggles res await expect(checkbox).not.toBeChecked() }) }) + +test.describe('MIGOPTS-002 — PR#2352: Hot-Add allows Hot migration data copy', () => { + test.beforeEach(async ({ page }) => { + await mockFormApis(page) + // Selecting the HotAdd radio triggers useProxyVMsQuery; stub it so the request + // resolves instead of hanging (an empty list is enough -- this test only checks + // that the Data copy method control unlocks, it doesn't need a Ready Proxy VM). + await mockRoute(page, API.proxyVMs, 'GET', { + apiVersion: 'vjailbreak.k8s.pf9.io/v1alpha1', + kind: 'ProxyVMList', + metadata: { continue: '', resourceVersion: '1' }, + items: [], + }) + }) + + test('Hot is a selectable data copy method once HotAdd storage copy is chosen', async ({ page }) => { + await goToMigrations(page) + await openMigrationDrawer(page) + await selectVmwareCluster(page, 'DC1-Cluster') + await selectPcdCluster(page, 'pcd-cluster-1') + await page.waitForFunction(() => { + const menus = document.querySelectorAll('.MuiMenu-root') + return Array.from(menus).every((m) => m.getAttribute('aria-hidden') === 'true') + }, { timeout: 5000 }) + + await selectStorageCopyMethod(page, /vJailbreak Accelerated Copy/i) + + const checkbox = page.getByRole('checkbox', { name: /data copy method/i }) + await checkbox.scrollIntoViewIfNeeded() + await expect(checkbox).toBeVisible() + await expect(checkbox).not.toBeDisabled() + + const box = await checkbox.boundingBox() + if (!box) throw new Error('checkbox not visible') + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2) + await expect(checkbox).toBeChecked() + + const dataCopySelect = page.getByTestId('data-copy-method-container').locator('[role="combobox"]') + await expect(dataCopySelect).not.toHaveAttribute('aria-disabled', 'true') + await dataCopySelect.click() + + const hotOption = page.getByRole('option', { name: 'Copy live VMs, then power off' }) + await expect(hotOption).toBeVisible() + await expect(hotOption).not.toHaveAttribute('aria-disabled', 'true') + await hotOption.click() + + await expect(dataCopySelect).toHaveText('Copy live VMs, then power off') + }) +}) diff --git a/ui/src/features/migration/steps/MigrationOptionsAlt.tsx b/ui/src/features/migration/steps/MigrationOptionsAlt.tsx index 528bf2834..8421e88fb 100644 --- a/ui/src/features/migration/steps/MigrationOptionsAlt.tsx +++ b/ui/src/features/migration/steps/MigrationOptionsAlt.tsx @@ -95,7 +95,6 @@ export default function MigrationOptionsAlt({ const { data: globalConfigMap } = useSettingsConfigMapQuery() const isStorageAcceleratedCopy = params?.storageCopyMethod === 'StorageAcceleratedCopy' - const isHotAdd = params?.storageCopyMethod === 'HotAdd' const hasWindowsVMSelected = useMemo(() => { if (!params?.vms || params.vms.length === 0) return false @@ -215,13 +214,6 @@ export default function MigrationOptionsAlt({ updateSelectedMigrationOptions ]) - useEffect(() => { - if (!isHotAdd) return - if (params?.dataCopyMethod !== 'cold' && params?.dataCopyMethod !== 'mock') { - onChange('dataCopyMethod')('cold') - } - }, [isHotAdd, onChange]) - // Fallback to 'cold' here is fine for rendering (a harmless flash before the real // value lands), but treating "not yet resolved" as "genuinely cold" would be wrong // for the destructive clearing effect below. @@ -310,12 +302,6 @@ export default function MigrationOptionsAlt({ - {isHotAdd && selectedMigrationOptions.dataCopyMethod && ( - - vJailbreak Accelerated Copy requires Cold or Mock copy. Other data copy methods - are not available. - - )}