@runblackcat/sdk 0.1.0-alpha.57 → 0.1.0-alpha.71
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/README.md +66 -12
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +385 -11
- package/sdk/dist/index.js +66 -2
package/README.md
CHANGED
|
@@ -26,8 +26,8 @@ are in [docs/PRODUCT.md](docs/PRODUCT.md).
|
|
|
26
26
|
rather than a connection.
|
|
27
27
|
- **Queue and backpressure** — a request the fleet has no room for waits durably
|
|
28
28
|
instead of failing, and is placed by a background drain as capacity arrives.
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Fleet capacity and the durable queue are the only backpressure; there are no
|
|
30
|
+
per-tenant quotas.
|
|
31
31
|
- **Demand-driven capacity** — the placer sizes the fleet from the backlog it
|
|
32
32
|
cannot place, through a provider seam, and retires idle workers back to zero.
|
|
33
33
|
- **Forkable templates** — build a Docker image into a validated state, memory,
|
|
@@ -49,7 +49,7 @@ are in [docs/PRODUCT.md](docs/PRODUCT.md).
|
|
|
49
49
|
| | |
|
|
50
50
|
|---|---|
|
|
51
51
|
| **Gateway** | Stateless. The single authenticated entry point; resolves sandbox→node and proxies. Scales horizontally. |
|
|
52
|
-
| **Placer** | Single writer. Bin-packs by memory and vCPU, proposes placements, owns
|
|
52
|
+
| **Placer** | Single writer. Bin-packs by memory and vCPU, proposes placements, owns the queue and the node registry. Sole writer of substrate desired capacity. |
|
|
53
53
|
| **Node agent** | One per host. Firecracker lifecycle, jailer, networking, UFFD handler. **Final authority on admission** — the placer proposes, the node accepts or rejects. |
|
|
54
54
|
| **Guest agent** | Inside each sandbox, reached over vsock. Exec, file I/O, template-declared boot services. |
|
|
55
55
|
| **Plugin supervisor (in development)** | Runs operator-installed, digest-pinned job-source plugins outside sandbox hosts. Each installation has its own identity, secret scope and sandbox grant. A small Rust SDK calls the scoped private API. |
|
|
@@ -71,7 +71,7 @@ flowchart TB
|
|
|
71
71
|
subgraph ctrl["Control host"]
|
|
72
72
|
direction LR
|
|
73
73
|
GW["blackcat-gateway<br/>auth · capabilities · routing<br/>listens :443"]
|
|
74
|
-
PL["blackcat-placer<br/>single writer · best-fit<br/>
|
|
74
|
+
PL["blackcat-placer<br/>single writer · best-fit<br/>queue · node registry"]
|
|
75
75
|
DB[("placer.sqlite")]
|
|
76
76
|
GW <-->|"Unix socket"| PL
|
|
77
77
|
PL --- DB
|
|
@@ -158,8 +158,7 @@ construction. Everything else is a network hop.
|
|
|
158
158
|
## Capacity and observability
|
|
159
159
|
|
|
160
160
|
Creation is admitted durably and answered immediately. A request the fleet cannot
|
|
161
|
-
place becomes a **queued acquisition** — it keeps its sandbox identity and
|
|
162
|
-
quota charge, and a background drain places it as capacity appears. Callers poll
|
|
161
|
+
place becomes a **queued acquisition** — it keeps its sandbox identity, and a background drain places it as capacity appears. Callers poll
|
|
163
162
|
the claim they already hold:
|
|
164
163
|
|
|
165
164
|
```
|
|
@@ -169,14 +168,27 @@ GET /v1/sandboxes/acquisitions/{id} -> 202 pending … -> 200 {"state":"p
|
|
|
169
168
|
POST /v1/sandboxes/{id}/renew {ttl_secs} -> 200 {"expires_at":…, "renewable_until":…}
|
|
170
169
|
```
|
|
171
170
|
|
|
171
|
+
To start many identical sandboxes, add `"count": N` (1 to 1000) to the create.
|
|
172
|
+
`request_id` then names the batch and member `i` is `request_id.i`. The whole batch
|
|
173
|
+
is recorded in one durable transaction and answered `202` at once, with no
|
|
174
|
+
connection held through restores; a retry returns the same members.
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
POST /v1/sandboxes {request_id,template,count,lease:{…}}
|
|
178
|
+
-> 202 {"request_id":…, "count":N, "counts":{"pending":N}, "members":[…]}
|
|
179
|
+
GET /v1/sandboxes/batches/{request_id} -> 202 while any member is pending, then 200
|
|
180
|
+
GET /v1/sandboxes/acquisitions/{id}.{i} -> one member, as an ordinary claim
|
|
181
|
+
```
|
|
182
|
+
|
|
172
183
|
`request_id` is caller-supplied and is the idempotency boundary, so repeating a
|
|
173
184
|
create is always safe: it returns the same claim rather than allocating a second
|
|
174
185
|
sandbox.
|
|
175
186
|
|
|
176
|
-
Backpressure
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
Backpressure is fleet capacity and the durable queue; there are no per-tenant
|
|
188
|
+
quotas. A queued acquisition that waits past the queue deadline is torn down by
|
|
189
|
+
the ordinary expiry reconciler. Request-rate limits are operator-set fleet state
|
|
190
|
+
(`blackcat admin rate-limits get|set`): token mint and OIDC limits are on by
|
|
191
|
+
default, every workload limit is unlimited until set.
|
|
180
192
|
|
|
181
193
|
The placer sizes the fleet from that backlog through the `CapacityProvider`
|
|
182
194
|
seam, and is the **only** writer of substrate desired capacity — it raises the
|
|
@@ -194,12 +206,53 @@ blackcat_demand_unsatisfied{tenant="…"} queued acquisitions
|
|
|
194
206
|
blackcat_demand_oldest_age_seconds longest current wait
|
|
195
207
|
blackcat_capacity_target_nodes workers last asked for
|
|
196
208
|
blackcat_nodes{health="healthy"} workers actually registered
|
|
197
|
-
blackcat_placement_rejected_total{reason="…"} no_capacity
|
|
209
|
+
blackcat_placement_rejected_total{reason="…"} no_capacity
|
|
198
210
|
```
|
|
199
211
|
|
|
200
212
|
A fleet pinned at its ceiling shows up as `blackcat_capacity_target_nodes`
|
|
201
213
|
persistently above `blackcat_nodes`.
|
|
202
214
|
|
|
215
|
+
## Recommended production shape: bare metal
|
|
216
|
+
|
|
217
|
+
Run workers on bare metal, one or a few large `.metal` hosts, and keep nested virtualization (a VM with `/dev/kvm`) as the quick-start
|
|
218
|
+
default: it launches in under a minute and needs no special quota. On metal every guest page fault costs about 7.6 times less than
|
|
219
|
+
nested, so the same fleet restores 2 to 3 times faster, fans out 30 times faster at 100 clones, costs a third as much per clone and
|
|
220
|
+
does not lose VMs to nested VM-entry failures.
|
|
221
|
+
|
|
222
|
+
| | bare metal (`c7i.metal-24xl`) | nested (`m8i.4xlarge`) |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| create p50 | 29 ms | 60 ms |
|
|
225
|
+
| first exec | 33 ms | 67 ms |
|
|
226
|
+
| snapshot restore | 13 ms | 43 ms |
|
|
227
|
+
| fan-out of 100 clones | 0.6 s | 17.9 s |
|
|
228
|
+
| cost per clone-hour | $0.0043 | $0.0132 |
|
|
229
|
+
| VM-entry deaths | 0 | 2.9 per 1,000 hot restores, 325 per 1,000 at density |
|
|
230
|
+
|
|
231
|
+
(Bake-off, 2 vCPU / 1 GiB app template, `contrib/bench/bakeoff/RESULTS.md`, "Huge pages". The hosts are different CPU
|
|
232
|
+
generations and metal has 12 times the CPUs, so the fan-out column is not virtualization alone. Metal is cheaper per clone because it
|
|
233
|
+
carries about 1,000 clones where the nested host was measured at 64.)
|
|
234
|
+
|
|
235
|
+
North star on one `c7i.metal-48xl` (500 clones of a built-from-source web app at once, each running a short test then destroyed):
|
|
236
|
+
99% done 112 s after the PR arrived, all 500 passed with no replacements and no VMM deaths, about $0.34 per PR from a stopped node,
|
|
237
|
+
against 300 s, 264 replacements and about $1.00 on five `m8i.8xlarge`.
|
|
238
|
+
|
|
239
|
+
Trade-offs, plainly:
|
|
240
|
+
|
|
241
|
+
* **Boot.** A metal reboot has been quoted at about 7.5 minutes. Measured on a `c7i.metal-48xl`: a fresh launch was registered with SSM
|
|
242
|
+
in 74 s and a stopped node was serving again 47 s after `StartInstances`, but stopping it took 320 s (unbilled). Time from a
|
|
243
|
+
planner launch to an enrolled node on metal has not been measured (see
|
|
244
|
+
`contrib/bench/results/2026-10-04-wave-c-metal-northstar.md`). Nested hosts register in 40 to 45 s.
|
|
245
|
+
* **Coarse sizes.** Metal is `.metal` sizes only: 96 or 192 vCPUs on the `c7i` family. A small or spiky fleet pays for a whole box
|
|
246
|
+
or runs nested.
|
|
247
|
+
* **Thin Spot capacity.** Metal Spot pools are small; the planner falls back to on-demand when one refuses, and the price you plan
|
|
248
|
+
with is the cheaper Spot one.
|
|
249
|
+
* **Large blast radius.** One host carries up to about a thousand clones. Losing it, or restarting its agent, hits all of them; keep
|
|
250
|
+
`fleet max-clones-per-host` as the cap you can accept and keep more than one host for anything that must survive a host loss.
|
|
251
|
+
|
|
252
|
+
The capacity planner prefers metal when a reservation's `needed_by` allows its boot time and the price per clone is lower
|
|
253
|
+
([docs/capacity-planning.md](docs/capacity-planning.md)); `blackcat_vm_entry_deaths` counts the VM-entry failures that make nested
|
|
254
|
+
hosts unreliable at density. Setup: [docs/bare-mode.md](docs/bare-mode.md#recommended-production-shape-bare-metal).
|
|
255
|
+
|
|
203
256
|
## Install (alpha)
|
|
204
257
|
|
|
205
258
|
Identical on bare metal and on AWS:
|
|
@@ -286,7 +339,8 @@ Linux x86-64/arm64, run `curl -fsSL https://runblackcat.dev/install.sh | sh`;
|
|
|
286
339
|
see [CLI installation](docs/cli-install.md). The installer uses release
|
|
287
340
|
checksums from the public
|
|
288
341
|
[`blackcat-releases`](https://github.com/TreebeardHQ/blackcat-releases) mirror
|
|
289
|
-
and needs neither Docker nor Rust on the client.
|
|
342
|
+
and needs neither Docker nor Rust on the client. To run a fleet on plain
|
|
343
|
+
Debian or Ubuntu servers, see [Bare mode](docs/bare-mode.md). The site itself lives in
|
|
290
344
|
[`site/`](site/) and publishes an allowlisted subset of `docs/`.
|
|
291
345
|
`blackcat template list` shows launchable templates to members without their
|
|
292
346
|
source; `blackcat sandbox claim alpine-e2e-matched` resolves the latest version
|
package/package.json
CHANGED
package/sdk/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export declare const CAPABILITY_OPS: {
|
|
|
21
21
|
readonly RENEW: 4096;
|
|
22
22
|
};
|
|
23
23
|
export type CapabilityOps = number;
|
|
24
|
+
export declare const BLACKCAT_ERROR_CODES: {
|
|
25
|
+
readonly SANDBOX_TRANSITIONING: "sandbox_transitioning";
|
|
26
|
+
readonly FLEET_UPGRADE_QUIESCING: "fleet_upgrade_quiescing";
|
|
27
|
+
};
|
|
24
28
|
export type FetchFunction = (input: RequestInfo | URL, init?: RequestInit & {
|
|
25
29
|
tls?: {
|
|
26
30
|
ca: string;
|
|
@@ -48,6 +52,17 @@ export interface TemplateDefaults {
|
|
|
48
52
|
memory_mib: number;
|
|
49
53
|
hibernate?: HibernateMode;
|
|
50
54
|
on_interruption?: "evacuate" | "destroy";
|
|
55
|
+
/** MiB of virtio-mem region booted into the checkpoint; a multiple of 128. Defaults to the template's memory. */
|
|
56
|
+
hotplug_max_mib?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The size a stored template version carries: `memory_mib` is the size it was
|
|
60
|
+
* captured at (boot memory plus what was plugged), and a sandbox can later
|
|
61
|
+
* take any size from `base_memory_mib` to `base_memory_mib + hotplug_max_mib`.
|
|
62
|
+
*/
|
|
63
|
+
export interface TemplateShapeDefaults extends TemplateDefaults {
|
|
64
|
+
base_memory_mib: number;
|
|
65
|
+
hotplug_max_mib: number;
|
|
51
66
|
}
|
|
52
67
|
export type HibernateMode = "memory";
|
|
53
68
|
export type TemplateEgressPolicy = {
|
|
@@ -95,6 +110,9 @@ interface TemplateDockerSourceBase {
|
|
|
95
110
|
defaults: TemplateDefaults;
|
|
96
111
|
egress: TemplateEgressPolicy;
|
|
97
112
|
restore_health: TemplateRestoreHealth;
|
|
113
|
+
/** Runs in the guest after it is ready and before the capture, so every
|
|
114
|
+
* clone restores with the app's caches and JIT already hot. Must exit 0. */
|
|
115
|
+
warmup?: TemplateRestoreHealth;
|
|
98
116
|
}
|
|
99
117
|
interface TemplatePreparation {
|
|
100
118
|
parameters: Record<string, {
|
|
@@ -133,8 +151,11 @@ export interface UploadedImage {
|
|
|
133
151
|
unchanged: boolean;
|
|
134
152
|
}
|
|
135
153
|
export interface TemplateSandboxSource {
|
|
154
|
+
/** `warmup` runs in the sandbox just before it is captured; omitted, the
|
|
155
|
+
* sandbox's template's own warm-up (if any) runs. */
|
|
136
156
|
sandbox: {
|
|
137
157
|
id: string;
|
|
158
|
+
warmup?: TemplateRestoreHealth;
|
|
138
159
|
};
|
|
139
160
|
/**
|
|
140
161
|
* A promoted sandbox keeps the machine dimensions it was captured on, so
|
|
@@ -144,20 +165,33 @@ export interface TemplateSandboxSource {
|
|
|
144
165
|
egress?: TemplateEgressPolicy;
|
|
145
166
|
}
|
|
146
167
|
export type TemplateSource = TemplateDockerSource | TemplateUploadedImageSource | TemplateSandboxSource;
|
|
168
|
+
/** What a promotion adds to a template create: how it is warmed and packed. */
|
|
169
|
+
export interface PromotionOptions {
|
|
170
|
+
/** Warm the new version onto this many more nodes in bulk, beyond the node
|
|
171
|
+
* that captured it (at most 99), or onto `"all"` the other healthy nodes.
|
|
172
|
+
* Nodes bound to a capacity reservation that follows the template are warmed
|
|
173
|
+
* regardless. */
|
|
174
|
+
prefetch_copies?: number | "all";
|
|
175
|
+
/** Millicores each clone of the new version reserves, at most 1000 per vCPU.
|
|
176
|
+
* Omitted, the version inherits what the sandbox's own template reserves. */
|
|
177
|
+
cpu_reservation_mcpu?: number;
|
|
178
|
+
}
|
|
147
179
|
export type CreateTemplateInput = {
|
|
148
180
|
name: string;
|
|
149
181
|
source: TemplateDockerSource;
|
|
150
182
|
} | {
|
|
151
183
|
name: string;
|
|
152
184
|
source: TemplateUploadedImageSource;
|
|
153
|
-
} | {
|
|
185
|
+
} | ({
|
|
154
186
|
name?: string;
|
|
155
187
|
source: TemplateSandboxSource;
|
|
156
|
-
};
|
|
188
|
+
} & PromotionOptions);
|
|
157
189
|
export interface Template {
|
|
158
190
|
id: string;
|
|
159
191
|
name: string;
|
|
160
192
|
version: number;
|
|
193
|
+
/** `local_ready` while a promotion's chunks are still being published. */
|
|
194
|
+
state?: "local_ready" | "published";
|
|
161
195
|
/** The gateway stores the complete source manifest as JSON text. */
|
|
162
196
|
source: string;
|
|
163
197
|
current: boolean;
|
|
@@ -187,27 +221,156 @@ export interface PrepareSandboxResponse {
|
|
|
187
221
|
export interface CreateTemplateResponse {
|
|
188
222
|
id: string;
|
|
189
223
|
version: number;
|
|
224
|
+
/**
|
|
225
|
+
* `local_ready`: a promotion whose capture has been proved but whose chunks
|
|
226
|
+
* are still being published. The version is current and claimable, but only
|
|
227
|
+
* on the node that captured it; clones depend only on that node's files and
|
|
228
|
+
* are destroyed if the publication fails. `published`: every chunk is in the
|
|
229
|
+
* object store.
|
|
230
|
+
*/
|
|
231
|
+
state?: "local_ready" | "published";
|
|
232
|
+
/**
|
|
233
|
+
* Capture/publish phase breakdown for a checkpoint promotion, when this
|
|
234
|
+
* create captured a running sandbox. Absent for a template created without
|
|
235
|
+
* a capture, and for a server that predates this field.
|
|
236
|
+
*/
|
|
237
|
+
capture_timing?: {
|
|
238
|
+
capture_ms: number;
|
|
239
|
+
/** How long the source VM stood paused: the memory/state dump, the disk
|
|
240
|
+
* freeze and the resume. Sealing and upload happen after it. */
|
|
241
|
+
pause_ms: number;
|
|
242
|
+
/** Absent while the version is `local_ready`: publication is not over. */
|
|
243
|
+
publish_ms?: number;
|
|
244
|
+
/** Restoring a disposable clone from the capture and running the
|
|
245
|
+
* template's restore-health command in it; both overlap the upload. */
|
|
246
|
+
validation_restore_ms?: number;
|
|
247
|
+
validation_health_ms?: number;
|
|
248
|
+
/** Whether the capturing node was left holding the new version's memory
|
|
249
|
+
* and disk for fault-free restores. */
|
|
250
|
+
warmed?: {
|
|
251
|
+
memory: boolean;
|
|
252
|
+
disk: boolean;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
/** Present for a promotion that asked for or achieved warming. */
|
|
256
|
+
warming?: {
|
|
257
|
+
capturing_node_warm: boolean;
|
|
258
|
+
prefetch_copies: number;
|
|
259
|
+
/** Pass to `templates.prefetchStatus` to follow the extra copies. */
|
|
260
|
+
prefetch_id?: string;
|
|
261
|
+
error?: string;
|
|
262
|
+
};
|
|
190
263
|
}
|
|
191
|
-
export type CreateSandboxInput =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
264
|
+
export type CreateSandboxInput = {
|
|
265
|
+
/**
|
|
266
|
+
* The template name or version id. Omitted, it is the tenant's `default`
|
|
267
|
+
* template, which the gateway builds on the tenant's first use: that first
|
|
268
|
+
* create fails with a 503 `default_template_building` until the build is
|
|
269
|
+
* ready, and repeating the same request is how to wait for it.
|
|
270
|
+
*/
|
|
271
|
+
template?: string;
|
|
272
|
+
/** Must equal the checkpoint's vCPUs, which are fixed at capture; omit it. */
|
|
273
|
+
vcpus?: number;
|
|
274
|
+
/**
|
|
275
|
+
* Guest memory in MiB: the checkpoint's captured size or larger, up to its
|
|
276
|
+
* boot memory plus hotplug region (the extra is plugged right after restore).
|
|
277
|
+
*/
|
|
278
|
+
memory_mib?: number;
|
|
279
|
+
/** CPU entitlement in millicores, at most the checkpoint's vCPUs * 1000. */
|
|
280
|
+
cpu_mcpu?: number;
|
|
200
281
|
/** Stable across retries of one acquisition; never reuse for another request. */
|
|
201
282
|
request_id: string;
|
|
202
283
|
ssh_public_key?: string;
|
|
203
284
|
lease: SandboxLeaseInput;
|
|
285
|
+
/** A capacity reservation this create should draw a bound slot from. */
|
|
286
|
+
reservation?: string;
|
|
287
|
+
/**
|
|
288
|
+
* Ask for a capability scoped to each sandbox this request creates, returned
|
|
289
|
+
* in the response (`capability` on the receipt, or on each batch member), so
|
|
290
|
+
* a caller driving many sandboxes mints none of its own. `ops` must be within
|
|
291
|
+
* the creating token's; `ttl_secs` is capped at 900 and renewable like any
|
|
292
|
+
* token. Only the request that creates a sandbox gets its capability: a
|
|
293
|
+
* repeat of the same `request_id` returns none.
|
|
294
|
+
*/
|
|
295
|
+
capability?: {
|
|
296
|
+
ops: string[] | CapabilityOps;
|
|
297
|
+
ttl_secs: number;
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
/** A capability scoped to one sandbox, as returned by a create. */
|
|
301
|
+
export interface SandboxCapability {
|
|
302
|
+
token: string;
|
|
303
|
+
token_id: string;
|
|
304
|
+
ops: string[];
|
|
305
|
+
/** Epoch milliseconds. */
|
|
306
|
+
expires_at: number;
|
|
307
|
+
renewable_until: number;
|
|
308
|
+
}
|
|
309
|
+
/** A batch create: `request_id` names the batch, member `i` is `<request_id>.<i>`. */
|
|
310
|
+
export type CreateSandboxBatchInput = CreateSandboxInput & {
|
|
311
|
+
/** How many identical sandboxes to create, 1 to 1000. */
|
|
312
|
+
count: number;
|
|
204
313
|
};
|
|
314
|
+
export type SandboxBatchMemberState = "pending" | "placed" | "failed" | "destroyed";
|
|
315
|
+
export interface SandboxBatchMember {
|
|
316
|
+
/** Poll this member alone with `sandboxes.acquisition(request_id)`. */
|
|
317
|
+
request_id: string;
|
|
318
|
+
id: string;
|
|
319
|
+
state: SandboxBatchMemberState;
|
|
320
|
+
/** Present when the create asked for `capability`. */
|
|
321
|
+
capability?: SandboxCapability;
|
|
322
|
+
}
|
|
323
|
+
export interface SandboxBatch {
|
|
324
|
+
/** The batch's request id. */
|
|
325
|
+
request_id: string;
|
|
326
|
+
count: number;
|
|
327
|
+
/** Members in each state; a state with no members is absent. */
|
|
328
|
+
counts: Partial<Record<SandboxBatchMemberState, number>>;
|
|
329
|
+
members: SandboxBatchMember[];
|
|
330
|
+
}
|
|
205
331
|
export interface SandboxLeaseInput {
|
|
206
332
|
/** Initial lease duration, between 60 seconds and 24 hours. */
|
|
207
333
|
ttl_secs: number;
|
|
208
334
|
/** Hard lifetime from acquisition, at least ttl_secs and at most 30 days. */
|
|
209
335
|
renewable_for_secs: number;
|
|
210
336
|
}
|
|
337
|
+
export type CreateCapacityReservationInput = ({
|
|
338
|
+
/** One immutable template version, pinned for the reservation's life. */
|
|
339
|
+
template: string;
|
|
340
|
+
template_resource?: never;
|
|
341
|
+
} | {
|
|
342
|
+
/** A template resource name: the reservation follows whichever version
|
|
343
|
+
* is current, even one that does not exist yet, and its bound nodes are
|
|
344
|
+
* warmed with each new version as it is activated. */
|
|
345
|
+
template_resource: string;
|
|
346
|
+
template?: never;
|
|
347
|
+
}) & {
|
|
348
|
+
/** How many sandboxes of this template/shape to reserve. */
|
|
349
|
+
count: number;
|
|
350
|
+
vcpus: number;
|
|
351
|
+
memory_mib: number;
|
|
352
|
+
/** How long to hold the reservation, between 1 second and 4 hours. */
|
|
353
|
+
ttl_secs: number;
|
|
354
|
+
};
|
|
355
|
+
export interface CapacityReservation {
|
|
356
|
+
id: string;
|
|
357
|
+
state: "active" | "released" | "expired";
|
|
358
|
+
/** The version nodes are warmed with; null while a reservation following a
|
|
359
|
+
* template resource waits for the resource's first version. */
|
|
360
|
+
template: string | null;
|
|
361
|
+
/** The template resource this reservation follows, or null when pinned. */
|
|
362
|
+
template_resource: string | null;
|
|
363
|
+
count: number;
|
|
364
|
+
vcpus: number;
|
|
365
|
+
memory_mib: number;
|
|
366
|
+
/** Slots consumed by an actual sandbox create. */
|
|
367
|
+
claimed: number;
|
|
368
|
+
/** Slots bound to a specific node and not yet claimed. */
|
|
369
|
+
bound: number;
|
|
370
|
+
/** Bound slots whose node already has the template warm. */
|
|
371
|
+
warm: number;
|
|
372
|
+
expires_at: SystemTimeValue;
|
|
373
|
+
}
|
|
211
374
|
/**
|
|
212
375
|
* Response returned by POST /v1/sandboxes (the gateway has no template field).
|
|
213
376
|
* The gateway sends a `failed` receipt with HTTP 409, so `create` and
|
|
@@ -221,6 +384,14 @@ export type CreateSandboxResponse = {
|
|
|
221
384
|
tier: "boot" | "warm_pool" | "restore";
|
|
222
385
|
expires_at: SystemTimeValue;
|
|
223
386
|
renewable_until: SystemTimeValue;
|
|
387
|
+
/**
|
|
388
|
+
* Phase-timing breakdown for the launch that placed this sandbox, once one
|
|
389
|
+
* has committed. Absent while still queued/pending, and for a server that
|
|
390
|
+
* predates this field.
|
|
391
|
+
*/
|
|
392
|
+
restore_timing?: RestoreTiming;
|
|
393
|
+
/** Present when the create asked for `capability` and created this sandbox. */
|
|
394
|
+
capability?: SandboxCapability;
|
|
224
395
|
} & ({
|
|
225
396
|
state: "placed";
|
|
226
397
|
node: string;
|
|
@@ -232,19 +403,96 @@ export type CreateSandboxResponse = {
|
|
|
232
403
|
node: null;
|
|
233
404
|
error: string;
|
|
234
405
|
});
|
|
406
|
+
/**
|
|
407
|
+
* Restore phase breakdown a node reported for a checkpoint launch (Proposal
|
|
408
|
+
* F, docs/restore-performance.md). `phase_ms` mirrors the node's own
|
|
409
|
+
* `checkpoint vm timing` log line; `chunk_source` reports how much of the
|
|
410
|
+
* checkpoint's memory/disk was fetched remotely vs. served locally, how many
|
|
411
|
+
* UFFD faults and NBD reads this restore actually served (both zero on the
|
|
412
|
+
* file-restore path), and the checkpoint's logical sizes. `memory_source` is
|
|
413
|
+
* `"file"` when a node-local materialized copy let this restore skip
|
|
414
|
+
* UFFD/NBD lazy faulting entirely (fault-free warm restore,
|
|
415
|
+
* docs/restore-performance.md), or `"uffd"` for the lazy chunk-source path
|
|
416
|
+
* every checkpoint restore used before it.
|
|
417
|
+
*/
|
|
418
|
+
export interface RestoreTiming {
|
|
419
|
+
proposal_ms: number;
|
|
420
|
+
restore_vm_ms: number;
|
|
421
|
+
commit_ms: number;
|
|
422
|
+
total_ms: number;
|
|
423
|
+
phase_ms: {
|
|
424
|
+
open: number;
|
|
425
|
+
network: number;
|
|
426
|
+
firecracker: number;
|
|
427
|
+
state: number;
|
|
428
|
+
disk: number;
|
|
429
|
+
pager: number;
|
|
430
|
+
load: number;
|
|
431
|
+
activation: number;
|
|
432
|
+
/** Milliseconds from the TAP being raised until the guest's gateway and resolver answered; absent from a node that predates it, 0 from a guest that does. */
|
|
433
|
+
network_ready?: number;
|
|
434
|
+
};
|
|
435
|
+
memory_source?: "file" | "uffd";
|
|
436
|
+
chunk_source: {
|
|
437
|
+
remote_requests: number;
|
|
438
|
+
remote_bytes: number;
|
|
439
|
+
local_requests?: number;
|
|
440
|
+
local_bytes?: number;
|
|
441
|
+
uffd_faults?: number;
|
|
442
|
+
uffd_fault_bytes?: number;
|
|
443
|
+
nbd_reads?: number;
|
|
444
|
+
nbd_read_bytes?: number;
|
|
445
|
+
logical_memory_bytes: number;
|
|
446
|
+
logical_disk_bytes: number;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
235
449
|
export interface Sandbox {
|
|
236
450
|
id: string;
|
|
237
451
|
template: string;
|
|
238
452
|
state: string;
|
|
239
453
|
node?: string;
|
|
454
|
+
/** vCPUs the guest has now: the template's, until a cold resize reboots it onto another count. */
|
|
455
|
+
vcpus: number;
|
|
456
|
+
/** Current guest memory in MiB (boot memory plus what is plugged). */
|
|
457
|
+
memory_mib: number;
|
|
458
|
+
/** Current CPU entitlement in millicores. */
|
|
459
|
+
cpu_mcpu: number;
|
|
240
460
|
expires_at: SystemTimeValue;
|
|
241
461
|
renewable_until: SystemTimeValue;
|
|
242
462
|
}
|
|
463
|
+
/** A short-lived SSH key to authorize on a running sandbox. */
|
|
464
|
+
export interface GrantSshKeyInput {
|
|
465
|
+
/** One `ssh-ed25519` public key line. */
|
|
466
|
+
public_key: string;
|
|
467
|
+
/** Lifetime in seconds, 1 to 86400. The key only matters while connecting. */
|
|
468
|
+
ttl_secs: number;
|
|
469
|
+
}
|
|
470
|
+
export interface SshKeyGrant {
|
|
471
|
+
/** Unix milliseconds at which the key stops authenticating. */
|
|
472
|
+
expires_at: number;
|
|
473
|
+
/** The login account in the sandbox. */
|
|
474
|
+
user: string;
|
|
475
|
+
/** The sandbox's SSH host public key; pin it before connecting. */
|
|
476
|
+
host_key: string;
|
|
477
|
+
}
|
|
243
478
|
export interface RenewSandboxResponse {
|
|
244
479
|
id: string;
|
|
245
480
|
expires_at: SystemTimeValue;
|
|
246
481
|
renewable_until: SystemTimeValue;
|
|
247
482
|
}
|
|
483
|
+
/** The CPU a node runs on, as it reports it. */
|
|
484
|
+
export interface CpuIdentity {
|
|
485
|
+
vendor: string;
|
|
486
|
+
model_name: string;
|
|
487
|
+
family: number;
|
|
488
|
+
model: number;
|
|
489
|
+
stepping: number;
|
|
490
|
+
microcode: string;
|
|
491
|
+
/** Hash of the CPUID feature leaves a guest's CPUID is derived from. */
|
|
492
|
+
features_hash: string;
|
|
493
|
+
/** The host is itself a guest (nested virtualization). */
|
|
494
|
+
nested: boolean;
|
|
495
|
+
}
|
|
248
496
|
/**
|
|
249
497
|
* Tenant-scoped view returned by GET /v1/fleet. Residents and cached snapshots
|
|
250
498
|
* are already filtered by the gateway to the caller's tenant.
|
|
@@ -262,6 +510,78 @@ export interface FleetNode {
|
|
|
262
510
|
residents: string[];
|
|
263
511
|
cached_snapshots: string[];
|
|
264
512
|
interruption: FleetInterruption | null;
|
|
513
|
+
/** Charged versus measured memory and the node's pressure state. */
|
|
514
|
+
memory: FleetNodeMemory;
|
|
515
|
+
/** Whether the host runs with SMT active. */
|
|
516
|
+
smt_active: boolean;
|
|
517
|
+
/** The node's CPU; absent from nodes older than CPU-aware placement. */
|
|
518
|
+
cpu?: CpuIdentity | null;
|
|
519
|
+
/** 1000 charges file-backed clones in full; 2000 charges them at half. */
|
|
520
|
+
max_memory_ratio_milli: number;
|
|
521
|
+
max_vcpu_ratio_milli: number;
|
|
522
|
+
/** How the fleet decides what a sandbox costs. */
|
|
523
|
+
packing_mode?: "learned" | "manual";
|
|
524
|
+
}
|
|
525
|
+
export type MemoryPressure = "normal" | "high" | "critical";
|
|
526
|
+
export interface FleetNodeMemory {
|
|
527
|
+
/** What admission charged for the node's residents. */
|
|
528
|
+
charged_mib: number;
|
|
529
|
+
/** The residents' sizes before the overcommit discount. */
|
|
530
|
+
plugged_mib: number;
|
|
531
|
+
/** Resident memory of the residents' VMMs, shared pages counted per mapper. */
|
|
532
|
+
measured_rss_mib: number;
|
|
533
|
+
/** Proportional memory the residents cost; compare with `charged_mib`. */
|
|
534
|
+
measured_pss_mib: number;
|
|
535
|
+
/** Memory only one resident maps (Private_Clean + Private_Dirty). */
|
|
536
|
+
measured_uss_mib: number;
|
|
537
|
+
host_available_mib: number;
|
|
538
|
+
psi_some_avg10_milli: number;
|
|
539
|
+
pressure: MemoryPressure;
|
|
540
|
+
}
|
|
541
|
+
export type FleetTrust = "single_trust" | "multi_tenant";
|
|
542
|
+
export interface ResizeSandboxInput {
|
|
543
|
+
/**
|
|
544
|
+
* Target vCPU count. A count different from the sandbox's reboots the guest
|
|
545
|
+
* cold on its own disk: every process ends, template boot services restart,
|
|
546
|
+
* processes started by hand do not.
|
|
547
|
+
*/
|
|
548
|
+
vcpus?: number;
|
|
549
|
+
/** Target guest memory in MiB: boot memory plus a multiple of 128. */
|
|
550
|
+
memory_mib?: number;
|
|
551
|
+
/** Target CPU entitlement in millicores. */
|
|
552
|
+
cpu_mcpu?: number;
|
|
553
|
+
/** Drop the guest's page caches before a shrink. Defaults to true. */
|
|
554
|
+
drop_caches?: boolean;
|
|
555
|
+
/** Stable across retries of one resize; generated when omitted. */
|
|
556
|
+
request_id?: string;
|
|
557
|
+
}
|
|
558
|
+
/** The size a resize reached. A shrink the guest could not fully honour reports what it reached. */
|
|
559
|
+
export interface SandboxResize {
|
|
560
|
+
id: string;
|
|
561
|
+
state: "placed";
|
|
562
|
+
request_id: string;
|
|
563
|
+
/** True when the guest was rebooted to change its vCPU count. */
|
|
564
|
+
cold: boolean;
|
|
565
|
+
vcpus: number;
|
|
566
|
+
memory_mib: number;
|
|
567
|
+
cpu_mcpu: number;
|
|
568
|
+
/** False when the guest stopped short of the requested memory. */
|
|
569
|
+
reached_target: boolean;
|
|
570
|
+
elapsed_ms: number;
|
|
571
|
+
/** Cold resizes only: durable processes the reboot ended. */
|
|
572
|
+
processes_ended?: number;
|
|
573
|
+
/** Cold resizes only: where the reboot spent its time, in milliseconds. */
|
|
574
|
+
phases?: {
|
|
575
|
+
quiesce_ms: number;
|
|
576
|
+
stop_ms: number;
|
|
577
|
+
boot_ms: number;
|
|
578
|
+
plug_ms: number;
|
|
579
|
+
handshake_ms: number;
|
|
580
|
+
health_ms: number;
|
|
581
|
+
};
|
|
582
|
+
/** Cold resizes only: unexpired SSH grants installed again, and ones that failed. */
|
|
583
|
+
ssh_grants_restored?: number;
|
|
584
|
+
ssh_grants_failed?: number;
|
|
265
585
|
}
|
|
266
586
|
export interface FleetInterruption {
|
|
267
587
|
reason: string;
|
|
@@ -269,6 +589,9 @@ export interface FleetInterruption {
|
|
|
269
589
|
}
|
|
270
590
|
export interface FleetResponse {
|
|
271
591
|
nodes: FleetNode[];
|
|
592
|
+
fleet_trust: FleetTrust;
|
|
593
|
+
/** `native`, or a CPU template (`t2`, `t2s`, `t2cl`, `t2a`). */
|
|
594
|
+
cpu_baseline?: string;
|
|
272
595
|
}
|
|
273
596
|
export interface TemplateRefreshPolicy {
|
|
274
597
|
interval_seconds: number;
|
|
@@ -395,6 +718,8 @@ export interface RenewTokenResponse {
|
|
|
395
718
|
}
|
|
396
719
|
export declare class BlackcatApiError extends Error {
|
|
397
720
|
readonly status: number;
|
|
721
|
+
/** Stable gateway error code when one is provided. */
|
|
722
|
+
readonly code?: string;
|
|
398
723
|
readonly retryAfter?: number;
|
|
399
724
|
readonly requestId?: string;
|
|
400
725
|
readonly details: unknown;
|
|
@@ -406,6 +731,7 @@ declare const MAX_PROCESS_STDIN: number;
|
|
|
406
731
|
declare const MAX_EXEC_STDIN: number;
|
|
407
732
|
export declare function epochMillis(value: unknown, field: string): number;
|
|
408
733
|
export declare function parseCreateSandboxResponse(value: unknown, requestId: string): CreateSandboxResponse;
|
|
734
|
+
export declare function parseSandboxBatch(value: unknown, requestId: string): SandboxBatch;
|
|
409
735
|
export declare function assertSecureBlackcatGatewayUrl(baseUrl: string): void;
|
|
410
736
|
declare class GatewayClient {
|
|
411
737
|
readonly baseUrl: string;
|
|
@@ -476,6 +802,16 @@ declare class FleetResource {
|
|
|
476
802
|
/** List the nodes and this tenant's residents/snapshots visible to INSPECT. */
|
|
477
803
|
list(): Promise<FleetResponse>;
|
|
478
804
|
}
|
|
805
|
+
declare class ReservationsResource {
|
|
806
|
+
private readonly client;
|
|
807
|
+
constructor(client: GatewayClient);
|
|
808
|
+
/** Reserve capacity for a burst of sandboxes of one template/shape. */
|
|
809
|
+
create(input: CreateCapacityReservationInput): Promise<CapacityReservation>;
|
|
810
|
+
get(id: string): Promise<CapacityReservation>;
|
|
811
|
+
/** Release early. Unclaimed slots free immediately; bound nodes then fall
|
|
812
|
+
* back to the normal empty-worker scale-in grace period. */
|
|
813
|
+
release(id: string): Promise<CapacityReservation>;
|
|
814
|
+
}
|
|
479
815
|
declare class SandboxesResource {
|
|
480
816
|
private readonly client;
|
|
481
817
|
constructor(client: GatewayClient);
|
|
@@ -487,10 +823,25 @@ declare class SandboxesResource {
|
|
|
487
823
|
* no automatic retry, new ID, or replacement allocation is synthesized here.
|
|
488
824
|
*/
|
|
489
825
|
create(input: CreateSandboxInput): Promise<CreateSandboxResponse>;
|
|
826
|
+
/**
|
|
827
|
+
* Create `count` identical sandboxes under one request ID. Resolves as soon
|
|
828
|
+
* as the whole batch is durably recorded; nothing waits on a restore.
|
|
829
|
+
* Repeat the exact request to recover a lost response: member IDs are
|
|
830
|
+
* stable. Poll `batch` for counts by state, or a member with `acquisition`.
|
|
831
|
+
*/
|
|
832
|
+
createBatch(input: CreateSandboxBatchInput): Promise<SandboxBatch>;
|
|
833
|
+
/** Counts by state and every member of a batch created with `createBatch`. */
|
|
834
|
+
batch(requestId: string): Promise<SandboxBatch>;
|
|
490
835
|
acquisition(requestId: string): Promise<CreateSandboxResponse>;
|
|
491
836
|
get(id: string): Promise<Sandbox>;
|
|
492
837
|
/** Extend the soft expiry without moving the immutable hard deadline. */
|
|
493
838
|
renew(id: string, ttl_secs: number): Promise<RenewSandboxResponse>;
|
|
839
|
+
/**
|
|
840
|
+
* Authorize `public_key` on a running sandbox for `ttl_secs`, whether or not
|
|
841
|
+
* it was created with a key. Needs the sandbox-scoped exec capability. Connect
|
|
842
|
+
* with `CONNECT /v1/sandboxes/{id}/ssh`, or let `blackcat sandbox ssh` do it.
|
|
843
|
+
*/
|
|
844
|
+
grantSshKey(id: string, input: GrantSshKeyInput): Promise<SshKeyGrant>;
|
|
494
845
|
exec(id: string, input: ExecInput): Promise<ExecResult>;
|
|
495
846
|
execStream(id: string, input: ExecInput): AsyncGenerator<ExecStreamChunk>;
|
|
496
847
|
startProcess(id: string, requestId: string, input: ProcessStartInput): Promise<ProcessRecord>;
|
|
@@ -528,10 +879,32 @@ declare class SandboxesResource {
|
|
|
528
879
|
snapshot(id: string): Promise<{
|
|
529
880
|
template: string;
|
|
530
881
|
}>;
|
|
882
|
+
/**
|
|
883
|
+
* Pause a running sandbox's VM in memory: processes, memory and network
|
|
884
|
+
* state are kept, nothing is captured, and `resume` continues it where it
|
|
885
|
+
* stopped. Memory stays resident and charged (`hibernate` frees it); the CPU
|
|
886
|
+
* entitlement is released. Idempotent. Requests that need the guest are
|
|
887
|
+
* refused while it is paused.
|
|
888
|
+
*/
|
|
889
|
+
pause(id: string): Promise<void>;
|
|
531
890
|
hibernate(id: string, input?: {
|
|
532
891
|
mode: HibernateMode;
|
|
533
892
|
}): Promise<void>;
|
|
893
|
+
/**
|
|
894
|
+
* Change a running sandbox's memory (live, within its checkpoint's hotplug
|
|
895
|
+
* region) and CPU entitlement; processes keep running. A `vcpus` different
|
|
896
|
+
* from the sandbox's is a cold reboot instead (`cold: true`). Repeating the
|
|
897
|
+
* same `request_id` replays the result.
|
|
898
|
+
*/
|
|
899
|
+
resize(id: string, input: ResizeSandboxInput): Promise<SandboxResize>;
|
|
900
|
+
/** Continue a paused sandbox, or restore a hibernated one. */
|
|
534
901
|
resume(id: string): Promise<void>;
|
|
902
|
+
/**
|
|
903
|
+
* Destroy a sandbox. Resolves when its VM is dead and the node has released
|
|
904
|
+
* its capacity, not when the node has finished removing its network device,
|
|
905
|
+
* jail and cgroup: that follows in the background, usually within a second.
|
|
906
|
+
* Destroying a sandbox that is already destroyed succeeds.
|
|
907
|
+
*/
|
|
535
908
|
destroy(id: string): Promise<void>;
|
|
536
909
|
}
|
|
537
910
|
export declare class BlackcatClient extends GatewayClient {
|
|
@@ -541,6 +914,7 @@ export declare class BlackcatClient extends GatewayClient {
|
|
|
541
914
|
readonly sandboxes: SandboxesResource;
|
|
542
915
|
readonly tokens: TokensResource;
|
|
543
916
|
readonly fleet: FleetResource;
|
|
917
|
+
readonly reservations: ReservationsResource;
|
|
544
918
|
constructor(options: BlackcatClientOptions);
|
|
545
919
|
/**
|
|
546
920
|
* Create a client that uses a sandbox capability for resource operations.
|
package/sdk/dist/index.js
CHANGED
|
@@ -14,9 +14,17 @@ var CAPABILITY_OPS = {
|
|
|
14
14
|
PROCESS_CANCEL: 2048,
|
|
15
15
|
RENEW: 4096
|
|
16
16
|
};
|
|
17
|
+
var BLACKCAT_ERROR_CODES = {
|
|
18
|
+
SANDBOX_TRANSITIONING: "sandbox_transitioning",
|
|
19
|
+
FLEET_UPGRADE_QUIESCING: "fleet_upgrade_quiescing"
|
|
20
|
+
};
|
|
21
|
+
function withTemplate(input) {
|
|
22
|
+
return { ...input, template: input.template ?? "default" };
|
|
23
|
+
}
|
|
17
24
|
|
|
18
25
|
class BlackcatApiError extends Error {
|
|
19
26
|
status;
|
|
27
|
+
code;
|
|
20
28
|
retryAfter;
|
|
21
29
|
requestId;
|
|
22
30
|
details;
|
|
@@ -25,6 +33,11 @@ class BlackcatApiError extends Error {
|
|
|
25
33
|
this.name = "BlackcatApiError";
|
|
26
34
|
this.status = status;
|
|
27
35
|
this.details = details;
|
|
36
|
+
if (typeof details === "object" && details !== null && !Array.isArray(details)) {
|
|
37
|
+
const code = details.code;
|
|
38
|
+
if (typeof code === "string")
|
|
39
|
+
this.code = code;
|
|
40
|
+
}
|
|
28
41
|
this.retryAfter = retryAfter;
|
|
29
42
|
this.requestId = typeof requestId === "string" && requestId.length === 32 && /^[a-f0-9]{32}$/i.test(requestId) ? requestId : undefined;
|
|
30
43
|
}
|
|
@@ -83,6 +96,20 @@ function parseCreateSandboxResponse(value, requestId) {
|
|
|
83
96
|
}
|
|
84
97
|
return value;
|
|
85
98
|
}
|
|
99
|
+
var BATCH_STATES = ["pending", "placed", "failed", "destroyed"];
|
|
100
|
+
function parseSandboxBatch(value, requestId) {
|
|
101
|
+
const malformed = () => new BlackcatProtocolError("sandbox batch is malformed or belongs to another request");
|
|
102
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
103
|
+
throw malformed();
|
|
104
|
+
const row = value;
|
|
105
|
+
if (row.request_id !== requestId || typeof row.count !== "number" || typeof row.counts !== "object" || row.counts === null || !Array.isArray(row.members) || row.members.length !== row.count || !row.members.every((member) => {
|
|
106
|
+
const m = member;
|
|
107
|
+
return typeof m === "object" && m !== null && typeof m.request_id === "string" && typeof m.id === "string" && BATCH_STATES.includes(String(m.state));
|
|
108
|
+
})) {
|
|
109
|
+
throw malformed();
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
86
113
|
function parseMintTokenResponse(value) {
|
|
87
114
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
88
115
|
throw new BlackcatProtocolError("token mint response is not an object");
|
|
@@ -395,6 +422,22 @@ class FleetResource {
|
|
|
395
422
|
}
|
|
396
423
|
}
|
|
397
424
|
|
|
425
|
+
class ReservationsResource {
|
|
426
|
+
client;
|
|
427
|
+
constructor(client) {
|
|
428
|
+
this.client = client;
|
|
429
|
+
}
|
|
430
|
+
create(input) {
|
|
431
|
+
return this.client.json("POST", "/v1/capacity-reservations", input, "token");
|
|
432
|
+
}
|
|
433
|
+
get(id) {
|
|
434
|
+
return this.client.json("GET", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "token");
|
|
435
|
+
}
|
|
436
|
+
release(id) {
|
|
437
|
+
return this.client.json("DELETE", `/v1/capacity-reservations/${pathSegment(id)}`, undefined, "token");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
398
441
|
class SandboxesResource {
|
|
399
442
|
client;
|
|
400
443
|
constructor(client) {
|
|
@@ -404,7 +447,13 @@ class SandboxesResource {
|
|
|
404
447
|
return this.client.json("GET", "/v1/sandboxes", undefined, "token");
|
|
405
448
|
}
|
|
406
449
|
async create(input) {
|
|
407
|
-
return parseCreateSandboxResponse(await this.client.json("POST", "/v1/sandboxes", input, "token", false), input.request_id);
|
|
450
|
+
return parseCreateSandboxResponse(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
|
|
451
|
+
}
|
|
452
|
+
async createBatch(input) {
|
|
453
|
+
return parseSandboxBatch(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
|
|
454
|
+
}
|
|
455
|
+
async batch(requestId) {
|
|
456
|
+
return parseSandboxBatch(await this.client.json("GET", `/v1/sandboxes/batches/${pathSegment(requestId)}`, undefined, "token"), requestId);
|
|
408
457
|
}
|
|
409
458
|
async acquisition(requestId) {
|
|
410
459
|
return parseCreateSandboxResponse(await this.client.json("GET", `/v1/sandboxes/acquisitions/${pathSegment(requestId)}`, undefined, "token"), requestId);
|
|
@@ -415,6 +464,9 @@ class SandboxesResource {
|
|
|
415
464
|
renew(id, ttl_secs) {
|
|
416
465
|
return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/renew`, { ttl_secs }, "token");
|
|
417
466
|
}
|
|
467
|
+
grantSshKey(id, input) {
|
|
468
|
+
return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/ssh-keys`, input, "token");
|
|
469
|
+
}
|
|
418
470
|
async exec(id, input) {
|
|
419
471
|
return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/exec`, input, "token", false, execDeadline(input)).then(execResult);
|
|
420
472
|
}
|
|
@@ -596,11 +648,19 @@ class SandboxesResource {
|
|
|
596
648
|
snapshot(id) {
|
|
597
649
|
return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/snapshot`, undefined, "token");
|
|
598
650
|
}
|
|
651
|
+
pause(id) {
|
|
652
|
+
return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/pause`, undefined, "token").then(() => {
|
|
653
|
+
return;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
599
656
|
hibernate(id, input) {
|
|
600
657
|
return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/hibernate`, input === undefined ? undefined : JSON.stringify(input), "token", input === undefined ? undefined : "application/json", false).then(() => {
|
|
601
658
|
return;
|
|
602
659
|
});
|
|
603
660
|
}
|
|
661
|
+
resize(id, input) {
|
|
662
|
+
return this.client.json("POST", `/v1/sandboxes/${pathSegment(id)}/resize`, { ...input, request_id: input.request_id ?? `resize-${crypto.randomUUID()}` }, "token", false);
|
|
663
|
+
}
|
|
604
664
|
resume(id) {
|
|
605
665
|
return this.client.raw("POST", `/v1/sandboxes/${pathSegment(id)}/resume`, undefined, "token").then(() => {
|
|
606
666
|
return;
|
|
@@ -618,6 +678,7 @@ class BlackcatClient extends GatewayClient {
|
|
|
618
678
|
sandboxes;
|
|
619
679
|
tokens;
|
|
620
680
|
fleet;
|
|
681
|
+
reservations;
|
|
621
682
|
constructor(options) {
|
|
622
683
|
if (options.apiKey !== undefined)
|
|
623
684
|
assertSecureBlackcatGatewayUrl(options.baseUrl);
|
|
@@ -628,6 +689,7 @@ class BlackcatClient extends GatewayClient {
|
|
|
628
689
|
this.sandboxes = new SandboxesResource(this);
|
|
629
690
|
this.tokens = new TokensResource(this);
|
|
630
691
|
this.fleet = new FleetResource(this);
|
|
692
|
+
this.reservations = new ReservationsResource(this);
|
|
631
693
|
}
|
|
632
694
|
withToken(token, renewCapability) {
|
|
633
695
|
const capability = typeof token === "string" ? undefined : token;
|
|
@@ -644,6 +706,7 @@ class BlackcatClient extends GatewayClient {
|
|
|
644
706
|
}
|
|
645
707
|
}
|
|
646
708
|
export {
|
|
709
|
+
parseSandboxBatch,
|
|
647
710
|
parseCreateSandboxResponse,
|
|
648
711
|
epochMillis,
|
|
649
712
|
assertSecureBlackcatGatewayUrl,
|
|
@@ -654,5 +717,6 @@ export {
|
|
|
654
717
|
MAX_EXEC_STDIN,
|
|
655
718
|
CAPABILITY_OPS,
|
|
656
719
|
BlackcatClient,
|
|
657
|
-
BlackcatApiError
|
|
720
|
+
BlackcatApiError,
|
|
721
|
+
BLACKCAT_ERROR_CODES
|
|
658
722
|
};
|