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
25 changes: 21 additions & 4 deletions app/test/e2e/floating-ip-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,45 @@ test('can create a Floating IP', async ({ page }) => {
})

test('can detach and attach a Floating IP', async ({ page }) => {
// check floating IP is visible on instance detail
await page.goto('/projects/mock-project/instances/db1')
await expect(page.getByText('192.168.64.64')).toBeVisible()

// now go detach it
await page.goto(floatingIpsPage)

await expectRowVisible(page.getByRole('table'), {
name: 'cola-float',
ip: '192.168.64.64',
'Attached to instance': 'db1',
})
await clickRowAction(page, 'cola-float', 'Detach')
await page.getByRole('button', { name: 'Confirm' }).click()

await expectNotVisible(page, ['role=heading[name*="Detach Floating IP"]'])
await expect(page.getByRole('dialog')).toBeHidden()
// Since we detached it, we don't expect to see db1 any longer
await expectNotVisible(page, ['text=db1'])
await expect(page.getByRole('cell', { name: 'db1' })).toBeHidden()

// click back over to instance page (can't use goto because it refreshes) and
// confirm the IP is no longer there either
await page.getByRole('link', { name: 'Instances' }).click()
await page.getByRole('link', { name: 'db1' }).click()
await expect(page.getByRole('heading', { name: 'db1' })).toBeVisible()
await expect(page.getByText('192.168.64.64')).toBeHidden()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all a bit silly but why not


// Reattach it to db1
// Now click back to floating IPs and reattach it to db1
await page.getByRole('link', { name: 'Floating IPs' }).click()
await clickRowAction(page, 'cola-float', 'Attach')
await page.getByRole('button', { name: 'Select instance' }).click()
await page.getByRole('option', { name: 'db1' }).click()

await page.getByRole('button', { name: 'Attach' }).click()

// The dialog should be gone
await expectNotVisible(page, ['role=heading[name*="Attach Floating IP"]'])
await expect(page.getByRole('dialog')).toBeHidden()
await expectRowVisible(page.getByRole('table'), {
name: 'cola-float',
ip: '192.168.64.64',
'Attached to instance': 'db1',
})
})
16 changes: 13 additions & 3 deletions libs/api-mocks/external-ip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@ import type { ExternalIp } from '@oxide/api'
import { instances } from './instance'
import type { Json } from './json-type'

/**
* This is a case where we need extra structure beyond the API response. The API
* response for an ephemeral IP does not indicate the instance it's attached to
* (maybe it should, but that's a separate problem), but we need the instance ID
* on each one in order to look up the IPs for a given instance. The floating IP
* arm of the union does have an instance_id field, so that's a point in favor
* of adding it to the ephemeral IP arm.
*/
type DbExternalIp = {
instance_id: string
external_ip: Json<ExternalIp>
}

// TODO: this type represents the API response, but we need to mock more
// structure in order to be able to look up IPs for a particular instance
export const externalIps: DbExternalIp[] = [
// Note that ExternalIp is a union of types representing ephemeral and floating
// IPs, but we only put the ephemeral ones here. We have a separate table for
// floating IPs analogous to the floating_ip view in Nexus.

export const ephemeralIps: DbExternalIp[] = [
{
instance_id: instances[0].id,
external_ip: {
Expand Down
2 changes: 1 addition & 1 deletion libs/api-mocks/msw/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ const initDb = {
/** Join table for `users` and `userGroups` */
groupMemberships: [...mock.groupMemberships],
images: [...mock.images],
externalIps: [...mock.externalIps],
ephemeralIps: [...mock.ephemeralIps],
instances: [...mock.instances],
ipPools: [...mock.ipPools],
ipPoolSilos: [...mock.ipPoolSilos],
Expand Down
11 changes: 9 additions & 2 deletions libs/api-mocks/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,11 +496,18 @@ export const handlers = makeHandlers({
},
instanceExternalIpList({ path, query }) {
const instance = lookup.instance({ ...path, ...query })
const externalIps = db.externalIps

const ephemeralIps = db.ephemeralIps
.filter((eip) => eip.instance_id === instance.id)
.map((eip) => eip.external_ip)

// floating IPs are missing their `kind` field in the DB so we add it
const floatingIps = db.floatingIps
.filter((f) => f.instance_id === instance.id)
.map((f) => ({ kind: 'floating' as const, ...f }))

// endpoint is not paginated. or rather, it's fake paginated
return { items: externalIps }
return { items: [...ephemeralIps, ...floatingIps] }
},
instanceNetworkInterfaceList({ query }) {
const instance = lookup.instance(query)
Expand Down