Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions k8s/migration/internal/controller/migrationplan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions ui/e2e/migration/helpers/migration.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<void> {
await page.getByRole('radio', { name: label }).check()
}

// ─── Route mocking helpers ────────────────────────────────────────────────────

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
Expand Down
50 changes: 50 additions & 0 deletions ui/e2e/migration/migration-options-toggle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
openMigrationDrawer,
selectVmwareCluster,
selectPcdCluster,
selectStorageCopyMethod,
mockRoute,
API,
} from './helpers/migration.helpers'
Expand Down Expand Up @@ -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')
})
})
22 changes: 2 additions & 20 deletions ui/src/features/migration/steps/MigrationOptionsAlt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -310,12 +302,6 @@ export default function MigrationOptionsAlt({
</SectionHeaderRow>
<Divider />

{isHotAdd && selectedMigrationOptions.dataCopyMethod && (
<Alert severity="info" sx={{ mt: 1 }}>
vJailbreak Accelerated Copy requires Cold or Mock copy. Other data copy methods
are not available.
</Alert>
)}
<OptionRow>
<OptionLeft>
<FormControlLabel
Expand Down Expand Up @@ -346,7 +332,7 @@ export default function MigrationOptionsAlt({
>
<Select
size="small"
disabled={!selectedMigrationOptions.dataCopyMethod && !isHotAdd}
disabled={!selectedMigrationOptions.dataCopyMethod}
labelId="source-item-label"
value={params?.dataCopyMethod || 'cold'}
onChange={(e) => {
Expand All @@ -358,11 +344,7 @@ export default function MigrationOptionsAlt({
fullWidth
>
{DATA_COPY_OPTIONS.map((item) => (
<MenuItem
key={item.value}
value={item.value}
disabled={isHotAdd && item.value !== 'cold' && item.value !== 'mock'}
>
<MenuItem key={item.value} value={item.value}>
{item.label}
</MenuItem>
))}
Expand Down
Loading
Loading