diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 1cbe62eb7..cd88dc8c1 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -af4a78e9cfcfeda9cf99156744c0efb913a9c1f8 +76ec1fa523c55daa1b927a6caada60a50e7409ab diff --git a/app/api/__generated__/Api.ts b/app/api/__generated__/Api.ts index 72faf2ffb..840ea669c 100644 --- a/app/api/__generated__/Api.ts +++ b/app/api/__generated__/Api.ts @@ -2533,21 +2533,23 @@ export type InstanceSerialConsoleData = { * Parameters of an `Instance` that can be reconfigured after creation. */ export type InstanceUpdate = { - /** Sets the auto-restart policy for this instance. + /** The auto-restart policy for this instance. This policy determines whether the instance should be automatically restarted by the control plane on failure. If this is `null`, any explicitly configured auto-restart policy will be unset, and the control plane will select the default policy when determining whether the instance can be automatically restarted. Currently, the global default auto-restart policy is "best-effort", so instances with `null` auto-restart policies will be automatically restarted. However, in the future, the default policy may be configurable through other mechanisms, such as on a per-project basis. In that case, any configured default policy will be used if this is `null`. */ autoRestartPolicy: InstanceAutoRestartPolicy | null - /** Name or ID of the disk the instance should be instructed to boot from. + /** The disk the instance is configured to boot from. -A null value unsets the boot disk. */ +Setting a boot disk is optional but recommended to ensure predictable boot behavior. The boot disk can be set during instance creation or later if the instance is stopped. The boot disk counts against the disk attachment limit. + +An instance that does not have a boot disk set will use the boot options specified in its UEFI settings, which are controlled by both the instance's UEFI firmware and the guest operating system. Boot options can change as disks are attached and detached, which may result in an instance that only boots to the EFI shell until a boot disk is set. */ bootDisk: NameOrId | null - /** The CPU platform to be used for this instance. If this is `null`, the instance requires no particular CPU platform. */ + /** The CPU platform to be used for this instance. If this is `null`, the instance requires no particular CPU platform; when it is started the instance will have the most general CPU platform supported by the sled it is initially placed on. */ cpuPlatform: InstanceCpuPlatform | null - /** The amount of memory to assign to this instance. */ + /** The amount of RAM (in bytes) to be allocated to the instance */ memory: ByteCount - /** The number of CPUs to assign to this instance. */ + /** The number of vCPUs to be allocated to the instance */ ncpus: InstanceCpuCount } @@ -2667,6 +2669,26 @@ export type InternetGatewayResultsPage = { nextPage?: string | null } +/** + * A count of bytes / rows accessed during a query. + */ +export type IoCount = { + /** The number of bytes accessed. */ + bytes: number + /** The number of rows accessed. */ + rows: number +} + +/** + * Summary of the I/O resources used by a query. + */ +export type IoSummary = { + /** The bytes and rows read by the query. */ + read: IoCount + /** The bytes and rows written by the query. */ + written: IoCount +} + /** * The IP address version. */ @@ -3068,6 +3090,20 @@ export type NetworkInterface = { vni: Vni } +/** + * Basic metadata about the resource usage of a single ClickHouse SQL query. + */ +export type OxqlQuerySummary = { + /** The total duration of the ClickHouse query (network plus execution). */ + elapsedMs: number + /** The database-assigned query ID. */ + id: string + /** Summary of the data read and written. */ + ioSummary: IoSummary + /** The raw ClickHouse SQL query. */ + query: string +} + /** * List of data values for one timeseries. * @@ -3119,6 +3155,8 @@ export type OxqlTable = { * The result of a successful OxQL query. */ export type OxqlQueryResult = { + /** Summaries of queries run against ClickHouse. */ + querySummaries?: OxqlQuerySummary[] | null /** Tables resulting from the query, each containing timeseries. */ tables: OxqlTable[] } @@ -3530,6 +3568,22 @@ export type SamlIdentityProviderCreate = { technicalContactEmail: string } +export type ScimClientBearerToken = { + id: string + timeCreated: Date + timeExpires?: Date | null +} + +/** + * The POST response is the only time the generated bearer token is returned to the client. + */ +export type ScimClientBearerTokenValue = { + bearerToken: string + id: string + timeCreated: Date + timeExpires?: Date | null +} + /** * Configuration of inbound ICMP allowed by API services. */ @@ -3556,6 +3610,9 @@ export type SiloIdentityMode = /** The system is the source of truth about users. There is no linkage to an external authentication provider or identity provider. */ | 'local_only' + /** Users are authenticated with SAML using an external authentication provider. Users and groups are managed with SCIM API calls, likely from the same authentication provider. */ + | 'saml_scim' + /** * View of a Silo * @@ -4396,6 +4453,8 @@ export type TimeseriesName = string * A timeseries query string, written in the Oximeter query language. */ export type TimeseriesQuery = { + /** Whether to include ClickHouse query summaries in the response. */ + includeSummaries?: boolean /** A timeseries query string, written in the Oximeter query language. */ query: string } @@ -6239,6 +6298,34 @@ export interface NetworkingSwitchPortSettingsViewPathParams { port: NameOrId } +export interface ScimTokenListQueryParams { + silo: NameOrId +} + +export interface ScimTokenCreateQueryParams { + silo: NameOrId +} + +export interface ScimTokenDeleteAllQueryParams { + silo: NameOrId +} + +export interface ScimTokenViewPathParams { + tokenId: string +} + +export interface ScimTokenViewQueryParams { + silo: NameOrId +} + +export interface ScimTokenDeletePathParams { + tokenId: string +} + +export interface ScimTokenDeleteQueryParams { + silo: NameOrId +} + export interface SystemQuotasListQueryParams { limit?: number | null pageToken?: string | null @@ -9824,6 +9911,79 @@ export class Api extends HttpClient { ...params, }) }, + /** + * List SCIM tokens + */ + scimTokenList: ( + { query }: { query: ScimTokenListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/scim/tokens`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Create SCIM token + */ + scimTokenCreate: ( + { query }: { query: ScimTokenCreateQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/scim/tokens`, + method: 'POST', + query, + ...params, + }) + }, + /** + * Delete all SCIM tokens + */ + scimTokenDeleteAll: ( + { query }: { query: ScimTokenDeleteAllQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/scim/tokens`, + method: 'DELETE', + query, + ...params, + }) + }, + /** + * Fetch SCIM token + */ + scimTokenView: ( + { path, query }: { path: ScimTokenViewPathParams; query: ScimTokenViewQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/scim/tokens/${path.tokenId}`, + method: 'GET', + query, + ...params, + }) + }, + /** + * Delete SCIM token + */ + scimTokenDelete: ( + { + path, + query, + }: { path: ScimTokenDeletePathParams; query: ScimTokenDeleteQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/scim/tokens/${path.tokenId}`, + method: 'DELETE', + query, + ...params, + }) + }, /** * Lists resource quotas for all silos */ diff --git a/app/api/__generated__/OMICRON_VERSION b/app/api/__generated__/OMICRON_VERSION index 297faee4b..b434a42ce 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 -af4a78e9cfcfeda9cf99156744c0efb913a9c1f8 +76ec1fa523c55daa1b927a6caada60a50e7409ab diff --git a/app/api/__generated__/msw-handlers.ts b/app/api/__generated__/msw-handlers.ts index 49e3afd79..a98a9aa06 100644 --- a/app/api/__generated__/msw-handlers.ts +++ b/app/api/__generated__/msw-handlers.ts @@ -1418,6 +1418,38 @@ export interface MSWHandlers { req: Request cookies: Record }) => Promisable> + /** `GET /v1/system/scim/tokens` */ + scimTokenList: (params: { + query: Api.ScimTokenListQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `POST /v1/system/scim/tokens` */ + scimTokenCreate: (params: { + query: Api.ScimTokenCreateQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/system/scim/tokens` */ + scimTokenDeleteAll: (params: { + query: Api.ScimTokenDeleteAllQueryParams + req: Request + cookies: Record + }) => Promisable + /** `GET /v1/system/scim/tokens/:tokenId` */ + scimTokenView: (params: { + path: Api.ScimTokenViewPathParams + query: Api.ScimTokenViewQueryParams + req: Request + cookies: Record + }) => Promisable> + /** `DELETE /v1/system/scim/tokens/:tokenId` */ + scimTokenDelete: (params: { + path: Api.ScimTokenDeletePathParams + query: Api.ScimTokenDeleteQueryParams + req: Request + cookies: Record + }) => Promisable /** `GET /v1/system/silo-quotas` */ systemQuotasList: (params: { query: Api.SystemQuotasListQueryParams @@ -3067,6 +3099,26 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { '/v1/system/policy', handler(handlers['systemPolicyUpdate'], null, schema.FleetRolePolicy) ), + http.get( + '/v1/system/scim/tokens', + handler(handlers['scimTokenList'], schema.ScimTokenListParams, null) + ), + http.post( + '/v1/system/scim/tokens', + handler(handlers['scimTokenCreate'], schema.ScimTokenCreateParams, null) + ), + http.delete( + '/v1/system/scim/tokens', + handler(handlers['scimTokenDeleteAll'], schema.ScimTokenDeleteAllParams, null) + ), + http.get( + '/v1/system/scim/tokens/:tokenId', + handler(handlers['scimTokenView'], schema.ScimTokenViewParams, null) + ), + http.delete( + '/v1/system/scim/tokens/:tokenId', + handler(handlers['scimTokenDelete'], schema.ScimTokenDeleteParams, null) + ), http.get( '/v1/system/silo-quotas', handler(handlers['systemQuotasList'], schema.SystemQuotasListParams, null) diff --git a/app/api/__generated__/validate.ts b/app/api/__generated__/validate.ts index 75bf5e980..09ea5436c 100644 --- a/app/api/__generated__/validate.ts +++ b/app/api/__generated__/validate.ts @@ -2453,6 +2453,22 @@ export const InternetGatewayResultsPage = z.preprocess( z.object({ items: InternetGateway.array(), nextPage: z.string().nullable().optional() }) ) +/** + * A count of bytes / rows accessed during a query. + */ +export const IoCount = z.preprocess( + processResponseBody, + z.object({ bytes: z.number().min(0), rows: z.number().min(0) }) +) + +/** + * Summary of the I/O resources used by a query. + */ +export const IoSummary = z.preprocess( + processResponseBody, + z.object({ read: IoCount, written: IoCount }) +) + /** * The IP address version. */ @@ -2820,6 +2836,19 @@ export const NetworkInterface = z.preprocess( }) ) +/** + * Basic metadata about the resource usage of a single ClickHouse SQL query. + */ +export const OxqlQuerySummary = z.preprocess( + processResponseBody, + z.object({ + elapsedMs: z.number().min(0), + id: z.uuid(), + ioSummary: IoSummary, + query: z.string(), + }) +) + /** * List of data values for one timeseries. * @@ -2888,7 +2917,10 @@ export const OxqlTable = z.preprocess( */ export const OxqlQueryResult = z.preprocess( processResponseBody, - z.object({ tables: OxqlTable.array() }) + z.object({ + querySummaries: OxqlQuerySummary.array().optional(), + tables: OxqlTable.array(), + }) ) /** @@ -3252,6 +3284,28 @@ export const SamlIdentityProviderCreate = z.preprocess( }) ) +export const ScimClientBearerToken = z.preprocess( + processResponseBody, + z.object({ + id: z.uuid(), + timeCreated: z.coerce.date(), + timeExpires: z.coerce.date().nullable().optional(), + }) +) + +/** + * The POST response is the only time the generated bearer token is returned to the client. + */ +export const ScimClientBearerTokenValue = z.preprocess( + processResponseBody, + z.object({ + bearerToken: z.string(), + id: z.uuid(), + timeCreated: z.coerce.date(), + timeExpires: z.coerce.date().nullable().optional(), + }) +) + /** * Configuration of inbound ICMP allowed by API services. */ @@ -3279,7 +3333,7 @@ export const SetTargetReleaseParams = z.preprocess( */ export const SiloIdentityMode = z.preprocess( processResponseBody, - z.enum(['saml_jit', 'local_only']) + z.enum(['saml_jit', 'local_only', 'saml_scim']) ) /** @@ -4000,7 +4054,7 @@ export const TimeseriesName = z.preprocess( */ export const TimeseriesQuery = z.preprocess( processResponseBody, - z.object({ query: z.string() }) + z.object({ includeSummaries: SafeBoolean.default(false).optional(), query: z.string() }) ) /** @@ -7017,6 +7071,60 @@ export const SystemPolicyUpdateParams = z.preprocess( }) ) +export const ScimTokenListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + silo: NameOrId, + }), + }) +) + +export const ScimTokenCreateParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + silo: NameOrId, + }), + }) +) + +export const ScimTokenDeleteAllParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + silo: NameOrId, + }), + }) +) + +export const ScimTokenViewParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + tokenId: z.uuid(), + }), + query: z.object({ + silo: NameOrId, + }), + }) +) + +export const ScimTokenDeleteParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + tokenId: z.uuid(), + }), + query: z.object({ + silo: NameOrId, + }), + }) +) + export const SystemQuotasListParams = z.preprocess( processResponseBody, z.object({ diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts index 4b16b60b7..e35dce40b 100644 --- a/mock-api/msw/db.ts +++ b/mock-api/msw/db.ts @@ -504,6 +504,7 @@ const initDb = { projects: [...mock.projects], racks: [...mock.racks], roleAssignments: [...mock.roleAssignments], + scimTokens: [...mock.scimTokens], silos: [...mock.silos], siloQuotas: [...mock.siloQuotas], siloProvisioned: [...mock.siloProvisioned], diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts index 7aaf91adc..872c02688 100644 --- a/mock-api/msw/handlers.ts +++ b/mock-api/msw/handlers.ts @@ -1808,6 +1808,50 @@ export const handlers = makeHandlers({ return paginated(query, affinityGroups) }, + // SCIM token endpoints + scimTokenList({ query, cookies }) { + requireFleetViewer(cookies) + const silo = lookup.silo({ silo: query.silo }) + // Filter by silo and strip out the siloId before returning + const tokens = db.scimTokens + .filter((t) => t.siloId === silo.id) + .map(({ siloId: _siloId, ...token }) => token) + return tokens + }, + scimTokenCreate({ query, cookies }) { + requireFleetCollab(cookies) + const silo = lookup.silo({ silo: query.silo }) + const newToken: Json = { + id: uuid(), + bearer_token: `token_${uuid()}`, + time_created: new Date().toISOString(), + time_expires: null, + } + // Store without the bearer_token but with siloId for filtering + const { bearer_token: _bearerToken, ...tokenWithoutBearer } = newToken + db.scimTokens.push({ ...tokenWithoutBearer, siloId: silo.id }) + return json(newToken, { status: 201 }) + }, + scimTokenView({ path, cookies }) { + requireFleetViewer(cookies) + const token = lookupById(db.scimTokens, path.tokenId) + // Strip out siloId before returning + const { siloId: _siloId, ...tokenResponse } = token + return tokenResponse + }, + scimTokenDelete({ path, cookies }) { + requireFleetCollab(cookies) + const token = lookupById(db.scimTokens, path.tokenId) + db.scimTokens = db.scimTokens.filter((t) => t.id !== token.id) + return 204 + }, + scimTokenDeleteAll({ query, cookies }) { + requireFleetCollab(cookies) + const silo = lookup.silo({ silo: query.silo }) + db.scimTokens = db.scimTokens.filter((t) => t.siloId !== silo.id) + return 204 + }, + // Misc endpoints we're not using yet in the console affinityGroupCreate: NotImplemented, affinityGroupDelete: NotImplemented, diff --git a/mock-api/silo.ts b/mock-api/silo.ts index 48fd8a551..1d8de9a11 100644 --- a/mock-api/silo.ts +++ b/mock-api/silo.ts @@ -10,6 +10,7 @@ import * as R from 'remeda' import type { IdentityProvider, SamlIdentityProvider, + ScimClientBearerToken, Silo, SiloAuthSettings, SiloQuotas, @@ -128,3 +129,27 @@ export const siloSettings: Json[] = [ device_token_max_ttl_seconds: 7200, // 2 hours in seconds }, ] + +// SCIM tokens are stored with siloId for filtering, similar to identity providers +type DbScimToken = Json & { siloId: string } + +export const scimTokens: DbScimToken[] = [ + { + id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + time_created: new Date(2024, 0, 15).toISOString(), + time_expires: null, + siloId: defaultSilo.id, + }, + { + id: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + time_created: new Date(2024, 1, 20).toISOString(), + time_expires: new Date(2025, 1, 20).toISOString(), + siloId: defaultSilo.id, + }, + { + id: 'c3d4e5f6-a7b8-9012-cdef-123456789012', + time_created: new Date(2024, 2, 10).toISOString(), + time_expires: null, + siloId: silos[1].id, + }, +]