diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 60adec500..02aaab0f8 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -17e4c0e5b0b697c63bd05d267c0fdaca2b2e79d7 +c65212d77d38581632bb972b606f581bd52c3298 diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index c26b8dbe3..456a6fb4d 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 */ @@ -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 { @@ -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 15c3dcf68..b91cbc9fc 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 +c65212d77d38581632bb972b606f581bd52c3298 diff --git a/app/api/__generated__/msw-handlers.ts b/app/api/__generated__/msw-handlers.ts index 6d7d002a4..1588ee744 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 71f5b1546..d94dedad7 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 */ @@ -4534,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 */ @@ -4562,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']) @@ -4652,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(), }), }) ) @@ -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 2493a99db..f90b6e90c 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/app/table/cells/IpPoolCell.tsx b/app/table/cells/IpPoolCell.tsx index f15a03fd8..03cbce193 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} diff --git a/mock-api/external-ip.ts b/mock-api/external-ip.ts index a90895b15..f444521e2 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 cecc37e66..8bdcbedc6 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) => { @@ -55,10 +55,16 @@ function ensureNoParentSelectors( } } -export const getIpFromPool = (poolName: string | undefined | null) => { - const pool = lookup.ipPool({ pool: poolName || undefined }) +/** + * 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) throw notFoundErr(`IP range for pool '${poolName}'`) + 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 8ea365cf4..0399ed049 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 { @@ -477,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(ip.pool) + const pool = resolveIpPool(ip.pool) + getIpFromPool(pool) } }) @@ -563,11 +565,14 @@ 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 = resolveIpPool(ip.pool) + const firstAvailableAddress = getIpFromPool(pool) + db.ephemeralIps.push({ instance_id: instanceId, external_ip: { ip: firstAvailableAddress, + ip_pool_id: pool.id, kind: 'ephemeral', }, }) @@ -736,12 +741,10 @@ export const handlers = makeHandlers({ }, instanceEphemeralIpAttach({ path, query: projectParams, body }) { const instance = lookup.instance({ ...path, ...projectParams }) - const { pool } = body - const firstAvailableAddress = getIpFromPool(pool) - const externalIp = { - ip: firstAvailableAddress, - kind: 'ephemeral' as const, - } + const pool = resolveIpPool(body.pool) + const ip = getIpFromPool(pool) + + const externalIp = { ip, ip_pool_id: pool.id, kind: 'ephemeral' as const } db.ephemeralIps.push({ instance_id: instance.id, external_ip: externalIp, @@ -1880,8 +1883,6 @@ export const handlers = makeHandlers({ probeList: NotImplemented, probeView: NotImplemented, rackView: NotImplemented, - roleList: NotImplemented, - roleView: NotImplemented, siloPolicyUpdate: NotImplemented, siloPolicyView: NotImplemented, siloUserList: NotImplemented, @@ -1904,6 +1905,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,