@astrogate/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admission.d.ts +35 -0
- package/dist/admission.js +112 -0
- package/dist/http.d.ts +4 -1
- package/dist/http.js +78 -20
- package/dist/inventory.js +2 -0
- package/dist/management.d.ts +25 -0
- package/dist/management.js +3 -0
- package/dist/operator-resources.d.ts +8 -0
- package/dist/operator-resources.js +73 -0
- package/dist/operator.d.ts +51 -0
- package/dist/operator.js +23 -2
- package/dist/wire.gen.d.ts +851 -43
- package/package.json +1 -1
- package/release.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ManagementOptions } from "./management.js";
|
|
2
|
+
import type { components } from "./wire.gen.js";
|
|
3
|
+
type Schemas = components["schemas"];
|
|
4
|
+
export type AdmissionPolicy = Schemas["AdmissionPolicy"];
|
|
5
|
+
export type AdmissionState = Schemas["AdmissionState"];
|
|
6
|
+
export type AdmissionUsagePage = Schemas["AdmissionUsagePage"];
|
|
7
|
+
export interface UsageQuery {
|
|
8
|
+
from: string;
|
|
9
|
+
until: string;
|
|
10
|
+
limit: number;
|
|
11
|
+
project?: string;
|
|
12
|
+
environment?: string;
|
|
13
|
+
workload?: "application" | "database" | "identity" | "storage" | "jobs" | "builds" | "realtime";
|
|
14
|
+
subject?: string;
|
|
15
|
+
after?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function admissionState(value: unknown): value is AdmissionState;
|
|
18
|
+
export declare function admissionUsage(value: unknown): value is AdmissionUsagePage;
|
|
19
|
+
export declare function admissionClient(options: ManagementOptions, endpoint: URL, base: string): {
|
|
20
|
+
get: (signal?: AbortSignal) => Promise<{
|
|
21
|
+
policy: components["schemas"]["AdmissionPolicy"];
|
|
22
|
+
occupancy: components["schemas"]["AdmissionOccupancy"][];
|
|
23
|
+
storageCredits: components["schemas"]["TenantStorageCredit"][];
|
|
24
|
+
}>;
|
|
25
|
+
assign: (input: Schemas["AssignAdmissionPolicy"], signal?: AbortSignal) => Promise<{
|
|
26
|
+
revision: number;
|
|
27
|
+
profile: components["schemas"]["AdmissionProfile"];
|
|
28
|
+
assignedAt: string;
|
|
29
|
+
}>;
|
|
30
|
+
usage: (query: UsageQuery, signal?: AbortSignal) => Promise<{
|
|
31
|
+
points: components["schemas"]["AdmissionUsagePoint"][];
|
|
32
|
+
next?: string;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AstrogateError, object, requestJSON } from "./http.js";
|
|
2
|
+
function count(value) {
|
|
3
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
4
|
+
}
|
|
5
|
+
function date(value) {
|
|
6
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
7
|
+
}
|
|
8
|
+
function positive(value) {
|
|
9
|
+
return count(value) && value > 0;
|
|
10
|
+
}
|
|
11
|
+
function rule(value) {
|
|
12
|
+
return (object(value) &&
|
|
13
|
+
typeof value.requestsPerSecond === "number" &&
|
|
14
|
+
Number.isFinite(value.requestsPerSecond) &&
|
|
15
|
+
value.requestsPerSecond > 0 &&
|
|
16
|
+
["burst", "concurrent", "executionMs"].every((key) => positive(value[key])) &&
|
|
17
|
+
["pending", "pendingBytes", "waitMs"].every((key) => count(value[key])));
|
|
18
|
+
}
|
|
19
|
+
function validRetention(value) {
|
|
20
|
+
return (object(value) &&
|
|
21
|
+
positive(value.rawLogsSeconds) &&
|
|
22
|
+
positive(value.usageSeconds) &&
|
|
23
|
+
count(value.subjectSeconds) &&
|
|
24
|
+
value.subjectSeconds <= value.usageSeconds);
|
|
25
|
+
}
|
|
26
|
+
function policy(value) {
|
|
27
|
+
if (!object(value) ||
|
|
28
|
+
!positive(value.revision) ||
|
|
29
|
+
!date(value.assignedAt) ||
|
|
30
|
+
!object(value.profile))
|
|
31
|
+
return false;
|
|
32
|
+
const p = value.profile;
|
|
33
|
+
return (typeof p.id === "string" &&
|
|
34
|
+
p.id.length > 0 &&
|
|
35
|
+
typeof p.evidence === "string" &&
|
|
36
|
+
p.evidence.length > 0 &&
|
|
37
|
+
positive(p.durableBytes) &&
|
|
38
|
+
validRetention(p.retention) &&
|
|
39
|
+
object(p.rules) &&
|
|
40
|
+
object(p.rules.all) &&
|
|
41
|
+
Object.entries(p.rules).every(([kind, value]) => [
|
|
42
|
+
"all",
|
|
43
|
+
"application",
|
|
44
|
+
"database",
|
|
45
|
+
"identity",
|
|
46
|
+
"storage",
|
|
47
|
+
"jobs",
|
|
48
|
+
"builds",
|
|
49
|
+
"realtime",
|
|
50
|
+
].includes(kind) &&
|
|
51
|
+
object(value) &&
|
|
52
|
+
rule(value.tenant) &&
|
|
53
|
+
rule(value.application)));
|
|
54
|
+
}
|
|
55
|
+
export function admissionState(value) {
|
|
56
|
+
return (object(value) &&
|
|
57
|
+
policy(value.policy) &&
|
|
58
|
+
Array.isArray(value.occupancy) &&
|
|
59
|
+
value.occupancy.every((o) => object(o) &&
|
|
60
|
+
typeof o.projectId === "string" &&
|
|
61
|
+
typeof o.workload === "string" &&
|
|
62
|
+
["running", "unacknowledged", "pending", "pendingBytes"].every((key) => count(o[key])) &&
|
|
63
|
+
(o.oldest === undefined || date(o.oldest))) &&
|
|
64
|
+
Array.isArray(value.storageCredits) &&
|
|
65
|
+
value.storageCredits.every((c) => object(c) &&
|
|
66
|
+
["projectId", "environmentId", "block", "owner"].every((key) => typeof c[key] === "string") &&
|
|
67
|
+
count(c.bytes) &&
|
|
68
|
+
count(c.epoch)));
|
|
69
|
+
}
|
|
70
|
+
export function admissionUsage(value) {
|
|
71
|
+
return (object(value) &&
|
|
72
|
+
(value.next === undefined || date(value.next)) &&
|
|
73
|
+
Array.isArray(value.points) &&
|
|
74
|
+
value.points.every((p) => object(p) &&
|
|
75
|
+
date(p.hour) &&
|
|
76
|
+
typeof p.workload === "string" &&
|
|
77
|
+
typeof p.outcome === "string" &&
|
|
78
|
+
["requests", "bytes", "elapsedMs"].every((key) => count(p[key]))));
|
|
79
|
+
}
|
|
80
|
+
// This namespace uses the same authority and transport as the management client.
|
|
81
|
+
// A profile reference is the only writable entitlement, never arbitrary limits.
|
|
82
|
+
export function admissionClient(options, endpoint, base) {
|
|
83
|
+
const headers = { authorization: `Bearer ${options.token}` };
|
|
84
|
+
return {
|
|
85
|
+
get: (signal) => requestJSON(new URL(base + "/admission", endpoint), options, { headers }, admissionState, signal),
|
|
86
|
+
assign: (input, signal) => {
|
|
87
|
+
if (!input.profileId || !count(input.expectedRevision))
|
|
88
|
+
throw new AstrogateError("invalid_policy_assignment", 0, "", false);
|
|
89
|
+
// A lost response to CAS needs a read/reconciliation, not a blind replay.
|
|
90
|
+
return requestJSON(new URL(base + "/admission", endpoint), { ...options, maxAttempts: 1 }, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
93
|
+
body: JSON.stringify(input),
|
|
94
|
+
}, policy, signal);
|
|
95
|
+
},
|
|
96
|
+
usage: (query, signal) => {
|
|
97
|
+
if (!date(query.from) ||
|
|
98
|
+
!date(query.until) ||
|
|
99
|
+
Date.parse(query.from) >= Date.parse(query.until) ||
|
|
100
|
+
!count(query.limit) ||
|
|
101
|
+
query.limit === 0 ||
|
|
102
|
+
(query.environment && !query.project) ||
|
|
103
|
+
(query.after && !date(query.after)))
|
|
104
|
+
throw new AstrogateError("invalid_usage_query", 0, "", false);
|
|
105
|
+
const url = new URL(base + "/usage", endpoint);
|
|
106
|
+
for (const [key, value] of Object.entries(query))
|
|
107
|
+
if (value !== undefined)
|
|
108
|
+
url.searchParams.set(key, String(value));
|
|
109
|
+
return requestJSON(url, options, { headers }, admissionUsage, signal);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { components } from "./wire.gen.js";
|
|
2
|
+
export type FailureDetails = Pick<components["schemas"]["Error"], "message" | "scope" | "dimension" | "reason">;
|
|
1
3
|
export declare class AstrogateError extends Error {
|
|
2
4
|
readonly code: string;
|
|
3
5
|
readonly status: number;
|
|
4
6
|
readonly requestId: string;
|
|
5
7
|
readonly retryable: boolean;
|
|
6
|
-
|
|
8
|
+
readonly details?: Partial<FailureDetails> | undefined;
|
|
9
|
+
constructor(code: string, status: number, requestId: string, retryable: boolean, details?: Partial<FailureDetails> | undefined);
|
|
7
10
|
}
|
|
8
11
|
export interface TransportBudget {
|
|
9
12
|
deadlineMs: number;
|
package/dist/http.js
CHANGED
|
@@ -1,42 +1,85 @@
|
|
|
1
|
-
// Shared owned HTTP behavior. Native database/auth clients retain their own
|
|
2
|
-
// return values and transport semantics.
|
|
3
1
|
export class AstrogateError extends Error {
|
|
4
2
|
code;
|
|
5
3
|
status;
|
|
6
4
|
requestId;
|
|
7
5
|
retryable;
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
details;
|
|
7
|
+
constructor(code, status, requestId, retryable, details) {
|
|
8
|
+
super(details?.message ?? code);
|
|
10
9
|
this.code = code;
|
|
11
10
|
this.status = status;
|
|
12
11
|
this.requestId = requestId;
|
|
13
12
|
this.retryable = retryable;
|
|
13
|
+
this.details = details;
|
|
14
14
|
this.name = "AstrogateError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
export function object(value) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
export function object(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
export function identifier(value) {
|
|
21
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
22
|
+
throw new AstrogateError("invalid_resource_reference", 0, "", false);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function validateBudget(options) {
|
|
26
|
+
if (!Number.isSafeInteger(options.deadlineMs) ||
|
|
27
|
+
options.deadlineMs <= 0 ||
|
|
28
|
+
options.deadlineMs > timerMaximumMs ||
|
|
29
|
+
!Number.isSafeInteger(options.maxAttempts) ||
|
|
30
|
+
options.maxAttempts < 1)
|
|
31
|
+
throw new AstrogateError("explicit_transport_budget_required", 0, "", false);
|
|
32
|
+
}
|
|
33
|
+
// Native browser/Node timers use signed 32-bit millisecond delays. Durable
|
|
34
|
+
// operations outlive transport calls and are polled instead of held for days.
|
|
35
|
+
const timerMaximumMs = 2 ** 31 - 1;
|
|
36
|
+
function optionalText(value, key) {
|
|
37
|
+
const field = value[key];
|
|
38
|
+
if (field !== undefined && typeof field !== "string")
|
|
39
|
+
throw new AstrogateError("invalid_error_response", 0, "", false);
|
|
40
|
+
return field;
|
|
41
|
+
}
|
|
42
|
+
function delay(ms, signal) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
if (signal.aborted) {
|
|
45
|
+
reject(signal.reason);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const abort = () => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
reject(signal.reason);
|
|
51
|
+
};
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
signal.removeEventListener("abort", abort);
|
|
54
|
+
resolve();
|
|
55
|
+
}, ms);
|
|
56
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
26
59
|
export async function requestResponse(url, options, input, signal) {
|
|
27
60
|
validateBudget(options);
|
|
28
|
-
const combined = signal
|
|
61
|
+
const combined = signal
|
|
62
|
+
? AbortSignal.any([AbortSignal.timeout(options.deadlineMs), signal])
|
|
63
|
+
: AbortSignal.timeout(options.deadlineMs);
|
|
29
64
|
let last;
|
|
30
65
|
for (let attempt = 0; attempt < options.maxAttempts; attempt++) {
|
|
31
66
|
let retryAfter = 0;
|
|
32
67
|
try {
|
|
33
|
-
const response = await fetch(url, {
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
...input,
|
|
70
|
+
signal: combined,
|
|
71
|
+
redirect: "error",
|
|
72
|
+
cache: "no-store",
|
|
73
|
+
});
|
|
34
74
|
if (response.ok)
|
|
35
75
|
return response;
|
|
36
76
|
const after = response.headers.get("retry-after");
|
|
37
77
|
if (after) {
|
|
38
78
|
const seconds = Number(after);
|
|
39
|
-
|
|
79
|
+
const parsed = Number.isFinite(seconds)
|
|
80
|
+
? seconds * 1000
|
|
81
|
+
: Date.parse(after) - Date.now();
|
|
82
|
+
retryAfter = Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
|
|
40
83
|
}
|
|
41
84
|
let value;
|
|
42
85
|
try {
|
|
@@ -45,9 +88,17 @@ export async function requestResponse(url, options, input, signal) {
|
|
|
45
88
|
catch {
|
|
46
89
|
throw new AstrogateError("invalid_error_response", response.status, "", false);
|
|
47
90
|
}
|
|
48
|
-
if (!object(value) ||
|
|
91
|
+
if (!object(value) ||
|
|
92
|
+
typeof value.code !== "string" ||
|
|
93
|
+
typeof value.requestId !== "string" ||
|
|
94
|
+
typeof value.retryable !== "boolean")
|
|
49
95
|
throw new AstrogateError("invalid_error_response", response.status, "", false);
|
|
50
|
-
throw new AstrogateError(value.code, response.status, value.requestId, value.retryable
|
|
96
|
+
throw new AstrogateError(value.code, response.status, value.requestId, value.retryable, {
|
|
97
|
+
message: optionalText(value, "message"),
|
|
98
|
+
scope: optionalText(value, "scope"),
|
|
99
|
+
dimension: optionalText(value, "dimension"),
|
|
100
|
+
reason: optionalText(value, "reason"),
|
|
101
|
+
});
|
|
51
102
|
}
|
|
52
103
|
catch (error) {
|
|
53
104
|
if (combined.aborted)
|
|
@@ -57,7 +108,7 @@ export async function requestResponse(url, options, input, signal) {
|
|
|
57
108
|
throw error;
|
|
58
109
|
if (attempt + 1 === options.maxAttempts)
|
|
59
110
|
break;
|
|
60
|
-
await delay(Math.max(retryAfter, 100 * 2 ** attempt * (0.5 + Math.random())), combined);
|
|
111
|
+
await delay(Math.min(options.deadlineMs, Math.max(retryAfter, 100 * 2 ** attempt * (0.5 + Math.random()))), combined);
|
|
61
112
|
}
|
|
62
113
|
}
|
|
63
114
|
if (last instanceof AstrogateError)
|
|
@@ -83,7 +134,14 @@ export function managementAuthority(options) {
|
|
|
83
134
|
if (typeof window !== "undefined")
|
|
84
135
|
throw new AstrogateError("server_only_entrypoint", 0, "", false);
|
|
85
136
|
const endpoint = new URL(options.endpoint);
|
|
86
|
-
if ((endpoint.protocol !== "https:" &&
|
|
137
|
+
if ((endpoint.protocol !== "https:" &&
|
|
138
|
+
!(endpoint.protocol === "http:" &&
|
|
139
|
+
["127.0.0.1", "[::1]", "localhost"].includes(endpoint.hostname))) ||
|
|
140
|
+
endpoint.username ||
|
|
141
|
+
endpoint.password ||
|
|
142
|
+
endpoint.search ||
|
|
143
|
+
endpoint.hash ||
|
|
144
|
+
endpoint.pathname !== "/")
|
|
87
145
|
throw new AstrogateError("unsafe_management_authority", 0, "", false);
|
|
88
146
|
validateBudget(options);
|
|
89
147
|
if (!options.token)
|
package/dist/inventory.js
CHANGED
|
@@ -6,6 +6,8 @@ export function listQuery(options) {
|
|
|
6
6
|
throw new AstrogateError("invalid_page_limit", 0, "", false);
|
|
7
7
|
query.set("limit", String(options.limit));
|
|
8
8
|
}
|
|
9
|
+
if (options.search !== undefined)
|
|
10
|
+
query.set("search", options.search);
|
|
9
11
|
if (options.cursor !== undefined)
|
|
10
12
|
query.set("cursor", options.cursor);
|
|
11
13
|
return query.size ? `?${query}` : "";
|
package/dist/management.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PageOptions } from "./inventory.js";
|
|
2
|
+
export type { AdmissionPolicy, AdmissionState, AdmissionUsagePage, UsageQuery } from "./admission.js";
|
|
2
3
|
export { AstrogateError } from "./http.js";
|
|
3
4
|
import type { components } from "./wire.gen.js";
|
|
4
5
|
type Schemas = components["schemas"];
|
|
@@ -47,6 +48,25 @@ export interface ManagementOptions {
|
|
|
47
48
|
maxAttempts: number;
|
|
48
49
|
}
|
|
49
50
|
export declare function createManagement(options: ManagementOptions): {
|
|
51
|
+
admission: {
|
|
52
|
+
get: (signal?: AbortSignal) => Promise<{
|
|
53
|
+
policy: components["schemas"]["AdmissionPolicy"];
|
|
54
|
+
occupancy: components["schemas"]["AdmissionOccupancy"][];
|
|
55
|
+
storageCredits: components["schemas"]["TenantStorageCredit"][];
|
|
56
|
+
}>;
|
|
57
|
+
assign: (input: {
|
|
58
|
+
profileId: string;
|
|
59
|
+
expectedRevision: number;
|
|
60
|
+
}, signal?: AbortSignal) => Promise<{
|
|
61
|
+
revision: number;
|
|
62
|
+
profile: components["schemas"]["AdmissionProfile"];
|
|
63
|
+
assignedAt: string;
|
|
64
|
+
}>;
|
|
65
|
+
usage: (query: import("./admission.js").UsageQuery, signal?: AbortSignal) => Promise<{
|
|
66
|
+
points: components["schemas"]["AdmissionUsagePoint"][];
|
|
67
|
+
next?: string;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
50
70
|
projects: {
|
|
51
71
|
create: (input: CreateProject, controls: RequestOptions) => Promise<{
|
|
52
72
|
operationId: string;
|
|
@@ -272,6 +292,11 @@ export declare function createManagement(options: ManagementOptions): {
|
|
|
272
292
|
}>;
|
|
273
293
|
};
|
|
274
294
|
environments: {
|
|
295
|
+
create: (project: string, input: Schemas["CreateEnvironment"], controls: RequestOptions) => Promise<{
|
|
296
|
+
operationId: string;
|
|
297
|
+
resourceId: string;
|
|
298
|
+
statusUrl: string;
|
|
299
|
+
}>;
|
|
275
300
|
list: (project: string, page?: PageOptions, signal?: AbortSignal) => Promise<import("./inventory.js").InventoryPage<{
|
|
276
301
|
id: string;
|
|
277
302
|
projectId: string;
|
package/dist/management.js
CHANGED
|
@@ -3,6 +3,7 @@ import { project, environment, placementState, placementLocations, operation, op
|
|
|
3
3
|
import { listQuery, pageOf } from "./inventory.js";
|
|
4
4
|
import { managementAuthority } from "./http.js";
|
|
5
5
|
import { serviceBindings } from "./services.js";
|
|
6
|
+
import { admissionClient } from "./admission.js";
|
|
6
7
|
export { AstrogateError } from "./http.js";
|
|
7
8
|
function object(value) {
|
|
8
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -66,6 +67,7 @@ export function createManagement(options) {
|
|
|
66
67
|
return requestJSON(new URL(base + path, endpoint), options, { method: method ?? (body === undefined ? "GET" : "POST"), headers, body }, validate, controls?.signal ?? signal);
|
|
67
68
|
}
|
|
68
69
|
return {
|
|
70
|
+
admission: admissionClient(options, endpoint, base),
|
|
69
71
|
projects: { create: (input, controls) => request(`/projects`, accepted, input, controls), list: (page = {}, signal) => request(`/projects${listQuery(page)}`, pageOf(project), undefined, undefined, signal), get: (resource, signal) => request(`/projects/${id(resource)}`, project, undefined, undefined, signal) },
|
|
70
72
|
repositories: {
|
|
71
73
|
create: (project, input, controls) => request(`/projects/${id(project)}/repository`, accepted, input, controls),
|
|
@@ -121,6 +123,7 @@ export function createManagement(options) {
|
|
|
121
123
|
create: (project, environment, input, controls) => request(`/projects/${id(project)}/environments/${id(environment)}/deployments`, accepted, input, controls),
|
|
122
124
|
},
|
|
123
125
|
environments: {
|
|
126
|
+
create: (project, input, controls) => request(`/projects/${id(project)}/environments`, accepted, input, controls),
|
|
124
127
|
list: (project, page = {}, signal) => request(`/projects/${id(project)}/environments${listQuery(page)}`, pageOf(environment), undefined, undefined, signal),
|
|
125
128
|
get: (project, resource, signal) => request(`/projects/${id(project)}/environments/${id(resource)}`, environment, undefined, undefined, signal),
|
|
126
129
|
getPlacement: (project, environment, signal) => request(`/projects/${id(project)}/environments/${id(environment)}/placement`, placementState, undefined, undefined, signal),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { components } from "./wire.gen.js";
|
|
2
|
+
export type ResourceSummary = components["schemas"]["ResourceSummary"];
|
|
3
|
+
export type ResourceRelationships = components["schemas"]["ResourceRelationships"];
|
|
4
|
+
export type ResourceNode = components["schemas"]["ResourceNode"];
|
|
5
|
+
export type ResourceRelationship = components["schemas"]["ResourceRelationship"];
|
|
6
|
+
export type ResourceSummaryPage = components["schemas"]["ResourceSummaryPage"];
|
|
7
|
+
export declare function resourceSummary(value: unknown): value is ResourceSummary;
|
|
8
|
+
export declare function resourceRelationships(value: unknown): value is ResourceRelationships;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { object } from "./http.js";
|
|
2
|
+
function strings(value, fields) {
|
|
3
|
+
return object(value) && fields.every((key) => typeof value[key] === "string");
|
|
4
|
+
}
|
|
5
|
+
export function resourceSummary(value) {
|
|
6
|
+
return strings(value, [
|
|
7
|
+
"product",
|
|
8
|
+
"tenant",
|
|
9
|
+
"project",
|
|
10
|
+
"projectName",
|
|
11
|
+
"environment",
|
|
12
|
+
"environmentName",
|
|
13
|
+
"environmentKind",
|
|
14
|
+
"cell",
|
|
15
|
+
"location",
|
|
16
|
+
"phase",
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
function dateOrNull(value) {
|
|
20
|
+
return (value === null ||
|
|
21
|
+
(typeof value === "string" && Number.isFinite(Date.parse(value))));
|
|
22
|
+
}
|
|
23
|
+
export function resourceRelationships(value) {
|
|
24
|
+
if (!object(value) ||
|
|
25
|
+
!resourceSummary(value.resource) ||
|
|
26
|
+
!Number.isSafeInteger(value.epoch) ||
|
|
27
|
+
!dateOrNull(value.observedAt) ||
|
|
28
|
+
!dateOrNull(value.validUntil) ||
|
|
29
|
+
typeof value.fresh !== "boolean")
|
|
30
|
+
return false;
|
|
31
|
+
if (!Array.isArray(value.nodes) ||
|
|
32
|
+
!value.nodes.every((node) => strings(node, [
|
|
33
|
+
"id",
|
|
34
|
+
"kind",
|
|
35
|
+
"label",
|
|
36
|
+
"resourceId",
|
|
37
|
+
"providerId",
|
|
38
|
+
"cell",
|
|
39
|
+
"location",
|
|
40
|
+
"state",
|
|
41
|
+
"allocation",
|
|
42
|
+
"evidence",
|
|
43
|
+
"operationId",
|
|
44
|
+
])))
|
|
45
|
+
return false;
|
|
46
|
+
const ids = new Set(value.nodes.map((node) => node.id));
|
|
47
|
+
if (ids.size !== value.nodes.length ||
|
|
48
|
+
!Array.isArray(value.relationships) ||
|
|
49
|
+
!value.relationships.every((edge) => strings(edge, ["id", "source", "target", "kind", "evidence"]) &&
|
|
50
|
+
object(edge) &&
|
|
51
|
+
ids.has(edge.source) &&
|
|
52
|
+
ids.has(edge.target)))
|
|
53
|
+
return false;
|
|
54
|
+
if (!object(value.recovery) ||
|
|
55
|
+
!strings(value.recovery, [
|
|
56
|
+
"protection",
|
|
57
|
+
"targetCell",
|
|
58
|
+
"targetLocation",
|
|
59
|
+
"automaticFailover",
|
|
60
|
+
]) ||
|
|
61
|
+
value.recovery.automaticFailover !== "not-established" ||
|
|
62
|
+
!dateOrNull(value.recovery.checkpointAt))
|
|
63
|
+
return false;
|
|
64
|
+
const test = value.recovery.latestTest;
|
|
65
|
+
if (test !== null &&
|
|
66
|
+
(!strings(test, ["operationId", "state", "recordedAt"]) ||
|
|
67
|
+
!object(test) ||
|
|
68
|
+
typeof test.recordedAt !== "string" ||
|
|
69
|
+
!Number.isFinite(Date.parse(test.recordedAt))))
|
|
70
|
+
return false;
|
|
71
|
+
return (Array.isArray(value.notes) &&
|
|
72
|
+
value.notes.every((note) => typeof note === "string"));
|
|
73
|
+
}
|
package/dist/operator.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type TransportBudget } from "./http.js";
|
|
2
|
+
import { type UsageQuery } from "./admission.js";
|
|
3
|
+
export type { AdmissionState, AdmissionPolicy, AdmissionUsagePage, UsageQuery } from "./admission.js";
|
|
4
|
+
export type { ResourceSummary, ResourceRelationships, ResourceNode, ResourceRelationship, ResourceSummaryPage } from "./operator-resources.js";
|
|
2
5
|
import { type PageOptions } from "./inventory.js";
|
|
3
6
|
import type { components } from "./wire.gen.js";
|
|
4
7
|
export { AstrogateError } from "./http.js";
|
|
@@ -9,6 +12,54 @@ export type OperatorOptions = TransportBudget & {
|
|
|
9
12
|
export type { PageOptions } from "./inventory.js";
|
|
10
13
|
type EnvironmentTopology = components["schemas"]["EnvironmentTopology"];
|
|
11
14
|
export declare function createOperator(options: OperatorOptions): {
|
|
15
|
+
admission: {
|
|
16
|
+
get: (product: string, tenant: string, signal?: AbortSignal) => Promise<{
|
|
17
|
+
policy: components["schemas"]["AdmissionPolicy"];
|
|
18
|
+
occupancy: components["schemas"]["AdmissionOccupancy"][];
|
|
19
|
+
storageCredits: components["schemas"]["TenantStorageCredit"][];
|
|
20
|
+
}>;
|
|
21
|
+
usage: (product: string, tenant: string, query: UsageQuery, signal?: AbortSignal) => Promise<{
|
|
22
|
+
points: components["schemas"]["AdmissionUsagePoint"][];
|
|
23
|
+
next?: string;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
resources: {
|
|
27
|
+
list: (product: string, tenant: string, page?: PageOptions, signal?: AbortSignal) => Promise<import("./inventory.js").InventoryPage<{
|
|
28
|
+
product: string;
|
|
29
|
+
tenant: string;
|
|
30
|
+
project: string;
|
|
31
|
+
projectName: string;
|
|
32
|
+
environment: string;
|
|
33
|
+
environmentName: string;
|
|
34
|
+
environmentKind: string;
|
|
35
|
+
cell: string;
|
|
36
|
+
location: string;
|
|
37
|
+
phase: string;
|
|
38
|
+
}>>;
|
|
39
|
+
relationships: (product: string, tenant: string, project: string, environment: string, signal?: AbortSignal) => Promise<{
|
|
40
|
+
resource: components["schemas"]["ResourceSummary"];
|
|
41
|
+
epoch: number;
|
|
42
|
+
observedAt: string | null;
|
|
43
|
+
validUntil: string | null;
|
|
44
|
+
fresh: boolean;
|
|
45
|
+
nodes: components["schemas"]["ResourceNode"][];
|
|
46
|
+
relationships: components["schemas"]["ResourceRelationship"][];
|
|
47
|
+
recovery: components["schemas"]["ResourceRecovery"];
|
|
48
|
+
notes: string[];
|
|
49
|
+
}>;
|
|
50
|
+
forHost: (providerId: string, page?: PageOptions, signal?: AbortSignal) => Promise<import("./inventory.js").InventoryPage<{
|
|
51
|
+
product: string;
|
|
52
|
+
tenant: string;
|
|
53
|
+
project: string;
|
|
54
|
+
projectName: string;
|
|
55
|
+
environment: string;
|
|
56
|
+
environmentName: string;
|
|
57
|
+
environmentKind: string;
|
|
58
|
+
cell: string;
|
|
59
|
+
location: string;
|
|
60
|
+
phase: string;
|
|
61
|
+
}>>;
|
|
62
|
+
};
|
|
12
63
|
products: {
|
|
13
64
|
list: (page?: PageOptions, signal?: AbortSignal) => Promise<import("./inventory.js").InventoryPage<{
|
|
14
65
|
id: string;
|
package/dist/operator.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// authenticate here. Issuance and mutation remain installation-owned.
|
|
3
3
|
import { identifier, managementAuthority, object, requestJSON, } from "./http.js";
|
|
4
4
|
import { project, environment, placementState, operation, operationLog, operatorOperationDetail, buildingBlock, hostnameBinding } from "./management-read.js";
|
|
5
|
+
import { admissionState, admissionUsage } from "./admission.js";
|
|
6
|
+
import { resourceSummary, resourceRelationships } from "./operator-resources.js";
|
|
5
7
|
import { listQuery, pageOf } from "./inventory.js";
|
|
6
8
|
export { AstrogateError } from "./http.js";
|
|
7
9
|
function catalogEntry(value) {
|
|
@@ -14,13 +16,32 @@ function environmentTopology(value) {
|
|
|
14
16
|
}
|
|
15
17
|
export function createOperator(options) {
|
|
16
18
|
const endpoint = managementAuthority(options);
|
|
17
|
-
function read(path, validate, signal) {
|
|
18
|
-
return requestJSON(new URL(
|
|
19
|
+
function read(path, validate, signal, root = "/v1/operator/products") {
|
|
20
|
+
return requestJSON(new URL(`${root}${path}`, endpoint), options, { method: "GET", headers: { authorization: `Bearer ${options.token}` } }, validate, signal);
|
|
19
21
|
}
|
|
20
22
|
const scope = (product, tenant) => `/${identifier(product)}/tenants/${identifier(tenant)}`;
|
|
21
23
|
const projectPath = (product, tenant, id) => `${scope(product, tenant)}/projects/${identifier(id)}`;
|
|
22
24
|
const environmentPath = (product, tenant, project, id) => `${projectPath(product, tenant, project)}/environments/${identifier(id)}`;
|
|
23
25
|
return {
|
|
26
|
+
admission: {
|
|
27
|
+
get: (product, tenant, signal) => read(`${scope(product, tenant)}/admission`, admissionState, signal),
|
|
28
|
+
usage: (product, tenant, query, signal) => {
|
|
29
|
+
const parameters = new URLSearchParams();
|
|
30
|
+
for (const [key, value] of Object.entries(query))
|
|
31
|
+
if (value !== undefined)
|
|
32
|
+
parameters.set(key, String(value));
|
|
33
|
+
return read(`${scope(product, tenant)}/usage?${parameters}`, admissionUsage, signal);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
resources: {
|
|
37
|
+
list: (product, tenant, page = {}, signal) => read(`${scope(product, tenant)}/resources${listQuery(page)}`, pageOf(resourceSummary), signal),
|
|
38
|
+
relationships: (product, tenant, project, environment, signal) => read(`${environmentPath(product, tenant, project, environment)}/relationships`, resourceRelationships, signal),
|
|
39
|
+
forHost: (providerId, page = {}, signal) => {
|
|
40
|
+
const query = new URLSearchParams(listQuery(page));
|
|
41
|
+
query.set("providerId", providerId);
|
|
42
|
+
return read(`?${query}`, pageOf(resourceSummary), signal, "/v1/operator/host-environments");
|
|
43
|
+
},
|
|
44
|
+
},
|
|
24
45
|
products: {
|
|
25
46
|
list: (page = {}, signal) => read(listQuery(page), pageOf(catalogEntry), signal),
|
|
26
47
|
},
|