diff --git a/OMICRON_VERSION b/OMICRON_VERSION index 099033c..c4d0c2d 100644 --- a/OMICRON_VERSION +++ b/OMICRON_VERSION @@ -1 +1 @@ -2d8bdf93ba98cdfd680fab79abf7bcd407f4a37c +d7d70d8366ee106d49cc0c7bfae2c6aa499ecf3b diff --git a/client/Api.ts b/client/Api.ts index f25395b..1e9785c 100644 --- a/client/Api.ts +++ b/client/Api.ts @@ -152,11 +152,43 @@ export type AddressLotResultsPage = { nextPage?: string; }; +export type BgpMessageHistory = Record; + +/** + * Identifies switch physical location + */ +export type SwitchLocation = + /** Switch in upper slot */ + | "switch0" + /** Switch in lower slot */ + | "switch1"; + +/** + * BGP message history for a particular switch. + */ +export type SwitchBgpHistory = { + /** Message history indexed by peer address. */ + history: Record; + /** Switch this message history is associated with. */ + switch: SwitchLocation; +}; + +/** + * BGP message history for rack switches. + */ +export type AggregateBgpMessageHistory = { + /** BGP history organized by switch. */ + switchHistories: SwitchBgpHistory[]; +}; + /** * Properties that uniquely identify an Oxide hardware component */ export type Baseboard = { part: string; revision: number; serial: string }; +/** + * BFD connection mode. + */ export type BfdMode = "single_hop" | "multi_hop"; /** @@ -298,15 +330,6 @@ export type BgpConfigResultsPage = { nextPage?: string; }; -/** - * Identifies switch physical location - */ -export type SwitchLocation = - /** Switch in upper slot */ - | "switch0" - /** Switch in lower slot */ - | "switch1"; - /** * A route imported from a BGP peer. */ @@ -1017,6 +1040,16 @@ export type DiskResultsPage = { nextPage?: string; }; +/** + * A distribution is a sequence of bins and counts in those bins. + */ +export type Distributiondouble = { bins: number[]; counts: number[] }; + +/** + * A distribution is a sequence of bins and counts in those bins. + */ +export type Distributionint64 = { bins: number[]; counts: number[] }; + /** * Parameters for creating an ephemeral IP address for an instance. */ @@ -1069,6 +1102,54 @@ export type ExternalIpResultsPage = { nextPage?: string; }; +/** + * The `FieldType` identifies the data type of a target or metric field. + */ +export type FieldType = + | "string" + | "i8" + | "u8" + | "i16" + | "u16" + | "i32" + | "u32" + | "i64" + | "u64" + | "ip_addr" + | "uuid" + | "bool"; + +/** + * The source from which a field is derived, the target or metric. + */ +export type FieldSource = "target" | "metric"; + +/** + * The name and type information for a field of a timeseries schema. + */ +export type FieldSchema = { + fieldType: FieldType; + name: string; + source: FieldSource; +}; + +/** + * The `FieldValue` contains the value of a target or metric field. + */ +export type FieldValue = + | { type: "string"; value: string } + | { type: "i8"; value: number } + | { type: "u8"; value: number } + | { type: "i16"; value: number } + | { type: "u16"; value: number } + | { type: "i32"; value: number } + | { type: "u32"; value: number } + | { type: "i64"; value: number } + | { type: "u64"; value: number } + | { type: "ip_addr"; value: string } + | { type: "uuid"; value: string } + | { type: "bool"; value: boolean }; + /** * Parameters for finalizing a disk */ @@ -1622,6 +1703,27 @@ export type IpPoolSiloUpdate = { */ export type IpPoolUpdate = { description?: string; name?: Name }; +export type Ipv4Utilization = { + /** The number of IPv4 addresses allocated from this pool */ + allocated: number; + /** The total number of IPv4 addresses in the pool, i.e., the sum of the lengths of the IPv4 ranges. Unlike IPv6 capacity, can be a 32-bit integer because there are only 2^32 IPv4 addresses. */ + capacity: number; +}; + +export type Ipv6Utilization = { + /** The number of IPv6 addresses allocated from this pool. A 128-bit integer string to match the capacity field. */ + allocated: string; + /** The total number of IPv6 addresses in the pool, i.e., the sum of the lengths of the IPv6 ranges. An IPv6 range can contain up to 2^128 addresses, so we represent this value in JSON as a numeric string with a custom "uint128" format. */ + capacity: string; +}; + +export type IpPoolUtilization = { + /** Number of allocated and total available IPv4 addresses in pool */ + ipv4: Ipv4Utilization; + /** Number of allocated and total available IPv6 addresses in pool */ + ipv6: Ipv6Utilization; +}; + /** * A range of IP ports * @@ -1760,6 +1862,17 @@ export type MeasurementResultsPage = { nextPage?: string; }; +/** + * The type of the metric itself, indicating what its values represent. + */ +export type MetricType = + /** The value represents an instantaneous measurement in time. */ + | "gauge" + /** The value represents a difference between two points in time. */ + | "delta" + /** The value represents an accumulation between two points in time. */ + | "cumulative"; + /** * The type of network interface */ @@ -1803,6 +1916,30 @@ export type Password = string; */ export type PhysicalDiskKind = "m2" | "u2"; +/** + * The operator-defined policy of a physical disk. + */ +export type PhysicalDiskPolicy = + /** The operator has indicated that the disk is in-service. */ + | { kind: "in_service" } + /** The operator has indicated that the disk has been permanently removed from service. + +This is a terminal state: once a particular disk ID is expunged, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.) + +An expunged disk is always non-provisionable. */ + | { kind: "expunged" }; + +/** + * The current state of the disk, as determined by Nexus. + */ +export type PhysicalDiskState = + /** The disk is currently active, and has resources allocated on it. */ + | "active" + /** The disk has been permanently removed from service. + +This is a terminal state: once a particular disk ID is decommissioned, it will never return to service. (The actual hardware may be reused, but it will be treated as a brand-new disk.) */ + | "decommissioned"; + /** * View of a Physical Disk * @@ -1813,9 +1950,13 @@ export type PhysicalDisk = { /** unique, immutable, system-controlled identifier for each resource */ id: string; model: string; + /** The operator-defined policy for a physical disk. */ + policy: PhysicalDiskPolicy; serial: string; /** The sled to which this disk is attached, if any. */ sledId?: string; + /** The current state Nexus believes the disk to be in. */ + state: PhysicalDiskState; /** timestamp when this resource was created */ timeCreated: Date; /** timestamp when this resource was last modified */ @@ -1840,6 +1981,33 @@ export type Ping = { status: PingStatus; }; +/** + * List of data values for one timeseries. + * + * Each element is an option, where `None` represents a missing sample. + */ +export type ValueArray = + | { type: "integer"; values: number[] } + | { type: "double"; values: number[] } + | { type: "boolean"; values: boolean[] } + | { type: "string"; values: string[] } + | { type: "integer_distribution"; values: Distributionint64[] } + | { type: "double_distribution"; values: Distributiondouble[] }; + +/** + * A single list of values, for one dimension of a timeseries. + */ +export type Values = { metricType: MetricType; values: ValueArray }; + +/** + * Timepoints and values for one timeseries. + */ +export type Points = { + startTimes?: Date[]; + timestamps: Date[]; + values: Values[]; +}; + /** * Identity-related metadata that's included in nearly all public API objects */ @@ -2771,6 +2939,57 @@ export type SwitchResultsPage = { nextPage?: string; }; +/** + * A timeseries contains a timestamped set of values from one source. + * + * This includes the typed key-value pairs that uniquely identify it, and the set of timestamps and data values from it. + */ +export type Timeseries = { fields: Record; points: Points }; + +/** + * A table represents one or more timeseries with the same schema. + * + * A table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data. + */ +export type Table = { name: string; timeseries: Record }; + +/** + * The name of a timeseries + * + * Names are constructed by concatenating the target and metric names with ':'. Target and metric names must be lowercase alphanumeric characters with '_' separating words. + */ +export type TimeseriesName = string; + +/** + * A timeseries query string, written in the Oximeter query language. + */ +export type TimeseriesQuery = { + /** A timeseries query string, written in the Oximeter query language. */ + query: string; +}; + +/** + * The schema for a timeseries. + * + * This includes the name of the timeseries, as well as the datum type of its metric and the schema for each field. + */ +export type TimeseriesSchema = { + created: Date; + datumType: DatumType; + fieldSchema: FieldSchema[]; + timeseriesName: TimeseriesName; +}; + +/** + * A single page of results + */ +export type TimeseriesSchemaResultsPage = { + /** list of items on this page of results */ + items: TimeseriesSchema[]; + /** token used to fetch the next page of results (if any) */ + nextPage?: string; +}; + /** * A sled that has not been added to an initialized rack yet */ @@ -3661,6 +3880,10 @@ export interface PhysicalDiskListQueryParams { sortBy?: IdSortMode; } +export interface PhysicalDiskViewPathParams { + diskId: string; +} + export interface RackListQueryParams { limit?: number; pageToken?: string; @@ -3843,6 +4066,10 @@ export interface IpPoolSiloUnlinkPathParams { silo: NameOrId; } +export interface IpPoolUtilizationViewPathParams { + pool: NameOrId; +} + export interface IpPoolServiceRangeListQueryParams { limit?: number; pageToken?: string; @@ -3900,6 +4127,10 @@ export interface NetworkingBgpAnnounceSetDeleteQueryParams { nameOrId: NameOrId; } +export interface NetworkingBgpMessageHistoryQueryParams { + asn: number; +} + export interface NetworkingBgpImportedRoutesIpv4QueryParams { asn: number; } @@ -4022,6 +4253,11 @@ export interface SiloUtilizationViewPathParams { silo: NameOrId; } +export interface TimeseriesSchemaListQueryParams { + limit?: number; + pageToken?: string; +} + export interface UserListQueryParams { group?: string; limit?: number; @@ -4169,6 +4405,7 @@ export type ApiListMethods = Pick< | "siloUserList" | "userBuiltinList" | "siloUtilizationList" + | "timeseriesSchemaList" | "userList" | "vpcSubnetList" | "vpcList" @@ -4511,7 +4748,7 @@ export class Api extends HttpClient { }); }, /** - * List all floating IPs + * List floating IPs */ floatingIpList: ( { query = {} }: { query?: FloatingIpListQueryParams }, @@ -5082,7 +5319,7 @@ export class Api extends HttpClient { }); }, /** - * List all IP pools + * List IP pools */ projectIpPoolList: ( { query = {} }: { query?: ProjectIpPoolListQueryParams }, @@ -5214,7 +5451,7 @@ export class Api extends HttpClient { }); }, /** - * Access metrics data + * View metrics */ siloMetric: ( { @@ -5540,6 +5777,19 @@ export class Api extends HttpClient { ...params, }); }, + /** + * Get a physical disk + */ + physicalDiskView: ( + { path }: { path: PhysicalDiskViewPathParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/hardware/disks/${path.diskId}`, + method: "GET", + ...params, + }); + }, /** * List racks */ @@ -6059,6 +6309,19 @@ export class Api extends HttpClient { ...params, }); }, + /** + * Fetch IP pool utilization + */ + ipPoolUtilizationView: ( + { path }: { path: IpPoolUtilizationViewPathParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/ip-pools/${path.pool}/utilization`, + method: "GET", + ...params, + }); + }, /** * Fetch Oxide service IP pool */ @@ -6112,7 +6375,7 @@ export class Api extends HttpClient { }); }, /** - * Access metrics data + * View metrics */ systemMetric: ( { @@ -6221,7 +6484,7 @@ export class Api extends HttpClient { * Get BFD status */ networkingBfdStatus: (_: EmptyObj, params: FetchParams = {}) => { - return this.request({ + return this.request({ path: `/v1/system/networking/bfd-status`, method: "GET", ...params, @@ -6276,7 +6539,7 @@ export class Api extends HttpClient { { query }: { query?: NetworkingBgpAnnounceSetListQueryParams }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/system/networking/bgp-announce`, method: "GET", query, @@ -6311,6 +6574,20 @@ export class Api extends HttpClient { ...params, }); }, + /** + * Get BGP router message history + */ + networkingBgpMessageHistory: ( + { query }: { query?: NetworkingBgpMessageHistoryQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/system/networking/bgp-message-history`, + method: "GET", + query, + ...params, + }); + }, /** * Get imported IPv4 BGP routes */ @@ -6318,7 +6595,7 @@ export class Api extends HttpClient { { query }: { query?: NetworkingBgpImportedRoutesIpv4QueryParams }, params: FetchParams = {} ) => { - return this.request({ + return this.request({ path: `/v1/system/networking/bgp-routes-ipv4`, method: "GET", query, @@ -6329,7 +6606,7 @@ export class Api extends HttpClient { * Get BGP peer status */ networkingBgpStatus: (_: EmptyObj, params: FetchParams = {}) => { - return this.request({ + return this.request({ path: `/v1/system/networking/bgp-status`, method: "GET", ...params, @@ -6709,6 +6986,34 @@ export class Api extends HttpClient { ...params, }); }, + /** + * Run timeseries query + */ + timeseriesQuery: ( + { body }: { body: TimeseriesQuery }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/timeseries/query`, + method: "POST", + body, + ...params, + }); + }, + /** + * List timeseries schemas + */ + timeseriesSchemaList: ( + { query = {} }: { query?: TimeseriesSchemaListQueryParams }, + params: FetchParams = {} + ) => { + return this.request({ + path: `/v1/timeseries/schema`, + method: "GET", + query, + ...params, + }); + }, /** * List users */ diff --git a/client/msw-handlers.ts b/client/msw-handlers.ts index 2a5413c..51e2bd2 100644 --- a/client/msw-handlers.ts +++ b/client/msw-handlers.ts @@ -603,6 +603,12 @@ export interface MSWHandlers { req: Request; cookies: Record; }) => Promisable>; + /** `GET /v1/system/hardware/disks/:diskId` */ + physicalDiskView: (params: { + path: Api.PhysicalDiskViewPathParams; + req: Request; + cookies: Record; + }) => Promisable>; /** `GET /v1/system/hardware/racks` */ rackList: (params: { query: Api.RackListQueryParams; @@ -814,6 +820,12 @@ export interface MSWHandlers { req: Request; cookies: Record; }) => Promisable; + /** `GET /v1/system/ip-pools/:pool/utilization` */ + ipPoolUtilizationView: (params: { + path: Api.IpPoolUtilizationViewPathParams; + req: Request; + cookies: Record; + }) => Promisable>; /** `GET /v1/system/ip-pools-service` */ ipPoolServiceView: (params: { req: Request; @@ -885,7 +897,7 @@ export interface MSWHandlers { networkingBfdStatus: (params: { req: Request; cookies: Record; - }) => Promisable; + }) => Promisable>; /** `GET /v1/system/networking/bgp` */ networkingBgpConfigList: (params: { query: Api.NetworkingBgpConfigListQueryParams; @@ -909,7 +921,7 @@ export interface MSWHandlers { query: Api.NetworkingBgpAnnounceSetListQueryParams; req: Request; cookies: Record; - }) => Promisable; + }) => Promisable>; /** `POST /v1/system/networking/bgp-announce` */ networkingBgpAnnounceSetCreate: (params: { body: Json; @@ -922,17 +934,23 @@ export interface MSWHandlers { req: Request; cookies: Record; }) => Promisable; + /** `GET /v1/system/networking/bgp-message-history` */ + networkingBgpMessageHistory: (params: { + query: Api.NetworkingBgpMessageHistoryQueryParams; + req: Request; + cookies: Record; + }) => Promisable>; /** `GET /v1/system/networking/bgp-routes-ipv4` */ networkingBgpImportedRoutesIpv4: (params: { query: Api.NetworkingBgpImportedRoutesIpv4QueryParams; req: Request; cookies: Record; - }) => Promisable; + }) => Promisable>; /** `GET /v1/system/networking/bgp-status` */ networkingBgpStatus: (params: { req: Request; cookies: Record; - }) => Promisable; + }) => Promisable>; /** `GET /v1/system/networking/loopback-address` */ networkingLoopbackAddressList: (params: { query: Api.NetworkingLoopbackAddressListQueryParams; @@ -1098,6 +1116,18 @@ export interface MSWHandlers { req: Request; cookies: Record; }) => Promisable>; + /** `POST /v1/timeseries/query` */ + timeseriesQuery: (params: { + body: Json; + req: Request; + cookies: Record; + }) => Promisable>; + /** `GET /v1/timeseries/schema` */ + timeseriesSchemaList: (params: { + query: Api.TimeseriesSchemaListQueryParams; + req: Request; + cookies: Record; + }) => Promisable>; /** `GET /v1/users` */ userList: (params: { query: Api.UserListQueryParams; @@ -1770,6 +1800,10 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { "/v1/system/hardware/disks", handler(handlers["physicalDiskList"], schema.PhysicalDiskListParams, null) ), + http.get( + "/v1/system/hardware/disks/:diskId", + handler(handlers["physicalDiskView"], schema.PhysicalDiskViewParams, null) + ), http.get( "/v1/system/hardware/racks", handler(handlers["rackList"], schema.RackListParams, null) @@ -1966,6 +2000,14 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { "/v1/system/ip-pools/:pool/silos/:silo", handler(handlers["ipPoolSiloUnlink"], schema.IpPoolSiloUnlinkParams, null) ), + http.get( + "/v1/system/ip-pools/:pool/utilization", + handler( + handlers["ipPoolUtilizationView"], + schema.IpPoolUtilizationViewParams, + null + ) + ), http.get( "/v1/system/ip-pools-service", handler(handlers["ipPoolServiceView"], null, null) @@ -2082,6 +2124,14 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { null ) ), + http.get( + "/v1/system/networking/bgp-message-history", + handler( + handlers["networkingBgpMessageHistory"], + schema.NetworkingBgpMessageHistoryParams, + null + ) + ), http.get( "/v1/system/networking/bgp-routes-ipv4", handler( @@ -2246,6 +2296,18 @@ export function makeHandlers(handlers: MSWHandlers): HttpHandler[] { null ) ), + http.post( + "/v1/timeseries/query", + handler(handlers["timeseriesQuery"], null, schema.TimeseriesQuery) + ), + http.get( + "/v1/timeseries/schema", + handler( + handlers["timeseriesSchemaList"], + schema.TimeseriesSchemaListParams, + null + ) + ), http.get( "/v1/users", handler(handlers["userList"], schema.UserListParams, null) diff --git a/client/type-test.ts b/client/type-test.ts index 298b585..f1209b4 100644 --- a/client/type-test.ts +++ b/client/type-test.ts @@ -37,6 +37,15 @@ assert< assert< Equals> >(); +assert>>(); +assert>>(); +assert>>(); +assert< + Equals< + A.AggregateBgpMessageHistory, + z.infer + > +>(); assert>>(); assert>>(); assert>>(); @@ -56,7 +65,6 @@ assert>>(); assert< Equals> >(); -assert>>(); assert< Equals> >(); @@ -125,12 +133,18 @@ assert>>(); assert>>(); assert>>(); assert>>(); +assert>>(); +assert>>(); assert>>(); assert>>(); assert>>(); assert< Equals> >(); +assert>>(); +assert>>(); +assert>>(); +assert>>(); assert>>(); assert>>(); assert>>(); @@ -233,6 +247,9 @@ assert< >(); assert>>(); assert>>(); +assert>>(); +assert>>(); +assert>>(); assert>>(); assert>>(); assert< @@ -255,6 +272,7 @@ assert>>(); assert< Equals> >(); +assert>>(); assert< Equals> >(); @@ -262,12 +280,17 @@ assert>>(); assert>>(); assert>>(); assert>>(); +assert>>(); +assert>>(); assert>>(); assert< Equals> >(); assert>>(); assert>>(); +assert>>(); +assert>>(); +assert>>(); assert>>(); assert>>(); assert>>(); @@ -422,6 +445,17 @@ assert< Equals> >(); assert>>(); +assert>>(); +assert>>(); +assert>>(); +assert>>(); +assert>>(); +assert< + Equals< + A.TimeseriesSchemaResultsPage, + z.infer + > +>(); assert>>(); assert>>(); assert< diff --git a/client/validate.ts b/client/validate.ts index e832055..7f0abce 100644 --- a/client/validate.ts +++ b/client/validate.ts @@ -177,6 +177,38 @@ export const AddressLotResultsPage = z.preprocess( z.object({ items: AddressLot.array(), nextPage: z.string().optional() }) ); +export const BgpMessageHistory = z.preprocess( + processResponseBody, + z.record(z.unknown()) +); + +/** + * Identifies switch physical location + */ +export const SwitchLocation = z.preprocess( + processResponseBody, + z.enum(["switch0", "switch1"]) +); + +/** + * BGP message history for a particular switch. + */ +export const SwitchBgpHistory = z.preprocess( + processResponseBody, + z.object({ + history: z.record(z.string().min(1), BgpMessageHistory), + switch: SwitchLocation, + }) +); + +/** + * BGP message history for rack switches. + */ +export const AggregateBgpMessageHistory = z.preprocess( + processResponseBody, + z.object({ switchHistories: SwitchBgpHistory.array() }) +); + /** * Properties that uniquely identify an Oxide hardware component */ @@ -185,6 +217,9 @@ export const Baseboard = z.preprocess( z.object({ part: z.string(), revision: z.number(), serial: z.string() }) ); +/** + * BFD connection mode. + */ export const BfdMode = z.preprocess( processResponseBody, z.enum(["single_hop", "multi_hop"]) @@ -315,14 +350,6 @@ export const BgpConfigResultsPage = z.preprocess( z.object({ items: BgpConfig.array(), nextPage: z.string().optional() }) ); -/** - * Identifies switch physical location - */ -export const SwitchLocation = z.preprocess( - processResponseBody, - z.enum(["switch0", "switch1"]) -); - /** * A route imported from a BGP peer. */ @@ -1131,6 +1158,22 @@ export const DiskResultsPage = z.preprocess( z.object({ items: Disk.array(), nextPage: z.string().optional() }) ); +/** + * A distribution is a sequence of bins and counts in those bins. + */ +export const Distributiondouble = z.preprocess( + processResponseBody, + z.object({ bins: z.number().array(), counts: z.number().min(0).array() }) +); + +/** + * A distribution is a sequence of bins and counts in those bins. + */ +export const Distributionint64 = z.preprocess( + processResponseBody, + z.object({ bins: z.number().array(), counts: z.number().min(0).array() }) +); + /** * Parameters for creating an ephemeral IP address for an instance. */ @@ -1188,6 +1231,73 @@ export const ExternalIpResultsPage = z.preprocess( z.object({ items: ExternalIp.array(), nextPage: z.string().optional() }) ); +/** + * The `FieldType` identifies the data type of a target or metric field. + */ +export const FieldType = z.preprocess( + processResponseBody, + z.enum([ + "string", + "i8", + "u8", + "i16", + "u16", + "i32", + "u32", + "i64", + "u64", + "ip_addr", + "uuid", + "bool", + ]) +); + +/** + * The source from which a field is derived, the target or metric. + */ +export const FieldSource = z.preprocess( + processResponseBody, + z.enum(["target", "metric"]) +); + +/** + * The name and type information for a field of a timeseries schema. + */ +export const FieldSchema = z.preprocess( + processResponseBody, + z.object({ fieldType: FieldType, name: z.string(), source: FieldSource }) +); + +/** + * The `FieldValue` contains the value of a target or metric field. + */ +export const FieldValue = z.preprocess( + processResponseBody, + z.union([ + z.object({ type: z.enum(["string"]), value: z.string() }), + z.object({ type: z.enum(["i8"]), value: z.number().min(-127).max(127) }), + z.object({ type: z.enum(["u8"]), value: z.number().min(0).max(255) }), + z.object({ + type: z.enum(["i16"]), + value: z.number().min(-32767).max(32767), + }), + z.object({ type: z.enum(["u16"]), value: z.number().min(0).max(65535) }), + z.object({ + type: z.enum(["i32"]), + value: z.number().min(-2147483647).max(2147483647), + }), + z.object({ + type: z.enum(["u32"]), + value: z.number().min(0).max(4294967295), + }), + z.object({ type: z.enum(["i64"]), value: z.number() }), + z.object({ type: z.enum(["u64"]), value: z.number().min(0) }), + z.object({ type: z.enum(["ip_addr"]), value: z.string().ip() }), + z.object({ type: z.enum(["uuid"]), value: z.string().uuid() }), + z.object({ type: z.enum(["bool"]), value: SafeBoolean }), + ]) +); + /** * Parameters for finalizing a disk */ @@ -1755,6 +1865,24 @@ export const IpPoolUpdate = z.preprocess( z.object({ description: z.string().optional(), name: Name.optional() }) ); +export const Ipv4Utilization = z.preprocess( + processResponseBody, + z.object({ + allocated: z.number().min(0).max(4294967295), + capacity: z.number().min(0).max(4294967295), + }) +); + +export const Ipv6Utilization = z.preprocess( + processResponseBody, + z.object({ allocated: z.string(), capacity: z.string() }) +); + +export const IpPoolUtilization = z.preprocess( + processResponseBody, + z.object({ ipv4: Ipv4Utilization, ipv6: Ipv6Utilization }) +); + /** * A range of IP ports * @@ -1882,6 +2010,14 @@ export const MeasurementResultsPage = z.preprocess( z.object({ items: Measurement.array(), nextPage: z.string().optional() }) ); +/** + * The type of the metric itself, indicating what its values represent. + */ +export const MetricType = z.preprocess( + processResponseBody, + z.enum(["gauge", "delta", "cumulative"]) +); + /** * The type of network interface */ @@ -1935,6 +2071,25 @@ export const PhysicalDiskKind = z.preprocess( z.enum(["m2", "u2"]) ); +/** + * The operator-defined policy of a physical disk. + */ +export const PhysicalDiskPolicy = z.preprocess( + processResponseBody, + z.union([ + z.object({ kind: z.enum(["in_service"]) }), + z.object({ kind: z.enum(["expunged"]) }), + ]) +); + +/** + * The current state of the disk, as determined by Nexus. + */ +export const PhysicalDiskState = z.preprocess( + processResponseBody, + z.enum(["active", "decommissioned"]) +); + /** * View of a Physical Disk * @@ -1946,8 +2101,10 @@ export const PhysicalDisk = z.preprocess( formFactor: PhysicalDiskKind, id: z.string().uuid(), model: z.string(), + policy: PhysicalDiskPolicy, serial: z.string(), sledId: z.string().uuid().optional(), + state: PhysicalDiskState, timeCreated: z.coerce.date(), timeModified: z.coerce.date(), vendor: z.string(), @@ -1969,6 +2126,49 @@ export const Ping = z.preprocess( z.object({ status: PingStatus }) ); +/** + * List of data values for one timeseries. + * + * Each element is an option, where `None` represents a missing sample. + */ +export const ValueArray = z.preprocess( + processResponseBody, + z.union([ + z.object({ type: z.enum(["integer"]), values: z.number().array() }), + z.object({ type: z.enum(["double"]), values: z.number().array() }), + z.object({ type: z.enum(["boolean"]), values: SafeBoolean.array() }), + z.object({ type: z.enum(["string"]), values: z.string().array() }), + z.object({ + type: z.enum(["integer_distribution"]), + values: Distributionint64.array(), + }), + z.object({ + type: z.enum(["double_distribution"]), + values: Distributiondouble.array(), + }), + ]) +); + +/** + * A single list of values, for one dimension of a timeseries. + */ +export const Values = z.preprocess( + processResponseBody, + z.object({ metricType: MetricType, values: ValueArray }) +); + +/** + * Timepoints and values for one timeseries. + */ +export const Points = z.preprocess( + processResponseBody, + z.object({ + startTimes: z.coerce.date().array().optional(), + timestamps: z.coerce.date().array(), + values: Values.array(), + }) +); + /** * Identity-related metadata that's included in nearly all public API objects */ @@ -2817,6 +3017,74 @@ export const SwitchResultsPage = z.preprocess( z.object({ items: Switch.array(), nextPage: z.string().optional() }) ); +/** + * A timeseries contains a timestamped set of values from one source. + * + * This includes the typed key-value pairs that uniquely identify it, and the set of timestamps and data values from it. + */ +export const Timeseries = z.preprocess( + processResponseBody, + z.object({ fields: z.record(z.string().min(1), FieldValue), points: Points }) +); + +/** + * A table represents one or more timeseries with the same schema. + * + * A table is the result of an OxQL query. It contains a name, usually the name of the timeseries schema from which the data is derived, and any number of timeseries, which contain the actual data. + */ +export const Table = z.preprocess( + processResponseBody, + z.object({ + name: z.string(), + timeseries: z.record(z.string().min(1), Timeseries), + }) +); + +/** + * The name of a timeseries + * + * Names are constructed by concatenating the target and metric names with ':'. Target and metric names must be lowercase alphanumeric characters with '_' separating words. + */ +export const TimeseriesName = z.preprocess( + processResponseBody, + z + .string() + .regex( + /^(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*):(([a-z]+[a-z0-9]*)(_([a-z0-9]+))*)$/ + ) +); + +/** + * A timeseries query string, written in the Oximeter query language. + */ +export const TimeseriesQuery = z.preprocess( + processResponseBody, + z.object({ query: z.string() }) +); + +/** + * The schema for a timeseries. + * + * This includes the name of the timeseries, as well as the datum type of its metric and the schema for each field. + */ +export const TimeseriesSchema = z.preprocess( + processResponseBody, + z.object({ + created: z.coerce.date(), + datumType: DatumType, + fieldSchema: FieldSchema.array().refine(...uniqueItems), + timeseriesName: TimeseriesName, + }) +); + +/** + * A single page of results + */ +export const TimeseriesSchemaResultsPage = z.preprocess( + processResponseBody, + z.object({ items: TimeseriesSchema.array(), nextPage: z.string().optional() }) +); + /** * A sled that has not been added to an initialized rack yet */ @@ -4168,6 +4436,16 @@ export const PhysicalDiskListParams = z.preprocess( }) ); +export const PhysicalDiskViewParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + diskId: z.string().uuid(), + }), + query: z.object({}), + }) +); + export const RackListParams = z.preprocess( processResponseBody, z.object({ @@ -4528,6 +4806,16 @@ export const IpPoolSiloUnlinkParams = z.preprocess( }) ); +export const IpPoolUtilizationViewParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({ + pool: NameOrId, + }), + query: z.object({}), + }) +); + export const IpPoolServiceViewParams = z.preprocess( processResponseBody, z.object({ @@ -4707,6 +4995,16 @@ export const NetworkingBgpAnnounceSetDeleteParams = z.preprocess( }) ); +export const NetworkingBgpMessageHistoryParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + asn: z.number().min(0).max(4294967295), + }), + }) +); + export const NetworkingBgpImportedRoutesIpv4Params = z.preprocess( processResponseBody, z.object({ @@ -5011,6 +5309,25 @@ export const SiloUtilizationViewParams = z.preprocess( }) ); +export const TimeseriesQueryParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({}), + }) +); + +export const TimeseriesSchemaListParams = z.preprocess( + processResponseBody, + z.object({ + path: z.object({}), + query: z.object({ + limit: z.number().min(1).max(4294967295).optional(), + pageToken: z.string().optional(), + }), + }) +); + export const UserListParams = z.preprocess( processResponseBody, z.object({ diff --git a/generator/client/api.ts b/generator/client/api.ts index c4a16e9..738ea8f 100644 --- a/generator/client/api.ts +++ b/generator/client/api.ts @@ -23,7 +23,6 @@ import { } from "../util"; import { initIO } from "../io"; import type { Schema } from "../schema/base"; -import { refToSchemaName } from "../schema/base"; import { contentRef, docComment, @@ -226,8 +225,7 @@ export function generateApi(spec: OpenAPIV3.Document, destDir: string) { (p) => "in" in p && p.in === "query" ) as OpenAPIV3.ParameterObject[]; - const bodyTypeRef = contentRef(conf.requestBody); - const bodyType = bodyTypeRef ? refToSchemaName(bodyTypeRef) : null; + const bodyType = contentRef(conf.requestBody); const successResponse = conf.responses["200"] || @@ -235,10 +233,7 @@ export function generateApi(spec: OpenAPIV3.Document, destDir: string) { conf.responses["202"] || conf.responses["204"]; - const successTypeRef = contentRef(successResponse); - const successType = successTypeRef - ? refToSchemaName(successTypeRef) - : "void"; + const successType = contentRef(successResponse); docComment(conf.summary || conf.description, schemaNames, io); @@ -270,7 +265,7 @@ export function generateApi(spec: OpenAPIV3.Document, destDir: string) { } w(`params: FetchParams = {}) => { - return this.request<${successType}>({ + return this.request<${successType || "void"}>({ path: ${pathToTemplateStr(path)}, method: "${method.toUpperCase()}",`); if (bodyType) { diff --git a/generator/client/base.ts b/generator/client/base.ts index 8557d6b..c24e42e 100644 --- a/generator/client/base.ts +++ b/generator/client/base.ts @@ -9,7 +9,7 @@ import type { OpenAPIV3 } from "openapi-types"; import { topologicalSort } from "../util"; import type { IO } from "../io"; -import type { Schema } from "../schema/base"; +import { refToSchemaName, type Schema } from "../schema/base"; import { OpenAPIV3 as O } from "openapi-types"; const HttpMethods = O.HttpMethods; @@ -36,14 +36,26 @@ export const jsdocLinkify = (s: string, schemaNames: string[]) => ); export function contentRef( - o: Schema | OpenAPIV3.RequestBodyObject | undefined + o: Schema | OpenAPIV3.RequestBodyObject | undefined, + prefix = "" ) { - return o && - "content" in o && - o.content?.["application/json"]?.schema && - "$ref" in o.content["application/json"].schema - ? o.content["application/json"].schema.$ref - : null; + if (!(o && "content" in o && o.content?.["application/json"]?.schema)) { + return null; + } + const schema = o.content["application/json"].schema; + + if ("$ref" in schema) { + return prefix + refToSchemaName(schema.$ref); + } + + if (schema.type === "array") { + if ("$ref" in schema.items) { + return prefix + refToSchemaName(schema.items.$ref) + "[]"; + } + return null; + } + + return null; } /** diff --git a/generator/client/msw-handlers.ts b/generator/client/msw-handlers.ts index 32dabbf..356be5b 100644 --- a/generator/client/msw-handlers.ts +++ b/generator/client/msw-handlers.ts @@ -8,7 +8,6 @@ import type { OpenAPIV3 } from "openapi-types"; import { initIO } from "../io"; -import { refToSchemaName } from "../schema/base"; import { snakeToCamel, snakeToPascal } from "../util"; import { contentRef, iterPathConfig } from "./base"; import path from "node:path"; @@ -83,13 +82,9 @@ export function generateMSWHandlers(spec: OpenAPIV3.Document, destDir: string) { conf.responses["202"] || conf.responses["204"]; - const successTypeRef = contentRef(successResponse); - const successType = successTypeRef - ? "Api." + refToSchemaName(successTypeRef) - : "void"; + const successType = contentRef(successResponse, "Api."); - const bodyTypeRef = contentRef(conf.requestBody); - const bodyType = bodyTypeRef ? refToSchemaName(bodyTypeRef) : null; + const bodyType = contentRef(conf.requestBody); const body = bodyType && (method === "post" || method === "put") ? `body: Json,` @@ -108,10 +103,9 @@ export function generateMSWHandlers(spec: OpenAPIV3.Document, destDir: string) { : ""; const params = `params: { ${pathParamsType} ${queryParamsType} ${body} req: Request, cookies: Record }`; - const resultType = - successType === "void" - ? "Promisable" - : `Promisable>`; + const resultType = successType + ? `Promisable>` + : "Promisable"; w(`/** \`${method.toUpperCase()} ${formatPath(path)}\` */`); w(` ${opName}: (${params}) => ${resultType},`); @@ -210,10 +204,10 @@ export function generateMSWHandlers(spec: OpenAPIV3.Document, destDir: string) { return [`); for (const { path, method, opId, conf } of iterPathConfig(spec.paths)) { const handler = snakeToCamel(opId); - const bodyTypeRef = contentRef(conf.requestBody); + const bodyType = contentRef(conf.requestBody, "schema."); const bodySchema = - bodyTypeRef && (method === "post" || method === "put") - ? `schema.${refToSchemaName(bodyTypeRef)}` + bodyType !== "void" && (method === "post" || method === "put") + ? bodyType : "null"; const paramSchema = conf.parameters?.length ? `schema.${snakeToPascal(opId)}Params` diff --git a/tests/__snapshots__/known-properties.test.ts.snap b/tests/__snapshots__/known-properties.test.ts.snap index 40a799a..411bb61 100644 --- a/tests/__snapshots__/known-properties.test.ts.snap +++ b/tests/__snapshots__/known-properties.test.ts.snap @@ -17,6 +17,7 @@ exports[`Known properties > Type array should contain only known properties 1`] "items", "nullable", "type", + "uniqueItems", ] `; @@ -24,6 +25,7 @@ exports[`Known properties > Type boolean should contain only known properties 1` [ "default", "description", + "nullable", "type", ] `; @@ -43,6 +45,7 @@ exports[`Known properties > Type integer should contain only known properties 1` exports[`Known properties > Type number should contain only known properties 1`] = ` [ "format", + "nullable", "type", ] `; diff --git a/tests/known-properties.test.ts b/tests/known-properties.test.ts index e540a06..a48684b 100644 --- a/tests/known-properties.test.ts +++ b/tests/known-properties.test.ts @@ -20,6 +20,8 @@ describe("Known properties", async () => { const types = await getSchemaTypes(); const typeNames = Object.keys(types) as Array; + // TODO: we need a way to say what schema is causing the failure! + // Just need to collect everything in a different structure, I guess test.each(typeNames)( "Type %s should contain only known properties", (typeName) => {