Skip to content
Merged
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
39 changes: 38 additions & 1 deletion e2e/specs/app/dataset_management_actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,43 @@ test.describe('Dataset management actions', () => {
expect(requestedStates).toContain(null);
});

test('retries a quota allocation failure with visible admin controls and no unrelated ZFS changes', async ({ page }) => {
let updateCalls = 0;
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
await installHaveApiMock(page, {
user: { id: 1, login: 'admin', level: 99 },
handlers: {
'GET datasets/10': () => ownedDataset(),
'GET transaction_chains': () => ({ transaction_chains: [] }),
'PUT datasets/10': () => ++updateCalls === 1
? failEnvelope('Resource allocation error: no diskspace left')
: { dataset: ownedDataset({ refquota: 20480 }) },
},
});
await page.goto('/admin/datasets/10');
const override = page.getByTestId('dataset.manage.admin_override');
await expect(override).toBeVisible();
await expect(override).not.toBeChecked();
await expect(page.getByTestId('dataset.manage.sync')).toBeHidden();
await page.getByTestId('dataset.manage.refquota').fill('20');
const first = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/datasets/10'));
await page.getByTestId('dataset.manage.edit.submit').click();
expect((await first).postDataJSON()).toEqual({ dataset: { refquota: 20480 } });
await expect(page.getByTestId('dataset.manage')).toContainText('no diskspace left');
await expect(override).not.toBeChecked();
await override.check();
await page.getByTestId('dataset.manage.admin_lock_type').selectOption('not_less');
expect(updateCalls).toBe(1);
const retry = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/datasets/10'));
await page.getByTestId('dataset.manage.edit.submit').click();
expect((await retry).postDataJSON()).toEqual({
dataset: { refquota: 20480, admin_override: true, admin_lock_type: 'not_less' },
});
await expect(override).not.toBeChecked();
await expect(page.getByTestId('dataset.manage.admin_lock_type')).toHaveValue('');
await expect(page.getByTestId('dataset.manage.sync')).toBeHidden();
});

test('creates, edits, and deletes a dataset from the overview', async ({ page }) => {
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });

Expand Down Expand Up @@ -277,7 +314,7 @@ test.describe('Dataset management actions', () => {
await page.getByTestId('dataset.manage.delete.confirm.confirm').click();
await deleteReq;
expect(deleteCalls).toBe(1);
await expect(page).toHaveURL(/\/app\/datasets$/);
await expect(page).toHaveURL(/\/app\/vps\/300\/storage$/);
});

test('sends only quota fields and preserves untouched advanced properties on update', async ({ page }) => {
Expand Down
4 changes: 2 additions & 2 deletions e2e/specs/app/vps_detail_tabs_matrix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ test('@workflow-matrix @smoke VPS detail tabs expose storage, access, lifecycle,
await expect(page.getByText('Autostart priority', { exact: true })).toHaveCount(0);
await expect(page.getByText('Change reason', { exact: true })).toHaveCount(0);
await expect(page.getByText('Admin lock type', { exact: true })).toHaveCount(0);
await expect(page.getByText('Admin override', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0);

await page.getByRole('link', { name: /^Storage$/ }).click();
await expect(page).toHaveURL(/\/app\/vps\/123\/storage$/);
Expand Down Expand Up @@ -432,5 +432,5 @@ test('@workflow-matrix @pr-smoke @pr-smoke-mobile VPS admin overview keeps each
await expect(page.getByText('Autostart priority', { exact: true })).toBeVisible();
await expect(page.getByText('Change reason', { exact: true })).toBeVisible();
await expect(page.getByText('Admin lock type', { exact: true })).toBeVisible();
await expect(page.getByText('Admin override', { exact: true })).toBeVisible();
await expect(page.getByTestId('vps.config.admin_override')).toBeVisible();
});
45 changes: 45 additions & 0 deletions e2e/specs/app/vps_lifecycle_tab_actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,51 @@ async function installLifecycleMock(page: Page, options?: {
}

test.describe('@pr-smoke VPS lifecycle tab', () => {
test('retries a resource allocation failure only after an admin explicitly enables override', async ({ page }) => {
let updateCalls = 0;
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
await installLifecycleMock(page, {
updateVps: () => ++updateCalls === 1
? failEnvelope('Resource allocation error: no Memory left')
: { vps, _meta: { action_state_id: 506 } },
});
await page.goto('/admin/vps/123/config');
await expect(page.getByTestId('vps.config.admin_override')).toBeVisible();
await expect(page.getByTestId('vps.config.admin_override')).not.toBeChecked();
await page.getByRole('spinbutton', { name: /^Memory/ }).fill('8192');
await page.getByTestId('vps.config.header.save').click();
const dialog = page.getByTestId('vps.config.confirm');
const first = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123'));
await dialog.getByTestId('vps.config.confirm.confirm').click();
expect((await first).postDataJSON()).toEqual({ vps: { memory: 8192 } });
await expect(dialog.getByTestId('vps.config.confirm.error')).toContainText('no Memory left');
await expect(dialog.getByTestId('vps.config.confirm.admin_override')).not.toBeChecked();
await dialog.getByTestId('vps.config.confirm.admin_override').check();
await expect(dialog.getByTestId('vps.config.confirm.error')).toHaveCount(0);
expect(updateCalls).toBe(1);
await page.screenshot({ path: test.info().outputPath('resource-review.png') });
const retry = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123'));
await dialog.getByTestId('vps.config.confirm.confirm').click();
expect((await retry).postDataJSON()).toEqual({ vps: { memory: 8192, admin_override: true } });
await expect(dialog).toBeHidden();
await expect(page.getByTestId('vps.config.admin_override')).not.toBeChecked();
});

test('does not expose resource override to a member', async ({ page }) => {
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
await installLifecycleMock(page, { user: { id: 7, login: 'owner', level: 1 } });
await page.goto('/app/vps/123/config');
await page.getByRole('spinbutton', { name: /^Memory/ }).fill('4096');
await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0);
await page.getByTestId('vps.config.header.save').click();
const dialog = page.getByTestId('vps.config.confirm');
await expect(dialog).toBeVisible();
await expect(dialog.getByTestId('vps.config.confirm.admin_override')).toHaveCount(0);
const request = page.waitForRequest(r => r.method() === 'PUT' && r.url().includes('/vpses/123'));
await dialog.getByTestId('vps.config.confirm.confirm').click();
expect((await request).postDataJSON()).toEqual({ vps: { memory: 4096 } });
});

test('@pr-smoke-mobile keeps rejected VPS configuration saves inside the review dialog and allows retry', async ({ page }) => {
let updateCalls = 0;
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
Expand Down
131 changes: 68 additions & 63 deletions e2e/specs/app/vps_storage_tab_mounts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,72 +50,77 @@ function mountItemControl(page: Page, mountId: number, control: 'dataset' | 'del
}

test.describe('@smoke VPS storage tab mounts', () => {
test('lets an admin resize the VPS root SSD live from the configuration entrypoint', async ({ page }) => {
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
let vpsUpdateCount = 0;

await installHaveApiMock(page, {
user: { id: 1, login: 'admin', level: 99 },
handlers: {
'GET vpses/123': () => ({ vps }),
'GET datasets/10': () => ({ dataset }),
'GET ip_addresses': () => ({ ip_addresses: [] }),
'GET transaction_chains': () => ({ transaction_chains: [] }),
'GET vpses/123/mounts': () => ({ mounts: [] }),
'GET dns_resolvers': () => ({ dns_resolvers: [] }),
'GET user_namespace_maps': () => ({ user_namespace_maps: [] }),
'PUT vpses/123': () => {
vpsUpdateCount += 1;
return { vps };
},
'PUT datasets/10': () => ({ _meta: { action_state_id: 905 } }),
'GET action_states/905': () => ({
action_state: {
id: 905,
label: 'Resize root dataset',
status: true,
finished: false,
current: 0,
total: 1,
for (const override of [false, true]) {
test(`lets an admin resize the VPS root SSD live from the configuration entrypoint (override: ${override})`, async ({ page }) => {
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
let vpsUpdateCount = 0;

await installHaveApiMock(page, {
user: { id: 1, login: 'admin', level: 99 },
handlers: {
'GET vpses/123': () => ({ vps }),
'GET datasets/10': () => ({ dataset }),
'GET ip_addresses': () => ({ ip_addresses: [] }),
'GET transaction_chains': () => ({ transaction_chains: [] }),
'GET vpses/123/mounts': () => ({ mounts: [] }),
'GET dns_resolvers': () => ({ dns_resolvers: [] }),
'GET user_namespace_maps': () => ({ user_namespace_maps: [] }),
'PUT vpses/123': () => {
vpsUpdateCount += 1;
return { vps };
},
}),
},
'PUT datasets/10': () => ({ _meta: { action_state_id: 905 } }),
'GET action_states/905': () => ({
action_state: {
id: 905,
label: 'Resize root dataset',
status: true,
finished: false,
current: 0,
total: 1,
},
}),
},
});

await page.goto('/admin/vps/123/config');
const resizeEntry = page.getByTestId('vps.config.ssd.resize');
await expect(resizeEntry).toBeVisible();
await expect(resizeEntry).toHaveAttribute('href', '/admin/vps/123/storage?resize=ssd');
await expect(resizeEntry.locator('..')).toContainText('20 GiB');

await resizeEntry.click();
await expect(page).toHaveURL(/\/admin\/vps\/123\/storage$/);
const modal = page.getByTestId('vps.storage.resize.modal');
await expect(modal).toBeVisible();
await expect(modal).toContainText('No VPS restart');
await expect(modal).toContainText('Current size: 20 GiB');
await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled();

await modal.getByTestId('vps.storage.resize.size').fill('4');
await expect(modal).toContainText('cannot be smaller than the currently used space');
await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled();

await modal.getByTestId('vps.storage.resize.size').fill('32');
await expect(modal.getByTestId('vps.storage.resize.admin_override')).not.toBeChecked();
if (override) await modal.getByTestId('vps.storage.resize.admin_override').check();
const requestPromise = page.waitForRequest(
(request) => request.method() === 'PUT' && request.url().includes('/api/v7.0/datasets/10')
);
await modal.getByTestId('vps.storage.resize.submit').click();

expect((await requestPromise).postDataJSON()).toEqual({ dataset: { refquota: 32 * 1024, ...(override ? { admin_override: true } : {}) } });
expect(vpsUpdateCount).toBe(0);
await expect(modal).toBeHidden();
await expect(page.getByTestId('vps.storage.root_dataset.resize')).toBeVisible();

await page.goto('/app/vps/123/config');
await expect(page.getByTestId('vps.config.ssd.resize')).toHaveCount(0);
await page.goto('/app/vps/123/storage');
await expect(page.getByTestId('vps.storage.root_dataset.resize')).toHaveCount(0);
});

await page.goto('/admin/vps/123/config');
const resizeEntry = page.getByTestId('vps.config.ssd.resize');
await expect(resizeEntry).toBeVisible();
await expect(resizeEntry).toHaveAttribute('href', '/admin/vps/123/storage?resize=ssd');
await expect(resizeEntry.locator('..')).toContainText('20 GiB');

await resizeEntry.click();
await expect(page).toHaveURL(/\/admin\/vps\/123\/storage$/);
const modal = page.getByTestId('vps.storage.resize.modal');
await expect(modal).toBeVisible();
await expect(modal).toContainText('No VPS restart');
await expect(modal).toContainText('Current size: 20 GiB');
await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled();

await modal.getByTestId('vps.storage.resize.size').fill('4');
await expect(modal).toContainText('cannot be smaller than the currently used space');
await expect(modal.getByTestId('vps.storage.resize.submit')).toBeDisabled();

await modal.getByTestId('vps.storage.resize.size').fill('32');
const requestPromise = page.waitForRequest(
(request) => request.method() === 'PUT' && request.url().includes('/api/v7.0/datasets/10')
);
await modal.getByTestId('vps.storage.resize.submit').click();

expect((await requestPromise).postDataJSON()).toEqual({ dataset: { refquota: 32 * 1024 } });
expect(vpsUpdateCount).toBe(0);
await expect(modal).toBeHidden();
await expect(page.getByTestId('vps.storage.root_dataset.resize')).toBeVisible();

await page.goto('/app/vps/123/config');
await expect(page.getByTestId('vps.config.ssd.resize')).toHaveCount(0);
await page.goto('/app/vps/123/storage');
await expect(page.getByTestId('vps.storage.root_dataset.resize')).toHaveCount(0);
});
}

test('resets a mount deletion confirm when the VPS route changes', async ({ page }) => {
await bootstrapVpsAdminWindow(page, { sessionToken: 'TEST' });
Expand Down
2 changes: 1 addition & 1 deletion e2e/specs/app/vps_support_permissions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ test('@pr-smoke support account stays read-only inside the admin VPS shell', asy
await expect(page.getByText('Autostart priority', { exact: true })).toHaveCount(0);
await expect(page.getByText('Change reason', { exact: true })).toHaveCount(0);
await expect(page.getByText('Admin lock type', { exact: true })).toHaveCount(0);
await expect(page.getByText('Admin override', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('vps.config.admin_override')).toHaveCount(0);
await expect(page.getByText('You do not have permission to perform this action.')).toBeVisible();
await expect(page.locator('fieldset:disabled')).toBeVisible();

Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/cs/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export const csStorage = {
"dataset.manage.field.atime": "Access time",
"dataset.manage.field.relatime": "Relative access time",
"dataset.manage.field.sharenfs": "NFS share",
"dataset.manage.field.admin_override": "Nastavit bez ohledu na volné prostředky",
"dataset.manage.field.admin_override": "Povolit překročení prostředků uživatele",
"dataset.manage.help.admin_override": "Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.",
"dataset.manage.field.admin_lock_type": "Typ admin locku",
"dataset.manage.sync.standard": "Standard",
"dataset.manage.sync.always": "Vždy",
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/locales/cs/vps/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const csVps_config = {
'vps.config.field.allow_admin_modifications': 'Povolit admin úpravy',
'vps.config.field.change_reason': 'Důvod změny',
'vps.config.field.admin_lock_type': 'Typ admin locku',
'vps.config.field.admin_override': 'Admin override',
'vps.config.field.admin_override': 'Povolit překročení prostředků uživatele',
'vps.config.help.hostname_mode': 'Spravovaný režim zapisuje hostname přes vpsAdmin. Manuální režim vypne správu hostname vpsAdminem.',
'vps.config.help.hostname': 'Použije se pouze při zapnuté správě hostname.',
'vps.config.help.owner': 'Změna vlastníka se před odesláním potvrzuje.',
Expand All @@ -60,7 +60,7 @@ export const csVps_config = {
'vps.config.help.ssd_unsaved': 'Nejdřív ulož nebo resetuj rozepsané změny CPU, paměti či swapu.',
'vps.config.help.change_reason': 'Volitelná admin poznámka posílaná se změnou prostředků.',
'vps.config.help.admin_lock_type': 'Volitelné vynucení locku použité se změnou prostředků.',
'vps.config.help.admin_override': 'Pošle admin_override=true se změnou prostředků.',
'vps.config.help.admin_override': 'Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.',
'vps.config.help.user_namespace_map': 'Změna namespace mapy se před odesláním potvrzuje.',
'vps.config.help.map_mode': 'Určuje, jak se UID/GID mapování použije na připojené datasety. Změna tohoto nastavení restartuje VPS.',
'vps.config.help.autostart_priority': 'Aktivní stav řídí backend. 0 je nejvyšší priorita; vyšší hodnoty startují později.',
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/locales/cs/vps/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export const csVps_storage = {
'vps.storage.resize.summary': 'Aktuální velikost: {current} · využito: {used}',
'vps.storage.resize.field.size': 'Nová velikost SSD (GiB)',
'vps.storage.resize.field.size_help': 'Zadej celkovou výslednou velikost, ne hodnotu, o kterou se má disk navýšit.',
'vps.storage.resize.field.admin_override': 'Vynutit bez ohledu na dostupné prostředky',
'vps.storage.resize.field.admin_override_help': 'Použij jen po ověření kapacity. API dostane parametr admin_override.',
'vps.storage.resize.field.admin_override': 'Povolit překročení prostředků uživatele',
'vps.storage.resize.field.admin_override_help': 'Povolí navýšení i při nedostatku přidělených prostředků uživatele. Ostatní kontroly zůstávají platné.',
'vps.storage.resize.error': 'Změnu velikosti SSD se nepodařilo spustit',
'vps.storage.resize.validation.required': 'Zadej novou velikost SSD.',
'vps.storage.resize.validation.invalid': 'Velikost musí být kladné číslo v GiB.',
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ export const enStorage = {
"dataset.manage.field.atime": "Access time",
"dataset.manage.field.relatime": "Relative access time",
"dataset.manage.field.sharenfs": "NFS share",
"dataset.manage.field.admin_override": "Set regardless of available resources",
"dataset.manage.field.admin_override": "Allow exceeding user resource allocation",
"dataset.manage.help.admin_override": "Allow increases even when the user has no allocated resources left. Other checks still apply.",
"dataset.manage.field.admin_lock_type": "Admin lock type",
"dataset.manage.sync.standard": "Standard",
"dataset.manage.sync.always": "Always",
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/locales/en/vps/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const enVps_config = {
'vps.config.field.allow_admin_modifications': 'Allow admin modifications',
'vps.config.field.change_reason': 'Change reason',
'vps.config.field.admin_lock_type': 'Admin lock type',
'vps.config.field.admin_override': 'Admin override',
'vps.config.field.admin_override': 'Allow exceeding user resource allocation',
'vps.config.help.hostname_mode': 'Managed mode writes the hostname through vpsAdmin. Manual mode disables vpsAdmin hostname management.',
'vps.config.help.hostname': 'Used only when hostname management is enabled.',
'vps.config.help.owner': 'Changing the owner is confirmed before it is sent.',
Expand All @@ -60,7 +60,7 @@ export const enVps_config = {
'vps.config.help.ssd_unsaved': 'Save or reset pending CPU, memory or swap changes first.',
'vps.config.help.change_reason': 'Optional admin note sent with resource changes.',
'vps.config.help.admin_lock_type': 'Optional lock enforcement used with resource changes.',
'vps.config.help.admin_override': 'Send admin_override=true with resource changes.',
'vps.config.help.admin_override': 'Allow increases even when the user has no allocated resources left. Other checks still apply.',
'vps.config.help.user_namespace_map': 'Changing the namespace map is confirmed before it is sent.',
'vps.config.help.map_mode': 'Controls how UID/GID mappings are applied to mounted datasets. Changing this setting restarts the VPS.',
'vps.config.help.autostart_priority': 'The active state is managed by the backend. 0 is the highest priority; greater values start later.',
Expand Down
Loading
Loading