From 041125c3bacdaaa096709575c77a6ba51e02ada7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 22 Jan 2024 16:00:00 -0600 Subject: [PATCH] bump API for silo IP pools list endpoint and implement endpoints --- OMICRON_VERSION | 2 +- libs/api-mocks/ip-pool.ts | 4 +- libs/api-mocks/msw/db.ts | 25 +++++++- libs/api-mocks/msw/handlers.ts | 23 ++++++- libs/api/__generated__/Api.ts | 86 +++++++++++++++++++++----- libs/api/__generated__/OMICRON_VERSION | 2 +- libs/api/__generated__/msw-handlers.ts | 25 +++++--- libs/api/__generated__/validate.ts | 53 +++++++++++++--- 8 files changed, 182 insertions(+), 38 deletions(-) diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 0fb87828ce..f5f3cdce79 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -19059b1bedc8bbc7a9d293486842a9a4fd264ea3 +e8b6dd1dc4e7abb39276ad347bdf1ac08171862d diff --git a/libs/api-mocks/ip-pool.ts b/libs/api-mocks/ip-pool.ts index f3accd49cd..51905e14e8 100644 --- a/libs/api-mocks/ip-pool.ts +++ b/libs/api-mocks/ip-pool.ts @@ -6,7 +6,7 @@ * Copyright Oxide Computer Company */ -import { type IpPool, type IpPoolSilo } from '@oxide/api' +import { type IpPool, type IpPoolSiloLink } from '@oxide/api' import type { Json } from './json-type' import { defaultSilo } from './silo' @@ -29,7 +29,7 @@ const ipPool2: Json = { export const ipPools: Json[] = [ipPool1, ipPool2] -export const ipPoolSilos: Json[] = [ +export const ipPoolSilos: Json[] = [ { ip_pool_id: ipPool1.id, silo_id: defaultSilo.id, diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 0f01be5594..b54b65e759 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -141,7 +141,10 @@ export const lookup = { return pool }, // unusual one because it's a sibling relationship. we look up both the pool and the silo first - ipPoolSilo({ pool: poolId, silo: siloId }: PP.IpPool & PP.Silo): Json { + ipPoolSiloLink({ + pool: poolId, + silo: siloId, + }: PP.IpPool & PP.Silo): Json { const pool = lookup.ipPool({ pool: poolId }) const silo = lookup.silo({ silo: siloId }) @@ -152,6 +155,26 @@ export const lookup = { return ipPoolSilo }, + // unusual because it returns a list, but we need it for multiple endpoints + siloIpPools(path: PP.Silo): Json[] { + const silo = lookup.silo(path) + + // effectively join db.ipPools and db.ipPoolSilos on ip_pool_id + return db.ipPoolSilos + .filter((link) => link.silo_id === silo.id) + .map((link) => { + const pool = db.ipPools.find((pool) => pool.id === link.ip_pool_id) + + // this should never happen + if (!pool) { + const linkStr = JSON.stringify(link) + const message = `Found IP pool-silo link without corresponding pool: ${linkStr}` + throw json({ message }, { status: 500 }) + } + + return { ...pool, is_default: link.is_default } + }) + }, samlIdp({ provider: id, ...siloSelector diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index b5037d4e7c..41542f9f2c 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -543,6 +543,25 @@ export const handlers = makeHandlers({ return json(instance, { status: 202 }) }, ipPoolList: ({ query }) => paginated(query, db.ipPools), + siloIpPoolList({ path, query }) { + const pools = lookup.siloIpPools(path) + return paginated(query, pools) + }, + projectIpPoolList({ query }) { + const pools = lookup.siloIpPools({ silo: defaultSilo.id }) + return paginated(query, pools) + }, + projectIpPoolView({ path }) { + // this will 404 if it doesn't exist at all... + const pool = lookup.ipPool(path) + // but we also want to 404 if it exists but isn't in the silo + const link = db.ipPoolSilos.find( + (link) => link.ip_pool_id === pool.id && link.silo_id === defaultSilo.id + ) + if (!link) throw notFoundErr() + + return { ...pool, is_default: link.is_default } + }, ipPoolView: ({ path }) => lookup.ipPool(path), ipPoolSiloList({ path /*query*/ }) { // TODO: paginated wants an id field, but this is a join table, so it has a @@ -585,7 +604,7 @@ export const handlers = makeHandlers({ return 204 }, ipPoolSiloUpdate: ({ path, body }) => { - const ipPoolSilo = lookup.ipPoolSilo(path) + const ipPoolSilo = lookup.ipPoolSiloLink(path) // if we're setting default, we need to set is_default false on the existing default if (body.is_default) { @@ -1062,8 +1081,6 @@ export const handlers = makeHandlers({ networkingSwitchPortSettingsDelete: NotImplemented, networkingSwitchPortSettingsView: NotImplemented, networkingSwitchPortSettingsList: NotImplemented, - projectIpPoolList: NotImplemented, - projectIpPoolView: NotImplemented, rackView: NotImplemented, roleList: NotImplemented, roleView: NotImplemented, diff --git a/libs/api/__generated__/Api.ts b/libs/api/__generated__/Api.ts index d981991c33..09e4cd8e05 100644 --- a/libs/api/__generated__/Api.ts +++ b/libs/api/__generated__/Api.ts @@ -1387,6 +1387,12 @@ export type IpPool = { */ export type IpPoolCreate = { description: string; name: Name } +export type IpPoolLinkSilo = { + /** When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified. There can be at most one default for a given silo. */ + isDefault: boolean + silo: NameOrId +} + /** * A non-decreasing IPv4 address range, inclusive of both ends. * @@ -1433,25 +1439,19 @@ export type IpPoolResultsPage = { /** * A link between an IP pool and a silo that allows one to allocate IPs from the pool within the silo */ -export type IpPoolSilo = { +export type IpPoolSiloLink = { ipPoolId: string /** When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified. There can be at most one default for a given silo. */ isDefault: boolean siloId: string } -export type IpPoolSiloLink = { - /** When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified. There can be at most one default for a given silo. */ - isDefault: boolean - silo: NameOrId -} - /** * A single page of results */ -export type IpPoolSiloResultsPage = { +export type IpPoolSiloLinkResultsPage = { /** list of items on this page of results */ - items: IpPoolSilo[] + items: IpPoolSiloLink[] /** token used to fetch the next page of results (if any) */ nextPage?: string } @@ -1899,6 +1899,34 @@ The default is that no Fleet roles are conferred by any Silo roles unless there' tlsCertificates: CertificateCreate[] } +/** + * An IP pool in the context of a silo + */ +export type SiloIpPool = { + /** human-readable free-form text about a resource */ + description: string + /** unique, immutable, system-controlled identifier for each resource */ + id: string + /** When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified. There can be at most one default for a given silo. */ + isDefault: boolean + /** unique, mutable, user-controlled identifier for each resource */ + name: Name + /** timestamp when this resource was created */ + timeCreated: Date + /** timestamp when this resource was last modified */ + timeModified: Date +} + +/** + * A single page of results + */ +export type SiloIpPoolResultsPage = { + /** list of items on this page of results */ + items: SiloIpPool[] + /** token used to fetch the next page of results (if any) */ + nextPage?: string +} + /** * A collection of resource counts used to set the virtual capacity of a silo */ @@ -3579,6 +3607,16 @@ export interface SiloDeletePathParams { silo: NameOrId } +export interface SiloIpPoolListPathParams { + silo: NameOrId +} + +export interface SiloIpPoolListQueryParams { + limit?: number + pageToken?: string + sortBy?: NameOrIdSortMode +} + export interface SiloPolicyViewPathParams { silo: NameOrId } @@ -3771,6 +3809,7 @@ export type ApiListMethods = Pick< | 'roleList' | 'systemQuotasList' | 'siloList' + | 'siloIpPoolList' | 'siloUserList' | 'userBuiltinList' | 'siloUtilizationList' @@ -4459,7 +4498,7 @@ export class Api extends HttpClient { { query = {} }: { query?: ProjectIpPoolListQueryParams }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/ip-pools`, method: 'GET', query, @@ -4473,7 +4512,7 @@ export class Api extends HttpClient { { path }: { path: ProjectIpPoolViewPathParams }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/ip-pools/${path.pool}`, method: 'GET', ...params, @@ -5322,7 +5361,7 @@ export class Api extends HttpClient { }: { path: IpPoolSiloListPathParams; query?: IpPoolSiloListQueryParams }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/system/ip-pools/${path.pool}/silos`, method: 'GET', query, @@ -5333,10 +5372,10 @@ export class Api extends HttpClient { * Make an IP pool available within a silo */ ipPoolSiloLink: ( - { path, body }: { path: IpPoolSiloLinkPathParams; body: IpPoolSiloLink }, + { path, body }: { path: IpPoolSiloLinkPathParams; body: IpPoolLinkSilo }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/system/ip-pools/${path.pool}/silos`, method: 'POST', body, @@ -5350,7 +5389,7 @@ export class Api extends HttpClient { { path, body }: { path: IpPoolSiloUpdatePathParams; body: IpPoolSiloUpdate }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/system/ip-pools/${path.pool}/silos/${path.silo}`, method: 'PUT', body, @@ -5802,6 +5841,23 @@ export class Api extends HttpClient { ...params, }) }, + /** + * List IP pools available within silo + */ + siloIpPoolList: ( + { + path, + query = {}, + }: { path: SiloIpPoolListPathParams; query?: SiloIpPoolListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/silos/${path.silo}/ip-pools`, + method: 'GET', + query, + ...params, + }) + }, /** * Fetch a silo's IAM policy */ diff --git a/libs/api/__generated__/OMICRON_VERSION b/libs/api/__generated__/OMICRON_VERSION index 2f27ab7450..3f7bb17192 100644 --- a/libs/api/__generated__/OMICRON_VERSION +++ b/libs/api/__generated__/OMICRON_VERSION @@ -1,2 +1,2 @@ # generated file. do not update manually. see docs/update-pinned-api.md -19059b1bedc8bbc7a9d293486842a9a4fd264ea3 +e8b6dd1dc4e7abb39276ad347bdf1ac08171862d diff --git a/libs/api/__generated__/msw-handlers.ts b/libs/api/__generated__/msw-handlers.ts index 3e76270e71..5507002f07 100644 --- a/libs/api/__generated__/msw-handlers.ts +++ b/libs/api/__generated__/msw-handlers.ts @@ -340,13 +340,13 @@ export interface MSWHandlers { query: Api.ProjectIpPoolListQueryParams req: Request cookies: Record - }) => Promisable> + }) => Promisable> /** `GET /v1/ip-pools/:pool` */ projectIpPoolView: (params: { path: Api.ProjectIpPoolViewPathParams req: Request cookies: Record - }) => Promisable> + }) => Promisable> /** `POST /v1/login/:siloName/local` */ loginLocal: (params: { path: Api.LoginLocalPathParams @@ -719,21 +719,21 @@ export interface MSWHandlers { query: Api.IpPoolSiloListQueryParams req: Request cookies: Record - }) => Promisable> + }) => Promisable> /** `POST /v1/system/ip-pools/:pool/silos` */ ipPoolSiloLink: (params: { path: Api.IpPoolSiloLinkPathParams - body: Json + body: Json req: Request cookies: Record - }) => Promisable> + }) => Promisable> /** `PUT /v1/system/ip-pools/:pool/silos/:silo` */ ipPoolSiloUpdate: (params: { path: Api.IpPoolSiloUpdatePathParams body: Json req: Request cookies: Record - }) => Promisable> + }) => Promisable> /** `DELETE /v1/system/ip-pools/:pool/silos/:silo` */ ipPoolSiloUnlink: (params: { path: Api.IpPoolSiloUnlinkPathParams @@ -937,6 +937,13 @@ export interface MSWHandlers { req: Request cookies: Record }) => Promisable + /** `GET /v1/system/silos/:silo/ip-pools` */ + siloIpPoolList: (params: { + path: Api.SiloIpPoolListPathParams + query: Api.SiloIpPoolListQueryParams + req: Request + cookies: Record + }) => Promisable> /** `GET /v1/system/silos/:silo/policy` */ siloPolicyView: (params: { path: Api.SiloPolicyViewPathParams @@ -1696,7 +1703,7 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { handler( handlers['ipPoolSiloLink'], schema.IpPoolSiloLinkParams, - schema.IpPoolSiloLink + schema.IpPoolLinkSilo ) ), http.put( @@ -1897,6 +1904,10 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { '/v1/system/silos/:silo', handler(handlers['siloDelete'], schema.SiloDeleteParams, null) ), + http.get( + '/v1/system/silos/:silo/ip-pools', + handler(handlers['siloIpPoolList'], schema.SiloIpPoolListParams, null) + ), http.get( '/v1/system/silos/:silo/policy', handler(handlers['siloPolicyView'], schema.SiloPolicyViewParams, null) diff --git a/libs/api/__generated__/validate.ts b/libs/api/__generated__/validate.ts index 0fab944772..fa38a6f527 100644 --- a/libs/api/__generated__/validate.ts +++ b/libs/api/__generated__/validate.ts @@ -1472,6 +1472,11 @@ export const IpPoolCreate = z.preprocess( z.object({ description: z.string(), name: Name }) ) +export const IpPoolLinkSilo = z.preprocess( + processResponseBody, + z.object({ isDefault: SafeBoolean, silo: NameOrId }) +) + /** * A non-decreasing IPv4 address range, inclusive of both ends. * @@ -1523,7 +1528,7 @@ export const IpPoolResultsPage = z.preprocess( /** * A link between an IP pool and a silo that allows one to allocate IPs from the pool within the silo */ -export const IpPoolSilo = z.preprocess( +export const IpPoolSiloLink = z.preprocess( processResponseBody, z.object({ ipPoolId: z.string().uuid(), @@ -1532,17 +1537,12 @@ export const IpPoolSilo = z.preprocess( }) ) -export const IpPoolSiloLink = z.preprocess( - processResponseBody, - z.object({ isDefault: SafeBoolean, silo: NameOrId }) -) - /** * A single page of results */ -export const IpPoolSiloResultsPage = z.preprocess( +export const IpPoolSiloLinkResultsPage = z.preprocess( processResponseBody, - z.object({ items: IpPoolSilo.array(), nextPage: z.string().optional() }) + z.object({ items: IpPoolSiloLink.array(), nextPage: z.string().optional() }) ) export const IpPoolSiloUpdate = z.preprocess( @@ -1955,6 +1955,29 @@ export const SiloCreate = z.preprocess( }) ) +/** + * An IP pool in the context of a silo + */ +export const SiloIpPool = z.preprocess( + processResponseBody, + z.object({ + description: z.string(), + id: z.string().uuid(), + isDefault: SafeBoolean, + name: Name, + timeCreated: z.coerce.date(), + timeModified: z.coerce.date(), + }) +) + +/** + * A single page of results + */ +export const SiloIpPoolResultsPage = z.preprocess( + processResponseBody, + z.object({ items: SiloIpPool.array(), nextPage: z.string().optional() }) +) + /** * A collection of resource counts used to set the virtual capacity of a silo */ @@ -4363,6 +4386,20 @@ export const SiloDeleteParams = z.preprocess( }) ) +export const SiloIpPoolListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + silo: NameOrId, + }), + query: z.object({ + limit: z.number().min(1).max(4294967295).optional(), + pageToken: z.string().optional(), + sortBy: NameOrIdSortMode.optional(), + }), + }) +) + export const SiloPolicyViewParams = z.preprocess( processResponseBody, z.object({