From 70de6cb4dccacce206e9f2fce9878030cee65a3d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 11 Jul 2025 16:30:29 -0700 Subject: [PATCH 1/8] Bump Omicron further and add IP Pool ID column to networking tab --- OMICRON_VERSION | 2 +- app/api/__generated__/Api.ts | 147 +++++++++++------- app/api/__generated__/OMICRON_VERSION | 2 +- app/api/__generated__/msw-handlers.ts | 71 ++++++--- app/api/__generated__/validate.ts | 116 ++++++++------ app/pages/project/instances/NetworkingTab.tsx | 5 + mock-api/external-ip.ts | 5 + mock-api/msw/db.ts | 8 +- mock-api/msw/handlers.ts | 13 +- 9 files changed, 233 insertions(+), 136 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 60adec500d..72bb264910 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -17e4c0e5b0b697c63bd05d267c0fdaca2b2e79d7 +034044c734cfdb377847f88da59bd0759633ebf6 diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index c26b8dbe3d..5caafe0d7a 100644 --- a/app/api/__generated__/Api.ts +++ b/app/api/__generated__/Api.ts @@ -1829,7 +1829,7 @@ export type EphemeralIpCreate = { } export type ExternalIp = - | { ip: string; kind: 'ephemeral' } + | { ip: string; ipPoolId: string; kind: 'ephemeral' } /** A Floating IP is a well-known IP address which can be attached and detached from instances. */ | { /** human-readable free-form text about a resource */ @@ -3193,28 +3193,6 @@ export type RackResultsPage = { nextPage?: string | null } -/** - * A name for a built-in role - * - * Role names consist of two string components separated by dot ("."). - */ -export type RoleName = string - -/** - * View of a Role - */ -export type Role = { description: string; name: RoleName } - -/** - * A single page of results - */ -export type RoleResultsPage = { - /** list of items on this page of results */ - items: Role[] - /** token used to fetch the next page of results (if any) */ - nextPage?: string | null -} - /** * A route to a destination network through a gateway address. */ @@ -4409,6 +4387,28 @@ export type UninitializedSledResultsPage = { nextPage?: string | null } +/** + * Trusted root role used by the update system to verify update repositories. + */ +export type UpdatesTrustRoot = { + /** The UUID of this trusted root role. */ + id: string + /** The trusted root role itself, a JSON document as described by The Update Framework. */ + rootRole: Record + /** Time the trusted root role was added. */ + timeCreated: Date +} + +/** + * A single page of results + */ +export type UpdatesTrustRootResultsPage = { + /** list of items on this page of results */ + items: UpdatesTrustRoot[] + /** token used to fetch the next page of results (if any) */ + nextPage?: string | null +} + /** * View of a User */ @@ -6085,15 +6085,6 @@ export interface NetworkingSwitchPortSettingsViewPathParams { port: NameOrId } -export interface RoleListQueryParams { - limit?: number | null - pageToken?: string | null -} - -export interface RoleViewPathParams { - roleName: string -} - export interface SystemQuotasListQueryParams { limit?: number | null pageToken?: string | null @@ -6153,6 +6144,20 @@ export interface SystemUpdateGetRepositoryPathParams { systemVersion: string } +export interface SystemUpdateTrustRootListQueryParams { + limit?: number | null + pageToken?: string | null + sortBy?: IdSortMode +} + +export interface SystemUpdateTrustRootViewPathParams { + trustRootId: string +} + +export interface SystemUpdateTrustRootDeletePathParams { + trustRootId: string +} + export interface SiloUserListQueryParams { limit?: number | null pageToken?: string | null @@ -9609,30 +9614,6 @@ export class Api extends HttpClient { ...params, }) }, - /** - * List built-in roles - */ - roleList: ( - { query = {} }: { query?: RoleListQueryParams }, - params: FetchParams = {} - ) => { - return this.request({ - path: `/v1/system/roles`, - method: 'GET', - query, - ...params, - }) - }, - /** - * Fetch built-in role - */ - roleView: ({ path }: { path: RoleViewPathParams }, params: FetchParams = {}) => { - return this.request({ - path: `/v1/system/roles/${path.roleName}`, - method: 'GET', - ...params, - }) - }, /** * Lists resource quotas for all silos */ @@ -9792,7 +9773,7 @@ export class Api extends HttpClient { }) }, /** - * Upload TUF repository + * Upload system release repository */ systemUpdatePutRepository: ( { query }: { query: SystemUpdatePutRepositoryQueryParams }, @@ -9806,7 +9787,7 @@ export class Api extends HttpClient { }) }, /** - * Fetch TUF repository description + * Fetch system release repository description by version */ systemUpdateGetRepository: ( { path }: { path: SystemUpdateGetRepositoryPathParams }, @@ -9842,6 +9823,56 @@ export class Api extends HttpClient { ...params, }) }, + /** + * List root roles in the updates trust store + */ + systemUpdateTrustRootList: ( + { query = {} }: { query?: SystemUpdateTrustRootListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/update/trust-roots`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Add trusted root role to updates trust store + */ + systemUpdateTrustRootCreate: (_: EmptyObj, params: FetchParams = {}) => { + return this.request({ + path: `/v1/system/update/trust-roots`, + method: 'POST', + ...params, + }) + }, + /** + * Fetch trusted root role + */ + systemUpdateTrustRootView: ( + { path }: { path: SystemUpdateTrustRootViewPathParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/update/trust-roots/${path.trustRootId}`, + method: 'GET', + ...params, + }) + }, + /** + * Delete trusted root role + */ + systemUpdateTrustRootDelete: ( + { path }: { path: SystemUpdateTrustRootDeletePathParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/update/trust-roots/${path.trustRootId}`, + method: 'DELETE', + ...params, + }) + }, /** * List built-in (system) users in silo */ diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index 15c3dcf687..ead18510e2 100644 --- a/app/api/__generated__/OMICRON_VERSION +++ b/app/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -17e4c0e5b0b697c63bd05d267c0fdaca2b2e79d7 +034044c734cfdb377847f88da59bd0759633ebf6 diff --git a/app/api/__generated__/msw-handlers.ts b/app/api/__generated__/msw-handlers.ts index 6d7d002a4e..1588ee744b 100644 --- a/app/api/__generated__/msw-handlers.ts +++ b/app/api/__generated__/msw-handlers.ts @@ -1405,18 +1405,6 @@ export interface MSWHandlers { req: Request cookies: Record }) => Promisable> - /** `GET /v1/system/roles` */ - roleList: (params: { - query: Api.RoleListQueryParams - req: Request - cookies: Record - }) => Promisable> - /** `GET /v1/system/roles/:roleName` */ - roleView: (params: { - path: Api.RoleViewPathParams - req: Request - cookies: Record - }) => Promisable> /** `GET /v1/system/silo-quotas` */ systemQuotasList: (params: { query: Api.SystemQuotasListQueryParams @@ -1515,6 +1503,29 @@ export interface MSWHandlers { req: Request cookies: Record }) => Promisable> + /** `GET /v1/system/update/trust-roots` */ + systemUpdateTrustRootList: (params: { + query: Api.SystemUpdateTrustRootListQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `POST /v1/system/update/trust-roots` */ + systemUpdateTrustRootCreate: (params: { + req: Request + cookies: Record + }) => Promisable> + /** `GET /v1/system/update/trust-roots/:trustRootId` */ + systemUpdateTrustRootView: (params: { + path: Api.SystemUpdateTrustRootViewPathParams + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/system/update/trust-roots/:trustRootId` */ + systemUpdateTrustRootDelete: (params: { + path: Api.SystemUpdateTrustRootDeletePathParams + req: Request + cookies: Record + }) => Promisable /** `GET /v1/system/users` */ siloUserList: (params: { query: Api.SiloUserListQueryParams @@ -3000,14 +3011,6 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { '/v1/system/policy', handler(handlers['systemPolicyUpdate'], null, schema.FleetRolePolicy) ), - http.get( - '/v1/system/roles', - handler(handlers['roleList'], schema.RoleListParams, null) - ), - http.get( - '/v1/system/roles/:roleName', - handler(handlers['roleView'], schema.RoleViewParams, null) - ), http.get( '/v1/system/silo-quotas', handler(handlers['systemQuotasList'], schema.SystemQuotasListParams, null) @@ -3089,6 +3092,34 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { '/v1/system/update/target-release', handler(handlers['targetReleaseUpdate'], null, schema.SetTargetReleaseParams) ), + http.get( + '/v1/system/update/trust-roots', + handler( + handlers['systemUpdateTrustRootList'], + schema.SystemUpdateTrustRootListParams, + null + ) + ), + http.post( + '/v1/system/update/trust-roots', + handler(handlers['systemUpdateTrustRootCreate'], null, null) + ), + http.get( + '/v1/system/update/trust-roots/:trustRootId', + handler( + handlers['systemUpdateTrustRootView'], + schema.SystemUpdateTrustRootViewParams, + null + ) + ), + http.delete( + '/v1/system/update/trust-roots/:trustRootId', + handler( + handlers['systemUpdateTrustRootDelete'], + schema.SystemUpdateTrustRootDeleteParams, + null + ) + ), http.get( '/v1/system/users', handler(handlers['siloUserList'], schema.SiloUserListParams, null) diff --git a/app/api/__generated__/validate.ts b/app/api/__generated__/validate.ts index 71f5b15466..b907bab43c 100644 --- a/app/api/__generated__/validate.ts +++ b/app/api/__generated__/validate.ts @@ -1719,7 +1719,11 @@ export const Error = z.preprocess( export const ExternalIp = z.preprocess( processResponseBody, z.union([ - z.object({ ip: z.string().ip(), kind: z.enum(['ephemeral']) }), + z.object({ + ip: z.string().ip(), + ipPoolId: z.string().uuid(), + kind: z.enum(['ephemeral']), + }), z.object({ description: z.string(), id: z.string().uuid(), @@ -3030,35 +3034,6 @@ export const RackResultsPage = z.preprocess( z.object({ items: Rack.array(), nextPage: z.string().nullable().optional() }) ) -/** - * A name for a built-in role - * - * Role names consist of two string components separated by dot ("."). - */ -export const RoleName = z.preprocess( - processResponseBody, - z - .string() - .max(63) - .regex(/[a-z-]+\.[a-z-]+/) -) - -/** - * View of a Role - */ -export const Role = z.preprocess( - processResponseBody, - z.object({ description: z.string(), name: RoleName }) -) - -/** - * A single page of results - */ -export const RoleResultsPage = z.preprocess( - processResponseBody, - z.object({ items: Role.array(), nextPage: z.string().nullable().optional() }) -) - /** * A route to a destination network through a gateway address. */ @@ -4102,6 +4077,26 @@ export const UninitializedSledResultsPage = z.preprocess( z.object({ items: UninitializedSled.array(), nextPage: z.string().nullable().optional() }) ) +/** + * Trusted root role used by the update system to verify update repositories. + */ +export const UpdatesTrustRoot = z.preprocess( + processResponseBody, + z.object({ + id: z.string().uuid(), + rootRole: z.record(z.unknown()), + timeCreated: z.coerce.date(), + }) +) + +/** + * A single page of results + */ +export const UpdatesTrustRootResultsPage = z.preprocess( + processResponseBody, + z.object({ items: UpdatesTrustRoot.array(), nextPage: z.string().nullable().optional() }) +) + /** * View of a User */ @@ -6943,27 +6938,6 @@ export const SystemPolicyUpdateParams = z.preprocess( }) ) -export const RoleListParams = z.preprocess( - processResponseBody, - z.object({ - path: z.object({}), - query: z.object({ - limit: z.number().min(1).max(4294967295).nullable().optional(), - pageToken: z.string().nullable().optional(), - }), - }) -) - -export const RoleViewParams = z.preprocess( - processResponseBody, - z.object({ - path: z.object({ - roleName: z.string(), - }), - query: z.object({}), - }) -) - export const SystemQuotasListParams = z.preprocess( processResponseBody, z.object({ @@ -7129,6 +7103,46 @@ export const TargetReleaseUpdateParams = z.preprocess( }) ) +export const SystemUpdateTrustRootListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + limit: z.number().min(1).max(4294967295).nullable().optional(), + pageToken: z.string().nullable().optional(), + sortBy: IdSortMode.optional(), + }), + }) +) + +export const SystemUpdateTrustRootCreateParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({}), + }) +) + +export const SystemUpdateTrustRootViewParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + trustRootId: z.string().uuid(), + }), + query: z.object({}), + }) +) + +export const SystemUpdateTrustRootDeleteParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + trustRootId: z.string().uuid(), + }), + query: z.object({}), + }) +) + export const SiloUserListParams = z.preprocess( processResponseBody, z.object({ diff --git a/app/pages/project/instances/NetworkingTab.tsx b/app/pages/project/instances/NetworkingTab.tsx index 2493a99db6..f90b6e90cb 100644 --- a/app/pages/project/instances/NetworkingTab.tsx +++ b/app/pages/project/instances/NetworkingTab.tsx @@ -39,6 +39,7 @@ import { confirmDelete } from '~/stores/confirm-delete' import { addToast } from '~/stores/toast' import { DescriptionCell } from '~/table/cells/DescriptionCell' import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell' +import { IpPoolCell } from '~/table/cells/IpPoolCell' import { LinkCell } from '~/table/cells/LinkCell' import { useColsWithActions, type MenuAction } from '~/table/columns/action-col' import { Columns } from '~/table/columns/common' @@ -179,6 +180,10 @@ const staticIpCols = [ ), cell: (info) => {info.getValue()}, }), + ipColHelper.accessor('ipPoolId', { + header: 'IP pool', + cell: (info) => , + }), ipColHelper.accessor('name', { cell: (info) => info.row.original.kind === 'ephemeral' ? : info.getValue(), diff --git a/mock-api/external-ip.ts b/mock-api/external-ip.ts index a90895b151..f444521e28 100644 --- a/mock-api/external-ip.ts +++ b/mock-api/external-ip.ts @@ -8,6 +8,7 @@ import type { ExternalIp } from '@oxide/api' import { instances } from './instance' +import { ipPool1 } from './ip-pool' import type { Json } from './json-type' /** @@ -34,6 +35,7 @@ export const ephemeralIps: DbExternalIp[] = [ instance_id: instances[0].id, external_ip: { ip: '123.4.56.0', + ip_pool_id: ipPool1.id, kind: 'ephemeral', }, }, @@ -42,6 +44,7 @@ export const ephemeralIps: DbExternalIp[] = [ instance_id: instances[2].id, external_ip: { ip: '123.4.56.1', + ip_pool_id: ipPool1.id, kind: 'ephemeral', }, }, @@ -49,6 +52,7 @@ export const ephemeralIps: DbExternalIp[] = [ instance_id: instances[2].id, external_ip: { ip: '123.4.56.2', + ip_pool_id: ipPool1.id, kind: 'ephemeral', }, }, @@ -56,6 +60,7 @@ export const ephemeralIps: DbExternalIp[] = [ instance_id: instances[2].id, external_ip: { ip: '123.4.56.3', + ip_pool_id: ipPool1.id, kind: 'ephemeral', }, }, diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index cecc37e669..dc4895ca0a 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -18,7 +18,7 @@ import type * as Sel from '~/api/selectors' import { commaSeries } from '~/util/str' import type { Json } from '../json-type' -import { siloSettings } from '../silo' +import { defaultSilo, siloSettings } from '../silo' import { internalError } from './util' export const notFoundErr = (msg: string) => { @@ -56,9 +56,11 @@ function ensureNoParentSelectors( } export const getIpFromPool = (poolName: string | undefined | null) => { - const pool = lookup.ipPool({ pool: poolName || undefined }) + const pool = poolName + ? lookup.ipPool({ pool: poolName }) + : lookup.siloDefaultIpPool({ silo: defaultSilo.name }) const ipPoolRange = db.ipPoolRanges.find((range) => range.ip_pool_id === pool.id) - if (!ipPoolRange) throw notFoundErr(`IP range for pool '${poolName}'`) + if (!ipPoolRange) throw notFoundErr(`IP range for pool '${poolName || 'default'}'`) // right now, we're just using the first address in the range, but we'll // want to filter the list of available IPs for the first unused address diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 8ea365cf46..9e6d0da257 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -564,10 +564,14 @@ export const handlers = makeHandlers({ floatingIp.instance_id = instanceId } else if (ip.type === 'ephemeral') { const firstAvailableAddress = getIpFromPool(ip.pool) + const pool = ip.pool + ? lookup.ipPool({ pool: ip.pool }) + : lookup.siloDefaultIpPool({ silo: defaultSilo.name }) db.ephemeralIps.push({ instance_id: instanceId, external_ip: { ip: firstAvailableAddress, + ip_pool_id: pool.id, kind: 'ephemeral', }, }) @@ -737,9 +741,12 @@ export const handlers = makeHandlers({ instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) const { pool } = body + if (!pool) throw new Error('Pool is required for ephemeral IP attachment') const firstAvailableAddress = getIpFromPool(pool) + const poolObj = lookup.ipPool({ pool }) const externalIp = { ip: firstAvailableAddress, + ip_pool_id: poolObj.id, kind: 'ephemeral' as const, } db.ephemeralIps.push({ @@ -1880,8 +1887,6 @@ export const handlers = makeHandlers({ probeList: NotImplemented, probeView: NotImplemented, rackView: NotImplemented, - roleList: NotImplemented, - roleView: NotImplemented, siloPolicyUpdate: NotImplemented, siloPolicyView: NotImplemented, siloUserList: NotImplemented, @@ -1904,6 +1909,10 @@ export const handlers = makeHandlers({ systemTimeseriesSchemaList: NotImplemented, systemUpdateGetRepository: NotImplemented, systemUpdatePutRepository: NotImplemented, + systemUpdateTrustRootCreate: NotImplemented, + systemUpdateTrustRootDelete: NotImplemented, + systemUpdateTrustRootList: NotImplemented, + systemUpdateTrustRootView: NotImplemented, targetReleaseUpdate: NotImplemented, targetReleaseView: NotImplemented, userBuiltinList: NotImplemented, From a952fe8d1e305ae3610040fdcf779ff607fa2061 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 11 Jul 2025 17:05:31 -0700 Subject: [PATCH 2/8] simplify IP Pool finding with safer fallback --- mock-api/msw/db.ts | 18 +++++++++++++++--- mock-api/msw/handlers.ts | 14 +++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index dc4895ca0a..1ec0b99f94 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -55,10 +55,22 @@ function ensureNoParentSelectors( } } +export const resolveIpPool = (poolName: string | undefined | null, context = '') => { + if (poolName) { + return lookup.ipPool({ pool: poolName }) + } + try { + return lookup.siloDefaultIpPool({ silo: defaultSilo.id }) + } catch (_error) { + const contextMsg = context ? ` ${context}` : '' + throw new Error( + `No IP pool specified${contextMsg} and no default IP pool configured for silo. Please specify a pool.` + ) + } +} + export const getIpFromPool = (poolName: string | undefined | null) => { - const pool = poolName - ? lookup.ipPool({ pool: poolName }) - : lookup.siloDefaultIpPool({ silo: defaultSilo.name }) + const pool = resolveIpPool(poolName) const ipPoolRange = db.ipPoolRanges.find((range) => range.ip_pool_id === pool.id) if (!ipPoolRange) throw notFoundErr(`IP range for pool '${poolName || 'default'}'`) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 9e6d0da257..bcd8117150 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -41,6 +41,7 @@ import { lookup, lookupById, notFoundErr, + resolveIpPool, utilizationForSilo, } from './db' import { @@ -563,10 +564,9 @@ export const handlers = makeHandlers({ // we've already validated that the IP isn't attached floatingIp.instance_id = instanceId } else if (ip.type === 'ephemeral') { - const firstAvailableAddress = getIpFromPool(ip.pool) - const pool = ip.pool - ? lookup.ipPool({ pool: ip.pool }) - : lookup.siloDefaultIpPool({ silo: defaultSilo.name }) + const pool = resolveIpPool(ip.pool, 'for ephemeral IP') + const firstAvailableAddress = getIpFromPool(pool.name) + db.ephemeralIps.push({ instance_id: instanceId, external_ip: { @@ -741,9 +741,9 @@ export const handlers = makeHandlers({ instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) const { pool } = body - if (!pool) throw new Error('Pool is required for ephemeral IP attachment') - const firstAvailableAddress = getIpFromPool(pool) - const poolObj = lookup.ipPool({ pool }) + const poolObj = resolveIpPool(pool, 'for ephemeral IP attachment') + const firstAvailableAddress = getIpFromPool(poolObj.name) + const externalIp = { ip: firstAvailableAddress, ip_pool_id: poolObj.id, From af7947dd8274e0ea5d48217d4c05297dc2bd2837 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 14 Jul 2025 10:56:49 -0700 Subject: [PATCH 3/8] Bump Omicron to latest and run gen-api; no changes to console --- OMICRON_VERSION | 2 +- app/api/__generated__/Api.ts | 16 ++++++++-------- app/api/__generated__/OMICRON_VERSION | 2 +- app/api/__generated__/validate.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 72bb264910..02aaab0f87 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -034044c734cfdb377847f88da59bd0759633ebf6 +c65212d77d38581632bb972b606f581bd52c3298 diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index 5caafe0d7a..456a6fb4d3 100644 --- a/app/api/__generated__/Api.ts +++ b/app/api/__generated__/Api.ts @@ -4850,13 +4850,6 @@ export type NameOrIdSortMode = /** sort in increasing order of "id" */ | 'id_ascending' -/** - * Supported set of sort modes for scanning by id only. - * - * Currently, we only support scanning in ascending order. - */ -export type IdSortMode = 'id_ascending' - /** * Supported set of sort modes for scanning by timestamp and ID */ @@ -4880,6 +4873,13 @@ export type DiskMetricName = */ export type PaginationOrder = 'ascending' | 'descending' +/** + * Supported set of sort modes for scanning by id only. + * + * Currently, we only support scanning in ascending order. + */ +export type IdSortMode = 'id_ascending' + export type SystemMetricName = | 'virtual_disk_space_provisioned' | 'cpus_provisioned' @@ -4922,7 +4922,7 @@ export interface ProbeDeleteQueryParams { export interface SupportBundleListQueryParams { limit?: number | null pageToken?: string | null - sortBy?: IdSortMode + sortBy?: TimeAndIdSortMode } export interface SupportBundleViewPathParams { diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index ead18510e2..b91cbc9fc3 100644 --- a/app/api/__generated__/OMICRON_VERSION +++ b/app/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -034044c734cfdb377847f88da59bd0759633ebf6 +c65212d77d38581632bb972b606f581bd52c3298 diff --git a/app/api/__generated__/validate.ts b/app/api/__generated__/validate.ts index b907bab43c..d94dedad78 100644 --- a/app/api/__generated__/validate.ts +++ b/app/api/__generated__/validate.ts @@ -4529,13 +4529,6 @@ export const NameOrIdSortMode = z.preprocess( z.enum(['name_ascending', 'name_descending', 'id_ascending']) ) -/** - * Supported set of sort modes for scanning by id only. - * - * Currently, we only support scanning in ascending order. - */ -export const IdSortMode = z.preprocess(processResponseBody, z.enum(['id_ascending'])) - /** * Supported set of sort modes for scanning by timestamp and ID */ @@ -4557,6 +4550,13 @@ export const PaginationOrder = z.preprocess( z.enum(['ascending', 'descending']) ) +/** + * Supported set of sort modes for scanning by id only. + * + * Currently, we only support scanning in ascending order. + */ +export const IdSortMode = z.preprocess(processResponseBody, z.enum(['id_ascending'])) + export const SystemMetricName = z.preprocess( processResponseBody, z.enum(['virtual_disk_space_provisioned', 'cpus_provisioned', 'ram_provisioned']) @@ -4647,7 +4647,7 @@ export const SupportBundleListParams = z.preprocess( query: z.object({ limit: z.number().min(1).max(4294967295).nullable().optional(), pageToken: z.string().nullable().optional(), - sortBy: IdSortMode.optional(), + sortBy: TimeAndIdSortMode.optional(), }), }) ) From f484aa39257cac5e166b35ce007716a99c06bbe7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 14 Jul 2025 13:55:46 -0500 Subject: [PATCH 4/8] give IpPoolCell loading state, don't blow up on errors --- app/table/cells/IpPoolCell.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/table/cells/IpPoolCell.tsx b/app/table/cells/IpPoolCell.tsx index f15a03fd87..03cbce1937 100644 --- a/app/table/cells/IpPoolCell.tsx +++ b/app/table/cells/IpPoolCell.tsx @@ -5,14 +5,20 @@ * * Copyright Oxide Computer Company */ -import { useApiQuery } from '~/api' +import { useApiQueryErrorsAllowed } from '~/api' import { Tooltip } from '~/ui/lib/Tooltip' -import { EmptyCell } from './EmptyCell' +import { EmptyCell, SkeletonCell } from './EmptyCell' export const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => { - const pool = useApiQuery('projectIpPoolView', { path: { pool: ipPoolId } }).data - if (!pool) return + const { data: result } = useApiQueryErrorsAllowed('projectIpPoolView', { + path: { pool: ipPoolId }, + }) + if (!result) return + // this should essentially never happen, but it's probably better than blowing + // up the whole page if the pool is not found + if (result.type === 'error') return + const pool = result.data return ( {pool.name} From f65554e5917226875190bf6801c6fefef901a4a7 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 14 Jul 2025 13:32:30 -0700 Subject: [PATCH 5/8] A few post-review adjustments --- mock-api/msw/db.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 1ec0b99f94..d21a85c753 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -55,9 +55,9 @@ function ensureNoParentSelectors( } } -export const resolveIpPool = (poolName: string | undefined | null, context = '') => { - if (poolName) { - return lookup.ipPool({ pool: poolName }) +export const resolveIpPool = (poolNameOrId: string | undefined | null, context = '') => { + if (poolNameOrId) { + return lookup.ipPool({ pool: poolNameOrId }) } try { return lookup.siloDefaultIpPool({ silo: defaultSilo.id }) @@ -69,10 +69,13 @@ export const resolveIpPool = (poolName: string | undefined | null, context = '') } } -export const getIpFromPool = (poolName: string | undefined | null) => { - const pool = resolveIpPool(poolName) +export const getIpFromPool = (poolNameOrId: string | undefined | null) => { + const pool = resolveIpPool(poolNameOrId) const ipPoolRange = db.ipPoolRanges.find((range) => range.ip_pool_id === pool.id) - if (!ipPoolRange) throw notFoundErr(`IP range for pool '${poolName || 'default'}'`) + if (!ipPoolRange) { + const poolLabel = poolNameOrId ? `pool '${pool.name}'` : 'default pool' + throw notFoundErr(`IP range for ${poolLabel}`) + } // right now, we're just using the first address in the range, but we'll // want to filter the list of available IPs for the first unused address From 2e7845f7a926bd06a051b2657f5a377e774f7822 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 14 Jul 2025 17:50:26 -0700 Subject: [PATCH 6/8] Simpler handling of resolveIpPool --- mock-api/msw/db.ts | 17 ++++------------- mock-api/msw/handlers.ts | 8 ++++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index d21a85c753..5adc3d9a59 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -55,19 +55,10 @@ function ensureNoParentSelectors( } } -export const resolveIpPool = (poolNameOrId: string | undefined | null, context = '') => { - if (poolNameOrId) { - return lookup.ipPool({ pool: poolNameOrId }) - } - try { - return lookup.siloDefaultIpPool({ silo: defaultSilo.id }) - } catch (_error) { - const contextMsg = context ? ` ${context}` : '' - throw new Error( - `No IP pool specified${contextMsg} and no default IP pool configured for silo. Please specify a pool.` - ) - } -} +export const resolveIpPool = (poolNameOrId: string | undefined | null) => + poolNameOrId + ? lookup.ipPool({ pool: poolNameOrId }) + : lookup.siloDefaultIpPool({ silo: defaultSilo.id }) export const getIpFromPool = (poolNameOrId: string | undefined | null) => { const pool = resolveIpPool(poolNameOrId) diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index bcd8117150..70d0224ae2 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -564,8 +564,8 @@ export const handlers = makeHandlers({ // we've already validated that the IP isn't attached floatingIp.instance_id = instanceId } else if (ip.type === 'ephemeral') { - const pool = resolveIpPool(ip.pool, 'for ephemeral IP') - const firstAvailableAddress = getIpFromPool(pool.name) + const pool = resolveIpPool(ip.pool) + const firstAvailableAddress = getIpFromPool(pool.id) db.ephemeralIps.push({ instance_id: instanceId, @@ -741,8 +741,8 @@ export const handlers = makeHandlers({ instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) const { pool } = body - const poolObj = resolveIpPool(pool, 'for ephemeral IP attachment') - const firstAvailableAddress = getIpFromPool(poolObj.name) + const poolObj = resolveIpPool(pool) + const firstAvailableAddress = getIpFromPool(poolObj.id) const externalIp = { ip: firstAvailableAddress, From e783c718b03d958d9b43853fee4b6f4d8f7ffc80 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 15 Jul 2025 10:24:51 -0700 Subject: [PATCH 7/8] small type refactor --- mock-api/msw/db.ts | 11 ++++++++--- mock-api/msw/handlers.ts | 15 +++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 5adc3d9a59..7c758551d1 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -60,11 +60,16 @@ export const resolveIpPool = (poolNameOrId: string | undefined | null) => ? lookup.ipPool({ pool: poolNameOrId }) : lookup.siloDefaultIpPool({ silo: defaultSilo.id }) -export const getIpFromPool = (poolNameOrId: string | undefined | null) => { - const pool = resolveIpPool(poolNameOrId) +export const getIpFromPool = ( + params: { pool: Json } | { poolNameOrId: string | undefined | null } +) => { + const pool = 'pool' in params ? params.pool : resolveIpPool(params.poolNameOrId) const ipPoolRange = db.ipPoolRanges.find((range) => range.ip_pool_id === pool.id) if (!ipPoolRange) { - const poolLabel = poolNameOrId ? `pool '${pool.name}'` : 'default pool' + if ('pool' in params) { + throw notFoundErr(`IP range for pool '${pool.name}'`) + } + const poolLabel = params.poolNameOrId ? `pool '${params.poolNameOrId}'` : 'default pool' throw notFoundErr(`IP range for ${poolLabel}`) } diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 70d0224ae2..30a84af197 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -478,7 +478,7 @@ export const handlers = makeHandlers({ // if there are no ranges in the pool or if the pool doesn't exist, // which aren't quite as good as checking that there are actually IPs // available, but they are good things to check - getIpFromPool(ip.pool) + getIpFromPool({ poolNameOrId: ip.pool }) } }) @@ -565,7 +565,7 @@ export const handlers = makeHandlers({ floatingIp.instance_id = instanceId } else if (ip.type === 'ephemeral') { const pool = resolveIpPool(ip.pool) - const firstAvailableAddress = getIpFromPool(pool.id) + const firstAvailableAddress = getIpFromPool({ pool }) db.ephemeralIps.push({ instance_id: instanceId, @@ -740,15 +740,10 @@ export const handlers = makeHandlers({ }, instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) - const { pool } = body - const poolObj = resolveIpPool(pool) - const firstAvailableAddress = getIpFromPool(poolObj.id) + const pool = resolveIpPool(body.pool) + const ip = getIpFromPool({ pool }) - const externalIp = { - ip: firstAvailableAddress, - ip_pool_id: poolObj.id, - kind: 'ephemeral' as const, - } + const externalIp = { ip, ip_pool_id: pool.id, kind: 'ephemeral' as const } db.ephemeralIps.push({ instance_id: instance.id, external_ip: externalIp, From 66525ff1fe8792f6d9d93f9acc674771220e26da Mon Sep 17 00:00:00 2001 From: David Crespo Date: Tue, 15 Jul 2025 12:52:57 -0500 Subject: [PATCH 8/8] shorter getIpFromPool --- mock-api/msw/db.ts | 25 +++++++++---------------- mock-api/msw/handlers.ts | 7 ++++--- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 7c758551d1..8bdcbedc61 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -55,23 +55,16 @@ function ensureNoParentSelectors( } } -export const resolveIpPool = (poolNameOrId: string | undefined | null) => - poolNameOrId - ? lookup.ipPool({ pool: poolNameOrId }) - : lookup.siloDefaultIpPool({ silo: defaultSilo.id }) - -export const getIpFromPool = ( - params: { pool: Json } | { poolNameOrId: string | undefined | null } -) => { - const pool = 'pool' in params ? params.pool : resolveIpPool(params.poolNameOrId) +/** + * If pool name or ID is given, look it up. Otherwise use silo default pool, + * (and error if the silo doesn't have one). + */ +export const resolveIpPool = (pool: string | undefined | null) => + pool ? lookup.ipPool({ pool }) : lookup.siloDefaultIpPool({ silo: defaultSilo.id }) + +export const getIpFromPool = (pool: Json) => { const ipPoolRange = db.ipPoolRanges.find((range) => range.ip_pool_id === pool.id) - if (!ipPoolRange) { - if ('pool' in params) { - throw notFoundErr(`IP range for pool '${pool.name}'`) - } - const poolLabel = params.poolNameOrId ? `pool '${params.poolNameOrId}'` : 'default pool' - throw notFoundErr(`IP range for ${poolLabel}`) - } + if (!ipPoolRange) throw notFoundErr(`IP range for pool '${pool.name}'`) // right now, we're just using the first address in the range, but we'll // want to filter the list of available IPs for the first unused address diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 30a84af197..0399ed0490 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -478,7 +478,8 @@ export const handlers = makeHandlers({ // if there are no ranges in the pool or if the pool doesn't exist, // which aren't quite as good as checking that there are actually IPs // available, but they are good things to check - getIpFromPool({ poolNameOrId: ip.pool }) + const pool = resolveIpPool(ip.pool) + getIpFromPool(pool) } }) @@ -565,7 +566,7 @@ export const handlers = makeHandlers({ floatingIp.instance_id = instanceId } else if (ip.type === 'ephemeral') { const pool = resolveIpPool(ip.pool) - const firstAvailableAddress = getIpFromPool({ pool }) + const firstAvailableAddress = getIpFromPool(pool) db.ephemeralIps.push({ instance_id: instanceId, @@ -741,7 +742,7 @@ export const handlers = makeHandlers({ instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) const pool = resolveIpPool(body.pool) - const ip = getIpFromPool({ pool }) + const ip = getIpFromPool(pool) const externalIp = { ip, ip_pool_id: pool.id, kind: 'ephemeral' as const } db.ephemeralIps.push({