Skip to content

Commit 5304c4b

Browse files
authored
Can't snapshot read-only disks (#3069)
can't snapshot read-only disks
1 parent cfe65e7 commit 5304c4b

File tree

6 files changed

+60
-19
lines changed

6 files changed

+60
-19
lines changed

app/api/util.spec.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,22 +151,46 @@ test('diskCan', () => {
151151
expect(diskCan.delete({ state: { state: 'attached', instance: 'xyz' } })).toBe(false)
152152
expect(diskCan.delete({ state: { state: 'detached' } })).toBe(true)
153153

154-
// snapshot requires distributed disk type
155-
expect(diskCan.snapshot({ state: { state: 'detached' }, diskType: 'distributed' })).toBe(
156-
true
157-
)
154+
// snapshot requires distributed, non-read-only disk type
155+
expect(
156+
diskCan.snapshot({
157+
state: { state: 'detached' },
158+
diskType: 'distributed',
159+
readOnly: false,
160+
})
161+
).toBe(true)
158162
expect(
159163
diskCan.snapshot({
160164
state: { state: 'attached', instance: 'x' },
161165
diskType: 'distributed',
166+
readOnly: false,
162167
})
163168
).toBe(true)
164-
expect(diskCan.snapshot({ state: { state: 'creating' }, diskType: 'distributed' })).toBe(
165-
false
166-
)
167-
expect(diskCan.snapshot({ state: { state: 'detached' }, diskType: 'local' })).toBe(false)
168169
expect(
169-
diskCan.snapshot({ state: { state: 'attached', instance: 'x' }, diskType: 'local' })
170+
diskCan.snapshot({
171+
state: { state: 'creating' },
172+
diskType: 'distributed',
173+
readOnly: false,
174+
})
175+
).toBe(false)
176+
expect(
177+
diskCan.snapshot({ state: { state: 'detached' }, diskType: 'local', readOnly: false })
178+
).toBe(false)
179+
expect(
180+
diskCan.snapshot({
181+
state: { state: 'attached', instance: 'x' },
182+
diskType: 'local',
183+
readOnly: false,
184+
})
185+
).toBe(false)
186+
187+
// read-only disks cannot be snapshotted
188+
expect(
189+
diskCan.snapshot({
190+
state: { state: 'detached' },
191+
diskType: 'distributed',
192+
readOnly: true,
193+
})
170194
).toBe(false)
171195

172196
// @ts-expect-error typechecker rejects actions that don't exist

app/api/util.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,18 @@ const diskStateActions = {
209209

210210
// snapshot has a type check in addition to state check
211211
// https://github.com/oxidecomputer/omicron/blob/078f636/nexus/src/app/snapshot.rs#L100-L109
212-
// NOTE: .states only captures the state requirement; local disks cannot be
213-
// snapshotted regardless of state
212+
// NOTE: .states only captures the state requirement; local and read-only disks
213+
// cannot be snapshotted regardless of state
214214
const snapshotStates: DiskState['state'][] = ['attached', 'detached']
215215
// accept both camelCase (Disk) and snake_case (Json<Disk>) for use in MSW handlers
216216
type SnapshotDisk =
217-
| Pick<Disk, 'state' | 'diskType'>
218-
| { state: DiskState; disk_type: DiskType }
217+
| Pick<Disk, 'state' | 'diskType' | 'readOnly'>
218+
| { state: DiskState; disk_type: DiskType; read_only: boolean }
219219
const canSnapshot = (d: SnapshotDisk) => {
220220
const diskType = 'diskType' in d ? d.diskType : d.disk_type
221+
const readOnly = 'readOnly' in d ? d.readOnly : d.read_only
221222
return (
223+
!readOnly &&
222224
snapshotStates.includes(d.state.state) &&
223225
match(diskType)
224226
.with('distributed', () => true)

app/forms/snapshot-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default function SnapshotCreate() {
8484
<ComboboxField
8585
label="Disk"
8686
name="disk"
87-
description="Only disks that can be snapshotted are listed"
87+
description="Only disks that support snapshots are listed"
8888
placeholder="Select a disk"
8989
items={diskItemsForCombobox}
9090
required

app/pages/project/instances/common.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ export const fancifyStates = (states: string[]) =>
2323
intersperse(states.map(white), <>, </>, <> or </>)
2424

2525
/** Returns a disabled reason if the disk cannot be snapshotted, false otherwise */
26-
export function snapshotDisabledReason(disk: Pick<Disk, 'state' | 'diskType'>): ReactNode {
26+
export function snapshotDisabledReason(disk: Disk): ReactNode {
2727
if (diskCan.snapshot(disk)) return false
28+
if (disk.readOnly) return "Read-only disks don't support snapshots"
2829
return match(disk.diskType)
2930
.with('distributed', () => (
30-
<>Only disks in state {fancifyStates(diskCan.snapshot.states)} can be snapshotted</>
31+
<>Only disks in state {fancifyStates(diskCan.snapshot.states)} support snapshots</>
3132
))
32-
.with('local', () => 'Only distributed disks support snapshots')
33+
.with('local', () => "Local disks don't support snapshots")
3334
.exhaustive()
3435
}
3536

mock-api/msw/handlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1349,9 +1349,10 @@ export const handlers = makeHandlers({
13491349

13501350
const disk = lookup.disk({ ...query, disk: body.disk })
13511351
if (!diskCan.snapshot(disk)) {
1352+
if (disk.read_only) throw "Read-only disks don't support snapshots"
13521353
throw match(disk.disk_type)
13531354
.with('distributed', () => 'Cannot snapshot disk in state ' + disk.state.state)
1354-
.with('local', () => 'Only distributed disks support snapshots')
1355+
.with('local', () => "Local disks don't support snapshots")
13551356
.exhaustive()
13561357
}
13571358

test/e2e/disks.e2e.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,22 @@ test('Local disk snapshot disabled', async ({ page }) => {
100100
await expect(snapshotItem).toBeDisabled()
101101

102102
// hover to see tooltip with disabled reason
103+
await snapshotItem.hover()
104+
await expect(page.getByRole('tooltip')).toHaveText("Local disks don't support snapshots")
105+
})
106+
107+
test('Read-only disk snapshot disabled', async ({ page }) => {
108+
await page.goto('/projects/mock-project/disks')
109+
110+
const row = page.getByRole('row', { name: 'read-only-disk', exact: false })
111+
await row.getByRole('button', { name: 'Row actions' }).click()
112+
113+
const snapshotItem = page.getByRole('menuitem', { name: 'Snapshot' })
114+
await expect(snapshotItem).toBeDisabled()
115+
103116
await snapshotItem.hover()
104117
await expect(page.getByRole('tooltip')).toHaveText(
105-
'Only distributed disks support snapshots'
118+
"Read-only disks don't support snapshots"
106119
)
107120
})
108121

0 commit comments

Comments
 (0)