@gjermundgaraba/clankerbox-sdk 0.16.0 → 0.18.1
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/{client-DgDb-TR7.d.mts → client-8af586hb.d.mts} +292 -243
- package/dist/{client-CLemHDCl.mjs → client-JikMOSHo.mjs} +226 -165
- package/dist/index.d.mts +28 -17
- package/dist/index.mjs +61 -53
- package/dist/node.d.mts +6 -2
- package/dist/node.mjs +3 -3
- package/package.json +5 -4
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
3
3
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
4
|
-
import { Context, Duration, Effect, Fiber, Layer, Match, Predicate, Result, Schema, SchemaIssue } from "effect";
|
|
4
|
+
import { Context, Duration, Effect, Fiber, Layer, Match, Predicate, Record, Result, Schema, SchemaIssue } from "effect";
|
|
5
5
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
6
6
|
//#region src/errors.ts
|
|
7
|
-
/** A host sends none of these; the client library sets them. */
|
|
8
7
|
const located = {
|
|
9
8
|
host: Schema.optionalKey(Schema.String),
|
|
10
9
|
action: Schema.optionalKey(Schema.String),
|
|
11
|
-
id: Schema.optionalKey(Schema.String)
|
|
10
|
+
id: Schema.optionalKey(Schema.String),
|
|
11
|
+
made: Schema.optionalKey(Schema.String)
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
14
|
* The request is malformed, or the host list is one a client can't route by, or a config,
|
|
@@ -18,49 +18,80 @@ var Invalid = class extends Schema.TaggedError()("Invalid", {
|
|
|
18
18
|
message: Schema.String,
|
|
19
19
|
...located,
|
|
20
20
|
file: Schema.optionalKey(Schema.String)
|
|
21
|
-
}, { httpApiStatus: 400 }) {
|
|
22
|
-
get retryable() {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
21
|
+
}, { httpApiStatus: 400 }) {};
|
|
26
22
|
/** The machine or checkpoint doesn't exist. A `delete` that meets it is done. */
|
|
27
23
|
var NotFound = class extends Schema.TaggedError()("NotFound", {
|
|
28
24
|
message: Schema.String,
|
|
29
25
|
...located
|
|
30
|
-
}, { httpApiStatus: 404 }) {
|
|
31
|
-
get retryable() {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
26
|
+
}, { httpApiStatus: 404 }) {};
|
|
35
27
|
const ConflictKind = Schema.Literals(["exists", "busy"]);
|
|
36
28
|
/** The name is taken (`exists`), or another action holds the resource (`busy`). */
|
|
37
29
|
var Conflict = class extends Schema.TaggedError()("Conflict", {
|
|
38
30
|
message: Schema.String,
|
|
39
31
|
kind: ConflictKind,
|
|
40
32
|
...located
|
|
41
|
-
}, { httpApiStatus: 409 }) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
}, { httpApiStatus: 409 }) {};
|
|
34
|
+
/** No host offers `base`: the host a create names, or any in the client's list. */
|
|
35
|
+
var NotOffered = class extends Schema.TaggedError()("NotOffered", {
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
...located,
|
|
38
|
+
base: Schema.String
|
|
39
|
+
}, { httpApiStatus: 412 }) {};
|
|
40
|
+
/** `machine`'s create, fork or restore failed before it was made: it can only be deleted. */
|
|
41
|
+
var NotMade = class extends Schema.TaggedError()("NotMade", {
|
|
42
|
+
message: Schema.String,
|
|
43
|
+
...located,
|
|
44
|
+
machine: Schema.String
|
|
45
|
+
}, { httpApiStatus: 412 }) {};
|
|
46
|
+
/** The runtime no longer has `machine`: it can only be deleted. */
|
|
47
|
+
var Missing = class extends Schema.TaggedError()("Missing", {
|
|
48
|
+
message: Schema.String,
|
|
49
|
+
...located,
|
|
50
|
+
machine: Schema.String
|
|
51
|
+
}, { httpApiStatus: 412 }) {};
|
|
52
|
+
/** `checkpoint`'s capture failed, so nothing restores from it: it can only be deleted. */
|
|
53
|
+
var NotReady = class extends Schema.TaggedError()("NotReady", {
|
|
54
|
+
message: Schema.String,
|
|
55
|
+
...located,
|
|
56
|
+
checkpoint: Schema.String
|
|
57
|
+
}, { httpApiStatus: 412 }) {};
|
|
58
|
+
/** `checkpoint` holds RAM state saved under another runtime pin than the one the host runs. */
|
|
59
|
+
var PinMismatch = class extends Schema.TaggedError()("PinMismatch", {
|
|
60
|
+
message: Schema.String,
|
|
61
|
+
...located,
|
|
62
|
+
checkpoint: Schema.String
|
|
63
|
+
}, { httpApiStatus: 412 }) {};
|
|
64
|
+
/** `machine` is in `state`, which the action doesn't act from. */
|
|
65
|
+
var WrongState = class extends Schema.TaggedError()("WrongState", {
|
|
66
|
+
message: Schema.String,
|
|
67
|
+
...located,
|
|
68
|
+
machine: Schema.String,
|
|
69
|
+
state: Schema.String
|
|
70
|
+
}, { httpApiStatus: 412 }) {};
|
|
71
|
+
/** The runtime can't give the requested sizes, such as a disk below the base's. */
|
|
72
|
+
var Unsizable = class extends Schema.TaggedError()("Unsizable", {
|
|
48
73
|
message: Schema.String,
|
|
49
74
|
...located
|
|
50
|
-
}, { httpApiStatus:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
}, { httpApiStatus: 422 }) {};
|
|
76
|
+
/**
|
|
77
|
+
* A guest script exited non-zero or ran past its timeout; the message says which, with its
|
|
78
|
+
* output's tail. A failed `setup` leaves the machine unmade, a failed `preparation` made.
|
|
79
|
+
*/
|
|
80
|
+
var ScriptFailed = class extends Schema.TaggedError()("ScriptFailed", {
|
|
81
|
+
message: Schema.String,
|
|
82
|
+
...located,
|
|
83
|
+
script: Schema.Literals(["setup", "preparation"])
|
|
84
|
+
}, { httpApiStatus: 422 }) {};
|
|
85
|
+
/** The provider under the runtime refused the call for good, such as for its plan or API key. */
|
|
86
|
+
var Refused = class extends Schema.TaggedError()("Refused", {
|
|
87
|
+
message: Schema.String,
|
|
88
|
+
...located
|
|
89
|
+
}, { httpApiStatus: 403 }) {};
|
|
55
90
|
/** The host has no room now; room can free up, so a later call may succeed. */
|
|
56
91
|
var Capacity = class extends Schema.TaggedError()("Capacity", {
|
|
57
92
|
message: Schema.String,
|
|
58
93
|
...located
|
|
59
|
-
}, { httpApiStatus: 503 }) {
|
|
60
|
-
get retryable() {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
94
|
+
}, { httpApiStatus: 503 }) {};
|
|
64
95
|
/**
|
|
65
96
|
* No reply: only the client library makes it. A mutation's request may have reached the host
|
|
66
97
|
* and run, so it is retryable only when `readOnly`.
|
|
@@ -69,46 +100,64 @@ var Unavailable = class extends Schema.TaggedError()("Unavailable", {
|
|
|
69
100
|
message: Schema.String,
|
|
70
101
|
readOnly: Schema.Boolean,
|
|
71
102
|
...located
|
|
72
|
-
}) {
|
|
73
|
-
get retryable() {
|
|
74
|
-
return this.readOnly;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
103
|
+
}) {};
|
|
77
104
|
var Internal = class extends Schema.TaggedError()("Internal", {
|
|
78
105
|
message: Schema.String,
|
|
79
106
|
...located
|
|
80
|
-
}, { httpApiStatus: 500 }) {
|
|
81
|
-
get retryable() {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
};
|
|
107
|
+
}, { httpApiStatus: 500 }) {};
|
|
85
108
|
/** What a host may answer with; each action declares its own share. */
|
|
86
109
|
const hostErrors = [
|
|
87
110
|
Invalid,
|
|
88
111
|
NotFound,
|
|
89
112
|
Conflict,
|
|
90
|
-
|
|
113
|
+
NotOffered,
|
|
114
|
+
NotMade,
|
|
115
|
+
Missing,
|
|
116
|
+
NotReady,
|
|
117
|
+
PinMismatch,
|
|
118
|
+
WrongState,
|
|
119
|
+
Unsizable,
|
|
120
|
+
ScriptFailed,
|
|
121
|
+
Refused,
|
|
91
122
|
Capacity,
|
|
92
123
|
Internal
|
|
93
124
|
];
|
|
125
|
+
const HostError = Schema.Union(hostErrors);
|
|
126
|
+
/**
|
|
127
|
+
* Each carries, where known: `host` and `action`, which the client sets; `id`, the resource the
|
|
128
|
+
* error is about, which a host sets on `NotFound` and `Conflict` and the client sets to the call's
|
|
129
|
+
* subject otherwise; and `made`, the resource a create, fork, restore or capture makes, which a
|
|
130
|
+
* failure may leave behind.
|
|
131
|
+
*/
|
|
94
132
|
const ClankerboxError = Schema.Union([...hostErrors, Unavailable]);
|
|
95
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Whether the same call may succeed later: a busy resource or a full host may free up, and a read
|
|
135
|
+
* that got no reply may get one.
|
|
136
|
+
*/
|
|
137
|
+
const retryable = (error) => Match.value(error).pipe(Match.tagsExhaustive({
|
|
138
|
+
Invalid: () => false,
|
|
139
|
+
NotFound: () => false,
|
|
140
|
+
Conflict: ({ kind }) => kind === "busy",
|
|
141
|
+
NotOffered: () => false,
|
|
142
|
+
NotMade: () => false,
|
|
143
|
+
Missing: () => false,
|
|
144
|
+
NotReady: () => false,
|
|
145
|
+
PinMismatch: () => false,
|
|
146
|
+
WrongState: () => false,
|
|
147
|
+
Unsizable: () => false,
|
|
148
|
+
ScriptFailed: () => false,
|
|
149
|
+
Refused: () => false,
|
|
150
|
+
Capacity: () => true,
|
|
151
|
+
Unavailable: ({ readOnly }) => readOnly,
|
|
152
|
+
Internal: () => false
|
|
153
|
+
}));
|
|
96
154
|
//#endregion
|
|
97
155
|
//#region src/ids.ts
|
|
98
156
|
const separator = "_";
|
|
99
157
|
const maxIdLength = 62;
|
|
100
|
-
/** The ID is written to `/var/lib/clankerbox/machine-id`, and clankercreds accepts only this. */
|
|
101
158
|
const idPattern = /^[A-Za-z0-9_-]{1,62}$/u;
|
|
102
|
-
/**
|
|
103
|
-
* boat's named snapshots are `cbx-<host>-<inst>`, and boat allows `^[a-z0-9][a-z0-9-]{0,62}$`:
|
|
104
|
-
* a lowercase host ID of at most 32 characters keeps the name within 63.
|
|
105
|
-
*/
|
|
106
159
|
const hostIdPattern = /^[a-z][a-z0-9-]{0,31}$/u;
|
|
107
160
|
const hostIdRule = "a host ID is 1 to 32 lowercase letters, digits and '-', starting with a letter";
|
|
108
|
-
/**
|
|
109
|
-
* smolvm names a machine `<name>-<inst>` and refuses consecutive hyphens, so a name has no
|
|
110
|
-
* `--` and doesn't end in `-`.
|
|
111
|
-
*/
|
|
112
161
|
const namePattern = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/u;
|
|
113
162
|
const nameRule = "a name starts with a letter, then letters, digits and '-', with no '--' and no '-' at the end";
|
|
114
163
|
const HostId = Schema.String.check(Schema.isPattern(hostIdPattern, { message: hostIdRule }));
|
|
@@ -147,37 +196,31 @@ const Runtime = Schema.Literals([
|
|
|
147
196
|
"boat"
|
|
148
197
|
]);
|
|
149
198
|
const Size = Schema.Int.check(Schema.isGreaterThan(0));
|
|
150
|
-
/** The mutations
|
|
199
|
+
/** The contract's mutations, which record themselves on a resource's `action`. */
|
|
151
200
|
const ActionName = Schema.Literals([
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
201
|
+
"createMachine",
|
|
202
|
+
"startMachine",
|
|
203
|
+
"stopMachine",
|
|
204
|
+
"deleteMachine",
|
|
205
|
+
"forkMachine",
|
|
206
|
+
"restoreMachine",
|
|
207
|
+
"captureCheckpoint",
|
|
208
|
+
"deleteCheckpoint"
|
|
159
209
|
]);
|
|
160
210
|
const ActionStatus = Schema.Literals([
|
|
161
211
|
"running",
|
|
162
212
|
"failed",
|
|
163
213
|
"done"
|
|
164
214
|
]);
|
|
165
|
-
|
|
166
|
-
|
|
215
|
+
/** A resource's last action; the next action replaces it. */
|
|
216
|
+
const ActionRecord = Schema.Union([Schema.Struct({
|
|
217
|
+
name: ActionName,
|
|
167
218
|
status: Schema.Literals(["running", "done"])
|
|
168
219
|
}), Schema.Struct({
|
|
169
|
-
name,
|
|
220
|
+
name: ActionName,
|
|
170
221
|
status: Schema.Literal("failed"),
|
|
171
|
-
error:
|
|
172
|
-
tag: HostErrorTag,
|
|
173
|
-
message: Schema.String
|
|
174
|
-
})
|
|
222
|
+
error: HostError
|
|
175
223
|
})]);
|
|
176
|
-
/** A resource's last action; the next action replaces it. */
|
|
177
|
-
const ActionRecord = actionRecord(ActionName);
|
|
178
|
-
/** A checkpoint is only ever captured or deleted. */
|
|
179
|
-
const CheckpointActionName = Schema.Literals(["capture", "delete"]);
|
|
180
|
-
const CheckpointActionRecord = actionRecord(CheckpointActionName);
|
|
181
224
|
const SshEndpoint = Schema.Struct({
|
|
182
225
|
host: Schema.String,
|
|
183
226
|
port: Schema.Int.check(Schema.isBetween({
|
|
@@ -189,6 +232,12 @@ const SshLogin = Schema.Struct({
|
|
|
189
232
|
user: Schema.String,
|
|
190
233
|
...SshEndpoint.fields
|
|
191
234
|
});
|
|
235
|
+
/**
|
|
236
|
+
* `state` is `missing` when the runtime doesn't have the machine, and `unknown` when it couldn't
|
|
237
|
+
* be read. Until its create, fork or restore has `made` it, a machine can only be read, stopped or
|
|
238
|
+
* deleted. `ssh` is only on a running machine whose runtime gives it an endpoint, and `hostKey`,
|
|
239
|
+
* `<type> <base64>`, once preparation has recorded it.
|
|
240
|
+
*/
|
|
192
241
|
const Machine = Schema.Struct({
|
|
193
242
|
id: Id,
|
|
194
243
|
runtime: Runtime,
|
|
@@ -198,55 +247,50 @@ const Machine = Schema.Struct({
|
|
|
198
247
|
cpu: Size,
|
|
199
248
|
ramMib: Size,
|
|
200
249
|
diskGib: Size,
|
|
201
|
-
/** `missing`: the runtime doesn't have it; `unknown`: it couldn't be read. */
|
|
202
250
|
state: Schema.Literals([
|
|
203
251
|
"running",
|
|
204
252
|
"stopped",
|
|
205
253
|
"missing",
|
|
206
254
|
"unknown"
|
|
207
255
|
]),
|
|
208
|
-
/** Until its create, fork or restore has made it, it can only be read, stopped or deleted. */
|
|
209
256
|
made: Schema.Boolean,
|
|
210
257
|
action: ActionRecord,
|
|
211
|
-
/** Only on a running machine whose runtime gives it an endpoint. */
|
|
212
258
|
ssh: Schema.optionalKey(SshLogin),
|
|
213
|
-
/** `<type> <base64>`, once preparation has recorded it. */
|
|
214
259
|
hostKey: Schema.optionalKey(Schema.String)
|
|
215
260
|
}).annotate({ identifier: "Machine" });
|
|
261
|
+
/** The state as people read it: `running`, or `stopped (unmade)` for one never made. */
|
|
262
|
+
const stateLabel = ({ state, made }) => made ? state : `${state} (unmade)`;
|
|
263
|
+
/** `kind` is `ram` (RAM and disks) on smolvm, and `disk` on Tart and boat. */
|
|
216
264
|
const Checkpoint = Schema.Struct({
|
|
217
265
|
id: Id,
|
|
218
266
|
createdAt: Schema.DateTimeUtcFromString,
|
|
219
267
|
machine: Id,
|
|
220
|
-
/** `ram` (RAM and disks) on smolvm, `disk` on Tart and boat. */
|
|
221
268
|
kind: Schema.Literals(["ram", "disk"]),
|
|
222
269
|
base: Schema.String,
|
|
223
270
|
profile: Schema.optionalKey(Schema.String),
|
|
224
271
|
cpu: Size,
|
|
225
272
|
ramMib: Size,
|
|
226
273
|
diskGib: Size,
|
|
227
|
-
action:
|
|
274
|
+
action: ActionRecord
|
|
228
275
|
}).annotate({ identifier: "Checkpoint" });
|
|
276
|
+
/** A checkpoint restores once its capture is done; it never changes after. */
|
|
277
|
+
const isReady = (checkpoint) => checkpoint.action.status === "done";
|
|
278
|
+
/**
|
|
279
|
+
* `runtimeVersion` is boat's API version on boat. `captureFrom` is the state a machine must be in
|
|
280
|
+
* to be captured: Tart's is `stopped`.
|
|
281
|
+
*/
|
|
229
282
|
const Host = Schema.Struct({
|
|
230
283
|
id: HostId,
|
|
231
284
|
runtime: Runtime,
|
|
232
285
|
version: Schema.String,
|
|
233
|
-
/** For boat, its API version. */
|
|
234
286
|
runtimeVersion: Schema.String,
|
|
235
|
-
/** The state a machine must be in to be captured: Tart's is `stopped`. */
|
|
236
287
|
captureFrom: Schema.Literals(["running", "stopped"]),
|
|
237
288
|
bases: Schema.Array(Schema.String)
|
|
238
289
|
}).annotate({ identifier: "Host" });
|
|
239
|
-
|
|
240
|
-
* `used` is counted as a `Capacity` refusal counts it, so it needn't match the host's own
|
|
241
|
-
* machines.
|
|
242
|
-
*/
|
|
243
|
-
const Limit = Schema.Struct({
|
|
290
|
+
const usageFields = {
|
|
244
291
|
host: HostId,
|
|
245
|
-
/**
|
|
246
|
-
* `ramMib`: smolvm's RAM budget. `runningVms`: Tart's, Apple's two VMs per Mac.
|
|
247
|
-
* `activeSandboxes` and the `starts` of each rolling window: the boat account's.
|
|
248
|
-
*/
|
|
249
292
|
resource: Schema.Literals([
|
|
293
|
+
"cpu",
|
|
250
294
|
"ramMib",
|
|
251
295
|
"runningVms",
|
|
252
296
|
"activeSandboxes",
|
|
@@ -254,9 +298,24 @@ const Limit = Schema.Struct({
|
|
|
254
298
|
"startsPerHour",
|
|
255
299
|
"startsPerDay"
|
|
256
300
|
]),
|
|
257
|
-
limit: Schema.Int
|
|
301
|
+
limit: Schema.optionalKey(Schema.Int)
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* A resource of a host and how much of it is in use, counted as a refusal counts it, so it
|
|
305
|
+
* needn't match the host's own machines. When a machine the sum counts couldn't be read, the
|
|
306
|
+
* sum would be a guess, so the row names those machines instead.
|
|
307
|
+
*
|
|
308
|
+
* `cpu` and `ramMib` are what the host's running machines were given, `runningVms` Apple's two
|
|
309
|
+
* VMs per Mac, and `activeSandboxes` and the `starts` of each rolling window the boat account's.
|
|
310
|
+
* `limit` is there only where the host refuses an action that would pass it.
|
|
311
|
+
*/
|
|
312
|
+
const Usage = Schema.Union([Schema.Struct({
|
|
313
|
+
...usageFields,
|
|
258
314
|
used: Schema.Int
|
|
259
|
-
}).
|
|
315
|
+
}), Schema.Struct({
|
|
316
|
+
...usageFields,
|
|
317
|
+
unread: Schema.Array(Id)
|
|
318
|
+
})]).annotate({ identifier: "Usage" });
|
|
260
319
|
/** Nothing logs the script: it can carry secrets. */
|
|
261
320
|
const Setup = Schema.Struct({
|
|
262
321
|
script: Schema.String,
|
|
@@ -286,7 +345,6 @@ const FromMachine = Schema.Struct({
|
|
|
286
345
|
machine: Id,
|
|
287
346
|
name: Name
|
|
288
347
|
});
|
|
289
|
-
/** Every action is public, as a host listens only on the tailnet or loopback. */
|
|
290
348
|
const read = {
|
|
291
349
|
caller: Action.Anyone,
|
|
292
350
|
readOnly: true
|
|
@@ -295,7 +353,6 @@ const write = {
|
|
|
295
353
|
caller: Action.Anyone,
|
|
296
354
|
readOnly: false
|
|
297
355
|
};
|
|
298
|
-
/** What each action fails with; each has `Invalid`, which the host's version check answers. */
|
|
299
356
|
const listing = [Invalid, Internal];
|
|
300
357
|
const reading = [
|
|
301
358
|
Invalid,
|
|
@@ -306,10 +363,16 @@ const changing = [
|
|
|
306
363
|
Invalid,
|
|
307
364
|
NotFound,
|
|
308
365
|
Conflict,
|
|
309
|
-
|
|
366
|
+
Refused,
|
|
310
367
|
Internal
|
|
311
368
|
];
|
|
312
|
-
const
|
|
369
|
+
const booting = [
|
|
370
|
+
...changing,
|
|
371
|
+
NotMade,
|
|
372
|
+
Missing,
|
|
373
|
+
WrongState,
|
|
374
|
+
Capacity
|
|
375
|
+
];
|
|
313
376
|
const Http = ActionHttp.make([
|
|
314
377
|
Action.make("listMachines", {
|
|
315
378
|
...read,
|
|
@@ -326,14 +389,23 @@ const Http = ActionHttp.make([
|
|
|
326
389
|
}),
|
|
327
390
|
Action.make("createMachine", {
|
|
328
391
|
...write,
|
|
329
|
-
error:
|
|
392
|
+
error: [
|
|
393
|
+
Invalid,
|
|
394
|
+
Conflict,
|
|
395
|
+
NotOffered,
|
|
396
|
+
Unsizable,
|
|
397
|
+
ScriptFailed,
|
|
398
|
+
Refused,
|
|
399
|
+
Capacity,
|
|
400
|
+
Internal
|
|
401
|
+
],
|
|
330
402
|
description: "Create a machine from a base, run its setup once, and prepare it.",
|
|
331
403
|
input: CreateRequest,
|
|
332
404
|
success: Machine
|
|
333
405
|
}),
|
|
334
406
|
Action.make("startMachine", {
|
|
335
407
|
...write,
|
|
336
|
-
error:
|
|
408
|
+
error: [...booting, ScriptFailed],
|
|
337
409
|
description: "Start a machine. On a running machine, run preparation again.",
|
|
338
410
|
input: ById,
|
|
339
411
|
success: Machine
|
|
@@ -353,14 +425,25 @@ const Http = ActionHttp.make([
|
|
|
353
425
|
}),
|
|
354
426
|
Action.make("forkMachine", {
|
|
355
427
|
...write,
|
|
356
|
-
error:
|
|
428
|
+
error: [
|
|
429
|
+
...booting,
|
|
430
|
+
Unsizable,
|
|
431
|
+
ScriptFailed
|
|
432
|
+
],
|
|
357
433
|
description: "Copy a machine to a new name on the same host.",
|
|
358
434
|
input: FromMachine,
|
|
359
435
|
success: Machine
|
|
360
436
|
}),
|
|
361
437
|
Action.make("restoreMachine", {
|
|
362
438
|
...write,
|
|
363
|
-
error:
|
|
439
|
+
error: [
|
|
440
|
+
...changing,
|
|
441
|
+
NotReady,
|
|
442
|
+
PinMismatch,
|
|
443
|
+
Unsizable,
|
|
444
|
+
ScriptFailed,
|
|
445
|
+
Capacity
|
|
446
|
+
],
|
|
364
447
|
description: "Create a machine on the checkpoint's host from that checkpoint.",
|
|
365
448
|
input: Schema.Struct({
|
|
366
449
|
checkpoint: Id,
|
|
@@ -383,7 +466,7 @@ const Http = ActionHttp.make([
|
|
|
383
466
|
}),
|
|
384
467
|
Action.make("captureCheckpoint", {
|
|
385
468
|
...write,
|
|
386
|
-
error:
|
|
469
|
+
error: booting,
|
|
387
470
|
description: "Capture a checkpoint of a machine. Its kind follows the runtime: ram on smolvm, which captures only a running machine, and disk on Tart and boat.",
|
|
388
471
|
input: FromMachine,
|
|
389
472
|
success: Checkpoint
|
|
@@ -404,27 +487,24 @@ const Http = ActionHttp.make([
|
|
|
404
487
|
...read,
|
|
405
488
|
error: [
|
|
406
489
|
Invalid,
|
|
407
|
-
|
|
490
|
+
Refused,
|
|
408
491
|
Internal
|
|
409
492
|
],
|
|
410
|
-
description: "Read the host's
|
|
411
|
-
success: Schema.Array(
|
|
493
|
+
description: "Read the host's resources, how much of each is in use, and their limits. A machine whose state can't be read leaves the rows that count it unread; it fails when the runtime can't be read at all.",
|
|
494
|
+
success: Schema.Array(Usage)
|
|
412
495
|
})
|
|
413
496
|
], { prefix: "/api" });
|
|
414
497
|
const actionsByName = Action.byName(Http.actions);
|
|
415
498
|
//#endregion
|
|
416
499
|
//#region src/release.ts
|
|
417
500
|
/** Also the version of the clankerbox binaries it ships with. */
|
|
418
|
-
const version = "0.
|
|
501
|
+
const version = "0.18.1";
|
|
419
502
|
/** major.minor: releases that share it speak the same API. */
|
|
420
503
|
const release = (of) => of.split(".").slice(0, 2).join(".");
|
|
421
504
|
/** A host refuses a request whose `release` differs from its own, or that has none. */
|
|
422
505
|
const versionHeader = "clankerbox-version";
|
|
423
506
|
//#endregion
|
|
424
|
-
//#region src/
|
|
425
|
-
const mibPerGib = 1024;
|
|
426
|
-
/** Exactly, as a size flag takes it: `32G`, `1.5G`, `512M`, `1500M`. */
|
|
427
|
-
const formatMib = (mib) => mib >= 1024 && mib % 512 === 0 ? `${mib / mibPerGib}G` : `${mib}M`;
|
|
507
|
+
//#region src/describe.ts
|
|
428
508
|
const jsonPath = (path, names) => path.map((key, index) => {
|
|
429
509
|
if (Number.isInteger(key)) return `[${String(key)}]`;
|
|
430
510
|
return index === 0 ? names?.get(key) ?? String(key) : `.${String(key)}`;
|
|
@@ -462,10 +542,6 @@ const HostEntry = Schema.Struct({
|
|
|
462
542
|
* reads the resource to see what happened.
|
|
463
543
|
*/
|
|
464
544
|
var Client = class extends Context.Service()("@gjermundgaraba/clankerbox-sdk/Client") {};
|
|
465
|
-
/**
|
|
466
|
-
* Input that doesn't encode fails as `InvalidInput` before anything is sent, so a `SchemaError`
|
|
467
|
-
* that reaches `settle` is a reply that didn't decode.
|
|
468
|
-
*/
|
|
469
545
|
const hostApi = (entry) => ActionHttp.client(Http, {
|
|
470
546
|
baseUrl: entry.url,
|
|
471
547
|
transformClient: HttpClient.mapRequest(HttpClientRequest.setHeader(versionHeader, version))
|
|
@@ -498,13 +574,18 @@ const settle = (call, context) => bounded(call, context).pipe(Effect.catchTags({
|
|
|
498
574
|
}));
|
|
499
575
|
const encodeError = Schema.encodeSync(ClankerboxError);
|
|
500
576
|
const decodeError = Schema.decodeSync(ClankerboxError);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
577
|
+
const locate = ({ action, host, id, made }) => (error) => decodeError({
|
|
578
|
+
...Record.filter({
|
|
579
|
+
action,
|
|
580
|
+
host,
|
|
581
|
+
id,
|
|
582
|
+
made
|
|
583
|
+
}, Predicate.isNotUndefined),
|
|
584
|
+
...encodeError(error)
|
|
505
585
|
});
|
|
506
586
|
const checkHost = (entry, host) => host.id === entry.id ? Effect.succeed(host) : Effect.fail(new Invalid({ message: `host ${entry.id} (${entry.url}) calls itself ${host.id}; its entry in the host list must use that ID` }));
|
|
507
587
|
const decodeHosts = Schema.decodeUnknownEffect(Schema.Array(HostEntry));
|
|
588
|
+
const subjectOf = (input) => "id" in input ? input.id : "machine" in input ? input.machine : input.checkpoint;
|
|
508
589
|
/**
|
|
509
590
|
* A client over `hosts`, in placement order. It sends nothing until called. An empty list, a
|
|
510
591
|
* malformed entry or a host ID that appears twice is Invalid.
|
|
@@ -523,11 +604,7 @@ const make = (hosts, options) => Effect.gen(function* () {
|
|
|
523
604
|
const found = routes.get(host);
|
|
524
605
|
return found === void 0 ? Effect.fail(new Invalid({ message: `host ${host} isn't in the host list` })) : Effect.succeed(found);
|
|
525
606
|
};
|
|
526
|
-
|
|
527
|
-
* A call of `name` on one host, about `subject`. Its errors carry the host, the action and
|
|
528
|
-
* `target`, the resource it reads, changes or makes.
|
|
529
|
-
*/
|
|
530
|
-
const send = ({ entry, api }, name, input, subject, target = subject) => {
|
|
607
|
+
const send = ({ entry, api }, name, input, subject, made) => {
|
|
531
608
|
const { readOnly } = actionsByName[name];
|
|
532
609
|
const method = api[name];
|
|
533
610
|
return settle(method(input), {
|
|
@@ -535,22 +612,14 @@ const make = (hosts, options) => Effect.gen(function* () {
|
|
|
535
612
|
timeout: readOnly ? options?.readTimeout ?? readTimeout : options?.timeout,
|
|
536
613
|
readOnly,
|
|
537
614
|
what: subject === void 0 ? name : `${name} ${subject}`,
|
|
538
|
-
target
|
|
539
|
-
}).pipe(Effect.mapError(locate(
|
|
540
|
-
action: name,
|
|
541
|
-
host: entry.id
|
|
542
|
-
} : {
|
|
615
|
+
target: made ?? subject
|
|
616
|
+
}).pipe(Effect.mapError(locate({
|
|
543
617
|
action: name,
|
|
544
618
|
host: entry.id,
|
|
545
|
-
id:
|
|
619
|
+
id: subject,
|
|
620
|
+
made
|
|
546
621
|
})));
|
|
547
622
|
};
|
|
548
|
-
/**
|
|
549
|
-
* A call of `name` on the host of `id`. A fork, restore or capture names the one it makes as
|
|
550
|
-
* `made`, which its errors then carry, so a caller can find what a failure left behind.
|
|
551
|
-
*/
|
|
552
|
-
const byId = (name, id, input, made) => Effect.flatMap(Effect.flatMap(parseId(id), ({ host }) => route(host)), (found) => send(found, name, input, id, made ?? id));
|
|
553
|
-
/** An error raised before the call is sent names its action too. */
|
|
554
623
|
const naming = (name) => (effect) => Effect.mapError(effect, (error) => error.action === void 0 ? locate({ action: name })(error) : error);
|
|
555
624
|
const gather = (call) => Effect.forEach(routed, (route) => Effect.map(Effect.result(call(route)), (result) => ({
|
|
556
625
|
host: route.entry.id,
|
|
@@ -572,18 +641,16 @@ const make = (hosts, options) => Effect.gen(function* () {
|
|
|
572
641
|
action: "getHost",
|
|
573
642
|
host: route.entry.id
|
|
574
643
|
})));
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
* host gave no reply: otherwise asking again changes nothing, and it is `Precondition`.
|
|
586
|
-
*/
|
|
644
|
+
const onResource = (name, input, makes) => naming(name)(Effect.gen(function* () {
|
|
645
|
+
const subject = subjectOf(input);
|
|
646
|
+
const { host } = yield* parseId(subject);
|
|
647
|
+
const made = makes === void 0 ? void 0 : yield* formatId(host, makes);
|
|
648
|
+
return yield* send(yield* route(host), name, input, subject, made);
|
|
649
|
+
}));
|
|
650
|
+
const createOn = (host, name, spec) => Effect.flatMap(formatId(host, name), (id) => onResource("createMachine", {
|
|
651
|
+
...spec,
|
|
652
|
+
id
|
|
653
|
+
}, name));
|
|
587
654
|
const offering = (base) => Effect.gen(function* () {
|
|
588
655
|
const asked = yield* Effect.forEach(routed, (route) => Effect.map(Effect.forkScoped(Effect.result(readHost(route))), (fiber) => ({
|
|
589
656
|
host: route.entry.id,
|
|
@@ -606,7 +673,10 @@ const make = (hosts, options) => Effect.gen(function* () {
|
|
|
606
673
|
return yield* noReply ? new Unavailable({
|
|
607
674
|
message,
|
|
608
675
|
readOnly: true
|
|
609
|
-
}) : new
|
|
676
|
+
}) : new NotOffered({
|
|
677
|
+
message,
|
|
678
|
+
base
|
|
679
|
+
});
|
|
610
680
|
}).pipe(Effect.scoped);
|
|
611
681
|
const create = (name, spec, options) => Effect.gen(function* () {
|
|
612
682
|
yield* parseName(name);
|
|
@@ -614,34 +684,25 @@ const make = (hosts, options) => Effect.gen(function* () {
|
|
|
614
684
|
return yield* createOn((yield* offering(spec.base)).id, name, spec);
|
|
615
685
|
});
|
|
616
686
|
const place = (base, options) => options?.host === void 0 ? offering(base) : Effect.flatMap(route(options.host), readHost);
|
|
617
|
-
const
|
|
687
|
+
const deleting = (removal) => removal.pipe(Effect.as("deleted"), Effect.catchTag("NotFound", () => Effect.succeed("absent")));
|
|
618
688
|
return {
|
|
619
689
|
hosts: gather((route) => Effect.map(readHost(route), (host) => [host])),
|
|
620
690
|
machines: gather((route) => send(route, "listMachines", {})),
|
|
621
691
|
checkpoints: gather((route) => send(route, "listCheckpoints", {})),
|
|
622
692
|
capacity: gather((route) => send(route, "getCapacity", {})),
|
|
623
|
-
machine: (id) => naming("getMachine")(byId("getMachine", id, { id })),
|
|
624
|
-
checkpoint: (id) => naming("getCheckpoint")(byId("getCheckpoint", id, { id })),
|
|
625
693
|
place: (base, options) => naming("getHost")(place(base, options)),
|
|
626
694
|
create: (name, spec, options) => naming("createMachine")(create(name, spec, options)),
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
name
|
|
637
|
-
}, made))),
|
|
638
|
-
capture: (machine, name) => naming("captureCheckpoint")(Effect.flatMap(sibling(machine, name), (made) => byId("captureCheckpoint", machine, {
|
|
639
|
-
machine,
|
|
640
|
-
name
|
|
641
|
-
}, made))),
|
|
642
|
-
deleteCheckpoint: (id) => naming("deleteCheckpoint")(byId("deleteCheckpoint", id, { id }))
|
|
695
|
+
getMachine: (input) => onResource("getMachine", input),
|
|
696
|
+
startMachine: (input) => onResource("startMachine", input),
|
|
697
|
+
stopMachine: (input) => onResource("stopMachine", input),
|
|
698
|
+
deleteMachine: (input) => deleting(onResource("deleteMachine", input)),
|
|
699
|
+
forkMachine: (input) => onResource("forkMachine", input, input.name),
|
|
700
|
+
restoreMachine: (input) => onResource("restoreMachine", input, input.name),
|
|
701
|
+
getCheckpoint: (input) => onResource("getCheckpoint", input),
|
|
702
|
+
captureCheckpoint: (input) => onResource("captureCheckpoint", input, input.name),
|
|
703
|
+
deleteCheckpoint: (input) => deleting(onResource("deleteCheckpoint", input))
|
|
643
704
|
};
|
|
644
705
|
});
|
|
645
706
|
const layer = (hosts, options) => Layer.effect(Client, make(hosts, options));
|
|
646
707
|
//#endregion
|
|
647
|
-
export {
|
|
708
|
+
export { parseId as A, NotFound as B, Usage as C, Id as D, HostId as E, ConflictKind as F, Refused as G, NotOffered as H, HostError as I, Unsizable as J, ScriptFailed as K, Internal as L, Capacity as M, ClankerboxError as N, Name as O, Conflict as P, Invalid as R, SshLogin as S, stateLabel as T, NotReady as U, NotMade as V, PinMismatch as W, hostErrors as X, WrongState as Y, retryable as Z, Machine as _, release as a, Setup as b, Http as c, ActionName as d, ActionRecord as f, Host as g, CreateRequest as h, describeIssues as i, parseName as j, formatId as k, actionsByName as l, Checkpoint as m, layer as n, version as o, ActionStatus as p, Unavailable as q, describeFileError as r, versionHeader as s, client_exports as t, readTimeout as u, MachineSpec as v, isReady as w, SshEndpoint as x, Runtime as y, Missing as z };
|