@intentius/chant-lexicon-fly 0.124.67 → 0.124.69
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 +1 -1
- package/dist/components/fly-release.d.ts +22 -3
- package/dist/components/fly-release.d.ts.map +1 -1
- package/dist/components/fly-release.js +86 -38
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +8 -3
- package/dist/integrity.json +6 -6
- package/dist/lint/audit-catalog.js +1 -1
- package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts +4 -0
- package/dist/lint/post-synth/fly011-mount-references-declared-volume.d.ts.map +1 -1
- package/dist/lint/post-synth/fly011-mount-references-declared-volume.js +10 -2
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -1
- package/dist/okf/rules/FLY011.md +2 -2
- package/dist/okf/types/Machine.md +1 -1
- package/dist/okf/types/Volume.md +1 -1
- package/dist/op/activities/fly-apply.d.ts +91 -1
- package/dist/op/activities/fly-apply.d.ts.map +1 -1
- package/dist/op/activities/fly-apply.js +165 -7
- package/dist/op/activities/machine-release.d.ts +105 -4
- package/dist/op/activities/machine-release.d.ts.map +1 -1
- package/dist/op/activities/machine-release.js +147 -8
- package/dist/op/activities/machines-fake.d.ts +30 -0
- package/dist/op/activities/machines-fake.d.ts.map +1 -1
- package/dist/op/activities/machines-fake.js +60 -3
- package/dist/rules/fly011-mount-references-declared-volume.ts +9 -2
- package/dist/skills/chant-fly-ops.md +26 -2
- package/dist/skills/chant-fly-patterns.md +4 -2
- package/dist/skills/chant-fly.md +1 -1
- package/dist/volume-id.d.ts +9 -0
- package/dist/volume-id.d.ts.map +1 -0
- package/dist/volume-id.js +10 -0
- package/package.json +3 -3
- package/src/components/fly-release.test.ts +231 -0
- package/src/components/fly-release.ts +106 -39
- package/src/describe-resources.test.ts +27 -0
- package/src/describe-resources.ts +9 -2
- package/src/lint/audit-catalog.ts +1 -1
- package/src/lint/post-synth/fly011-mount-references-declared-volume.test.ts +31 -0
- package/src/lint/post-synth/fly011-mount-references-declared-volume.ts +9 -2
- package/src/op/activities/fly-apply.test.ts +170 -2
- package/src/op/activities/fly-apply.ts +229 -9
- package/src/op/activities/machine-release.test.ts +175 -1
- package/src/op/activities/machine-release.ts +222 -12
- package/src/op/activities/machines-fake.ts +89 -3
- package/src/op/activities/machines-local.test.ts +1 -0
- package/src/skills/chant-fly-ops.md +26 -2
- package/src/skills/chant-fly-patterns.md +4 -2
- package/src/skills/chant-fly.md +1 -1
- package/src/volume-id.ts +11 -0
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
* release says so), and report the release and config it replaced. A
|
|
20
20
|
* scheduled Machine (`config.schedule`, #4696) is updated without starting:
|
|
21
21
|
* it runs on its schedule. The plan's other Machines can take images of
|
|
22
|
-
* their own (`images`).
|
|
22
|
+
* their own (`images`). Given `checks` (#4719), each Machine it starts must
|
|
23
|
+
* pass its Fly checks before the next is updated, so a group of serving
|
|
24
|
+
* Machines rolls one at a time.
|
|
23
25
|
* - {@link flyMachineExec}: run a command inside the Machine (a migration).
|
|
24
26
|
* - {@link flyMachineRestart}, {@link flyMachineStop}: under a lease, then
|
|
25
27
|
* wait for the Machine to settle.
|
|
26
28
|
* - {@link flyMachineVerify}: the Machine is started with this release in its
|
|
27
|
-
* metadata, and, given a URL, its health endpoint answers.
|
|
29
|
+
* metadata, and, given a URL, its health endpoint answers. With `checks`,
|
|
30
|
+
* its Fly checks pass too.
|
|
31
|
+
* - {@link flyMachineChecks}: the Machine is started and its Fly checks pass.
|
|
28
32
|
* - {@link flyMachineRestore}: put a recorded Machine config back (restore on
|
|
29
33
|
* a failed release, and rollback).
|
|
30
34
|
*
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
* (`FLY_FLAPS_BASE_URL`, `FLY_API_TOKEN`), so the same code reaches mudflaps
|
|
33
37
|
* or a real Fly org, and an injectable `http` for tests (./machines-fake.ts).
|
|
34
38
|
*/
|
|
35
|
-
import { appNameFromRequest, defaultFlyHttp, flyApply, isAppRequest, isMachineRequest, listMachines, machineAppSegment, resolveApp, resolveEndpoint, skipsLaunch, waitForMachine, waitForUnlaunched, withLease, LEASE_NONCE_HEADER, } from "./fly-apply.js";
|
|
39
|
+
import { appNameFromRequest, configEqual, defaultFlyHttp, flyApply, isAppRequest, isMachineRequest, listMachines, machineAppSegment, resolveApp, resolveEndpoint, resolveMounts, skipsLaunch, waitForMachine, waitForUnlaunched, withLease, LEASE_NONCE_HEADER, } from "./fly-apply.js";
|
|
36
40
|
import { parsePlan } from "./fly-apply.js";
|
|
37
41
|
import { readFileSync } from "node:fs";
|
|
38
42
|
import { readMachineRelease, withReleaseMetadata } from "../../release-metadata.js";
|
|
@@ -77,6 +81,66 @@ async function mustFindMachine(ctx, app, name, http, signal) {
|
|
|
77
81
|
return m;
|
|
78
82
|
}
|
|
79
83
|
const machinePath = (ctx, app, id) => `${ctx.base}/v1/apps/${encodeURIComponent(app)}/machines/${encodeURIComponent(id)}`;
|
|
84
|
+
/** Five minutes, as `fly deploy`'s `--wait-timeout`. */
|
|
85
|
+
export const DEFAULT_CHECKS_TIMEOUT_MS = 300000;
|
|
86
|
+
/**
|
|
87
|
+
* True when a Machine config declares a Fly check: one under `checks`, or
|
|
88
|
+
* one on any of its `services`. Pure.
|
|
89
|
+
*/
|
|
90
|
+
export function declaresChecks(config) {
|
|
91
|
+
const c = (config ?? {});
|
|
92
|
+
if (c.checks && typeof c.checks === "object" && Object.keys(c.checks).length > 0)
|
|
93
|
+
return true;
|
|
94
|
+
const services = Array.isArray(c.services) ? c.services : [];
|
|
95
|
+
return services.some((svc) => Array.isArray(svc?.checks) && svc.checks.length > 0);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Why a Machine's Fly checks are not all passing, or undefined when they are
|
|
99
|
+
* or it has none. A Machine whose config declares checks that have not
|
|
100
|
+
* reported yet is not passing.
|
|
101
|
+
*
|
|
102
|
+
* Given `since`, the Machine's own `updated_at` as the call that changed it
|
|
103
|
+
* answered it, a check counts as passing only when its `updated_at` is at or
|
|
104
|
+
* after that time: a `passing` left over from before the change is stale.
|
|
105
|
+
* Both times come from flaps, so the local clock plays no part. A check
|
|
106
|
+
* that reports no `updated_at` cannot be told stale and counts on its status.
|
|
107
|
+
* Pure.
|
|
108
|
+
*/
|
|
109
|
+
export function checksPending(m, since) {
|
|
110
|
+
const checks = m.checks ?? [];
|
|
111
|
+
if (checks.length === 0)
|
|
112
|
+
return declaresChecks(m.config) ? "no check has reported yet" : undefined;
|
|
113
|
+
const from = since ? Date.parse(since) : NaN;
|
|
114
|
+
const stale = (c) => {
|
|
115
|
+
const at = c.updated_at ? Date.parse(c.updated_at) : NaN;
|
|
116
|
+
return !Number.isNaN(from) && !Number.isNaN(at) && at < from;
|
|
117
|
+
};
|
|
118
|
+
const failing = checks.filter((c) => c.status !== "passing" || stale(c));
|
|
119
|
+
if (failing.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return failing
|
|
122
|
+
.map((c) => {
|
|
123
|
+
if (c.status === "passing")
|
|
124
|
+
return `check ${c.name} has not reported since the update at ${since} (last at ${c.updated_at})`;
|
|
125
|
+
const output = (c.output ?? "").trim().slice(0, 200);
|
|
126
|
+
return `check ${c.name} is ${c.status || "unknown"}${output ? ` (${output})` : ""}`;
|
|
127
|
+
})
|
|
128
|
+
.join("; ");
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A release that failed partway through its apply (#4719): a Machine that
|
|
132
|
+
* did not settle or whose checks did not pass. `applied` holds the Machines
|
|
133
|
+
* it applied before the failure, the failing one included when its update
|
|
134
|
+
* went through, so the caller can put each back.
|
|
135
|
+
*/
|
|
136
|
+
export class MachineReleaseError extends Error {
|
|
137
|
+
constructor(reason, applied) {
|
|
138
|
+
super(reason instanceof Error ? reason.message : String(reason));
|
|
139
|
+
this.name = "MachineReleaseError";
|
|
140
|
+
this.applied = applied;
|
|
141
|
+
this.reason = reason;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
80
144
|
/**
|
|
81
145
|
* Upload and start: apply the declared plan (the App, its Volumes, IPs,
|
|
82
146
|
* Secrets) with the Machine serving `release`. The Machine is updated in place
|
|
@@ -88,6 +152,11 @@ const machinePath = (ctx, app, id) => `${ctx.base}/v1/apps/${encodeURIComponent(
|
|
|
88
152
|
* another in the plan, is created or updated with `skip_launch` and left
|
|
89
153
|
* unstarted: Fly starts it on its schedule. Fly counts the interval from the
|
|
90
154
|
* update, so each release pushes the next run back by up to one interval.
|
|
155
|
+
*
|
|
156
|
+
* Given `checks` (#4719), each Machine the apply starts must pass its Fly
|
|
157
|
+
* checks, reported since its update, before the next one is updated. With or
|
|
158
|
+
* without them, a failure partway through the apply throws a
|
|
159
|
+
* {@link MachineReleaseError} naming the Machines it changed so far.
|
|
91
160
|
*/
|
|
92
161
|
export async function flyMachineRelease(args, signal, http = defaultFlyHttp(args.token)) {
|
|
93
162
|
const plan = args.plan ?? parsePlan(readFileSync(requirePlanPath(args.planPath), "utf8"));
|
|
@@ -134,7 +203,36 @@ export async function flyMachineRelease(args, signal, http = defaultFlyHttp(args
|
|
|
134
203
|
const found = await findMachine(ctx, m.app, m.name, http, signal).catch(() => undefined);
|
|
135
204
|
before.set(m.entity, found ? (found.config ?? {}) : null);
|
|
136
205
|
}
|
|
137
|
-
const
|
|
206
|
+
const rolled = [];
|
|
207
|
+
const afterMachine = async (m) => {
|
|
208
|
+
const serving = m.entity === target.entity;
|
|
209
|
+
const previous = serving ? (live ? (liveConfig ?? {}) : null) : (before.get(m.entity) ?? null);
|
|
210
|
+
rolled.push({ app: m.app, name: m.name, id: m.id, action: m.action, unlaunched: m.unlaunched, previous, serving, ...(m.updatedAt ? { updatedAt: m.updatedAt } : {}) });
|
|
211
|
+
if (args.checks && m.action !== "noop" && !m.unlaunched) {
|
|
212
|
+
await waitForChecks(ctx, m.app, m.name, http, signal, args.checks, m.updatedAt);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
let applied;
|
|
216
|
+
try {
|
|
217
|
+
applied = await flyApply({ plan: { ...plan, ...overrides, [target.entity]: request }, endpoint: args.endpoint, token: args.token, wait: args.wait, afterMachine }, signal, http);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// A Machine whose update went through but which never settled is not in
|
|
221
|
+
// `rolled` yet: find it by its config, so it is put back too.
|
|
222
|
+
const reached = new Set(rolled.map((m) => `${m.app}/${m.name}`));
|
|
223
|
+
for (const m of planMachines(plan)) {
|
|
224
|
+
if (reached.has(`${m.app}/${m.name}`))
|
|
225
|
+
continue;
|
|
226
|
+
const serving = m.entity === target.entity;
|
|
227
|
+
const was = serving ? (live ? (liveConfig ?? {}) : null) : (before.get(m.entity) ?? null);
|
|
228
|
+
const now = await findMachine(ctx, m.app, m.name, http, signal).catch(() => undefined);
|
|
229
|
+
if (!now || (was && configEqual(now.config, was)))
|
|
230
|
+
continue;
|
|
231
|
+
const unlaunched = skipsLaunch((serving ? request : (overrides[m.entity] ?? m.request)).body);
|
|
232
|
+
rolled.push({ app: m.app, name: m.name, id: now.id, action: was ? "updated" : "created", unlaunched, previous: was, serving });
|
|
233
|
+
}
|
|
234
|
+
throw new MachineReleaseError(err, rolled);
|
|
235
|
+
}
|
|
138
236
|
const actionOf = (app, name) => applied.machines.find((m) => m.app === app && m.name === name)?.action ?? "noop";
|
|
139
237
|
const now = await mustFindMachine(ctx, target.app, target.name, http, signal);
|
|
140
238
|
return {
|
|
@@ -152,6 +250,7 @@ export async function flyMachineRelease(args, signal, http = defaultFlyHttp(args
|
|
|
152
250
|
unlaunched: skipsLaunch((overrides[m.entity] ?? m.request).body),
|
|
153
251
|
previous: before.get(m.entity) ?? null,
|
|
154
252
|
})),
|
|
253
|
+
applied: rolled,
|
|
155
254
|
};
|
|
156
255
|
}
|
|
157
256
|
function requirePlanPath(planPath) {
|
|
@@ -230,6 +329,11 @@ export async function flyMachineVerify(args, signal, http = defaultFlyHttp(args.
|
|
|
230
329
|
if (release?.digest !== args.digest) {
|
|
231
330
|
throw new Error(`machine ${args.app}/${m.id} serves ${release?.digest ?? "no release"}, expected ${args.digest}`);
|
|
232
331
|
}
|
|
332
|
+
if (args.checks && m.state === "started") {
|
|
333
|
+
const pending = checksPending(m, args.checksSince);
|
|
334
|
+
if (pending)
|
|
335
|
+
throw new Error(`machine ${args.app}/${args.machine} (${m.id}): ${pending}`);
|
|
336
|
+
}
|
|
233
337
|
if (args.url) {
|
|
234
338
|
const url = `${args.url.replace(/\/$/, "")}${args.healthPath ?? "/health"}`;
|
|
235
339
|
const res = await fetchImpl(url, { signal: AbortSignal.timeout(5000) });
|
|
@@ -262,27 +366,62 @@ export async function flyMachineVerify(args, signal, http = defaultFlyHttp(args.
|
|
|
262
366
|
* under a lease and wait for it to be started. Used on a failed release (the
|
|
263
367
|
* config it replaced) and by a rollback (the config an earlier release applied).
|
|
264
368
|
* A scheduled config (#4696) goes back with `skip_launch`, and the wait is for
|
|
265
|
-
* the Machine to rest unstarted.
|
|
369
|
+
* the Machine to rest unstarted. A mount that names a volume goes back with
|
|
370
|
+
* the volume's id, looked up as `flyApply` looks it up (#4717); a mount with an
|
|
371
|
+
* id (a config read from the live Machine) goes back as it is.
|
|
266
372
|
*/
|
|
267
373
|
export async function flyMachineRestore(args, signal, http = defaultFlyHttp(args.token)) {
|
|
268
374
|
const ctx = { base: resolveEndpoint(args) };
|
|
269
375
|
const m = await mustFindMachine(ctx, args.app, args.machine, http, signal);
|
|
270
376
|
const unlaunched = skipsLaunch({ config: args.config });
|
|
271
|
-
const
|
|
377
|
+
const config = await resolveMounts(ctx, args.app, args.config, { machineId: m.id, region: m.region }, http, signal);
|
|
378
|
+
const body = { name: m.name, config, ...(unlaunched ? { skip_launch: true } : {}) };
|
|
272
379
|
const res = await withLease(ctx, args.app, m.id, http, signal, (nonce) => http("POST", machinePath(ctx, args.app, m.id), body, { [LEASE_NONCE_HEADER]: nonce }, signal));
|
|
273
380
|
if (res.status >= 300)
|
|
274
381
|
throw new Error(`restore of machine ${args.app}/${m.id} failed (${res.status}): ${res.text}`);
|
|
275
382
|
let instance = "";
|
|
383
|
+
let updatedAt;
|
|
276
384
|
try {
|
|
277
|
-
|
|
385
|
+
const answered = JSON.parse(res.text);
|
|
386
|
+
instance = answered.instance_id ?? "";
|
|
387
|
+
updatedAt = answered.updated_at;
|
|
278
388
|
}
|
|
279
389
|
catch {
|
|
280
390
|
instance = "";
|
|
281
391
|
}
|
|
282
392
|
if (unlaunched)
|
|
283
393
|
await waitForUnlaunched(ctx, args.app, m.id, instance, http, signal, args.wait ?? {});
|
|
284
|
-
else
|
|
394
|
+
else {
|
|
285
395
|
await waitForMachine(ctx, args.app, m.id, instance, http, signal, args.wait ?? {});
|
|
396
|
+
if (args.checks)
|
|
397
|
+
await waitForChecks(ctx, args.app, args.machine, http, signal, args.checks, updatedAt);
|
|
398
|
+
}
|
|
286
399
|
const release = readMachineRelease(args.config.metadata);
|
|
287
400
|
return { id: m.id, ...(release ? { release } : {}) };
|
|
288
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Wait until the Machine is started and every one of its Fly checks
|
|
404
|
+
* (`checks[].status` on the Machine) is `passing`, and, given `since`, has
|
|
405
|
+
* reported since then. A Machine with no checks is done once started. Throws after `timeoutMs`, naming the Machine and each
|
|
406
|
+
* check that is not passing.
|
|
407
|
+
*/
|
|
408
|
+
export async function flyMachineChecks(args, signal, http = defaultFlyHttp(args.token)) {
|
|
409
|
+
return waitForChecks({ base: resolveEndpoint(args) }, args.app, args.machine, http, signal, args, args.since);
|
|
410
|
+
}
|
|
411
|
+
async function waitForChecks(ctx, app, name, http, signal, opts, since) {
|
|
412
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CHECKS_TIMEOUT_MS;
|
|
413
|
+
const interval = opts.intervalMs ?? 1000;
|
|
414
|
+
const deadline = Date.now() + timeoutMs;
|
|
415
|
+
for (;;) {
|
|
416
|
+
if (signal?.aborted)
|
|
417
|
+
throw new Error(`waiting for the checks of ${app}/${name} aborted`);
|
|
418
|
+
const m = await mustFindMachine(ctx, app, name, http, signal);
|
|
419
|
+
const why = m.state === "started" ? checksPending(m, since) : `it is ${m.state}`;
|
|
420
|
+
if (!why)
|
|
421
|
+
return { id: m.id };
|
|
422
|
+
if (Date.now() + interval > deadline) {
|
|
423
|
+
throw new Error(`machine ${app}/${name} (${m.id}) did not pass its Fly checks within ${timeoutMs >= 1000 ? `${Math.round(timeoutMs / 1000)}s` : `${timeoutMs}ms`}: ${why}`);
|
|
424
|
+
}
|
|
425
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -22,6 +22,18 @@ export interface FakeMachine {
|
|
|
22
22
|
config: Record<string, unknown> & {
|
|
23
23
|
metadata?: Record<string, string>;
|
|
24
24
|
};
|
|
25
|
+
/** When the machine was last created or updated (RFC 3339), as flaps reports it. */
|
|
26
|
+
updated_at?: string;
|
|
27
|
+
}
|
|
28
|
+
/** One volume the fake holds, as flaps lists it. */
|
|
29
|
+
export interface FakeVolume {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
region?: string;
|
|
33
|
+
size_gb?: number;
|
|
34
|
+
state: string;
|
|
35
|
+
/** The Machine whose config mounts it by id; null when none does. */
|
|
36
|
+
attached_machine_id: string | null;
|
|
25
37
|
}
|
|
26
38
|
/** What an `exec` answers: the Machines API's exec envelope. */
|
|
27
39
|
export interface FakeExecResult {
|
|
@@ -33,6 +45,8 @@ export interface MachinesFake {
|
|
|
33
45
|
http: FlyHttp;
|
|
34
46
|
apps: Set<string>;
|
|
35
47
|
machines: Map<string, FakeMachine[]>;
|
|
48
|
+
/** Each app's volumes, in creation order, with ids `vol_...` (#4717). */
|
|
49
|
+
volumes: Map<string, FakeVolume[]>;
|
|
36
50
|
/** Each app's certificate hostnames, in creation order. */
|
|
37
51
|
certs: Map<string, string[]>;
|
|
38
52
|
/** Every exec, in order: the app, the machine id and the command. */
|
|
@@ -51,6 +65,22 @@ export interface MachinesFakeOptions {
|
|
|
51
65
|
exec?: (app: string, machine: FakeMachine, command: string[]) => FakeExecResult | Promise<FakeExecResult>;
|
|
52
66
|
/** The state a created or updated machine settles in. Default `started`. */
|
|
53
67
|
settle?: (machine: FakeMachine) => string;
|
|
68
|
+
/** The clock a create or update stamps `updated_at` with. Default: the wall clock. */
|
|
69
|
+
now?: () => Date;
|
|
70
|
+
/**
|
|
71
|
+
* The Fly checks a machine reports each time it is read (#4719), as the
|
|
72
|
+
* Machines API's `checks`: asked on every GET of the machine or its list.
|
|
73
|
+
* Default: none.
|
|
74
|
+
*/
|
|
75
|
+
checks?: (app: string, machine: FakeMachine) => FakeCheck[] | undefined;
|
|
76
|
+
}
|
|
77
|
+
/** One Fly check's status on a machine, as the Machines API reports it. */
|
|
78
|
+
export interface FakeCheck {
|
|
79
|
+
name: string;
|
|
80
|
+
status: "passing" | "warning" | "critical";
|
|
81
|
+
output?: string;
|
|
82
|
+
/** When the check last reported (RFC 3339). */
|
|
83
|
+
updated_at?: string;
|
|
54
84
|
}
|
|
55
85
|
/** Fly's certificate-list page size: 25 when no `limit` is given, 500 at most. */
|
|
56
86
|
export declare const CERT_PAGE_DEFAULT = 25;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"machines-fake.d.ts","sourceRoot":"","sources":["../../../src/op/activities/machines-fake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"machines-fake.d.ts","sourceRoot":"","sources":["../../../src/op/activities/machines-fake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,kCAAkC;AAClC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IACxE,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,oDAAoD;AACpD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACrC,yEAAyE;IACzE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IACnC,2DAA2D;IAC3D,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,qEAAqE;IACrE,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC7D,wDAAwD;IACxD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sDAAsD;IACtD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;CAC7D;AAED,MAAM,WAAW,mBAAmB;IAClC,gIAAgI;IAChI,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAC1G,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IAC1C,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,SAAS,EAAE,GAAG,SAAS,CAAC;CACzE;AAED,2EAA2E;AAC3E,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,aAAa,MAAM,CAAC;AAWjC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CAwNlF"}
|
|
@@ -32,12 +32,30 @@ const nextId = (prefix) => `${prefix}${(++seq).toString(16).padStart(10, "0")}`;
|
|
|
32
32
|
export function createMachinesFake(options = {}) {
|
|
33
33
|
const apps = new Set();
|
|
34
34
|
const machines = new Map();
|
|
35
|
+
const volumes = new Map();
|
|
35
36
|
const certs = new Map();
|
|
36
37
|
const execs = [];
|
|
37
38
|
const calls = [];
|
|
38
39
|
const settle = options.settle ?? (() => "started");
|
|
40
|
+
const stamp = () => (options.now?.() ?? new Date()).toISOString();
|
|
39
41
|
const list = (app) => machines.get(app) ?? [];
|
|
40
42
|
const find = (app, id) => list(app).find((m) => m.id === id);
|
|
43
|
+
const withChecks = (app, m) => {
|
|
44
|
+
const checks = options.checks?.(app, m);
|
|
45
|
+
return checks ? { ...m, checks } : m;
|
|
46
|
+
};
|
|
47
|
+
const vols = (app) => volumes.get(app) ?? [];
|
|
48
|
+
// A volume is attached to the Machine whose config mounts its id, as Fly
|
|
49
|
+
// attaches it. A mount naming no volume id attaches nothing.
|
|
50
|
+
const attach = (app, m, id) => {
|
|
51
|
+
const mounted = new Set((m?.config.mounts ?? []).map((x) => x.volume).filter((v) => typeof v === "string"));
|
|
52
|
+
for (const v of vols(app)) {
|
|
53
|
+
if (v.attached_machine_id === id && !mounted.has(v.id))
|
|
54
|
+
v.attached_machine_id = null;
|
|
55
|
+
if (m && mounted.has(v.id))
|
|
56
|
+
v.attached_machine_id = id;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
41
59
|
const http = async (method, url, body) => {
|
|
42
60
|
const { pathname, searchParams } = new URL(url);
|
|
43
61
|
calls.push(`${method} ${pathname}`);
|
|
@@ -63,6 +81,7 @@ export function createMachinesFake(options = {}) {
|
|
|
63
81
|
if (method === "DELETE") {
|
|
64
82
|
apps.delete(app);
|
|
65
83
|
machines.delete(app);
|
|
84
|
+
volumes.delete(app);
|
|
66
85
|
certs.delete(app);
|
|
67
86
|
return json(202, undefined);
|
|
68
87
|
}
|
|
@@ -75,7 +94,7 @@ export function createMachinesFake(options = {}) {
|
|
|
75
94
|
const action = seg[5];
|
|
76
95
|
if (!id) {
|
|
77
96
|
if (method === "GET")
|
|
78
|
-
return json(200, list(app));
|
|
97
|
+
return json(200, list(app).map((m) => withChecks(app, m)));
|
|
79
98
|
if (method === "POST") {
|
|
80
99
|
const m = {
|
|
81
100
|
id: nextId("m"),
|
|
@@ -83,10 +102,12 @@ export function createMachinesFake(options = {}) {
|
|
|
83
102
|
state: "created",
|
|
84
103
|
instance_id: nextId("I"),
|
|
85
104
|
config: structuredClone((b.config ?? {})),
|
|
105
|
+
updated_at: stamp(),
|
|
86
106
|
};
|
|
87
107
|
// skip_launch: the Machine is created and not booted (#4696).
|
|
88
108
|
m.state = b.skip_launch === true ? "created" : settle(m);
|
|
89
109
|
machines.set(app, [...list(app), m]);
|
|
110
|
+
attach(app, m, m.id);
|
|
90
111
|
return json(200, m);
|
|
91
112
|
}
|
|
92
113
|
}
|
|
@@ -95,10 +116,12 @@ export function createMachinesFake(options = {}) {
|
|
|
95
116
|
return action === "wait" ? json(200, { ok: true }) : json(404, { error: "machine not found" });
|
|
96
117
|
if (!action) {
|
|
97
118
|
if (method === "GET")
|
|
98
|
-
return json(200, m);
|
|
119
|
+
return json(200, withChecks(app, m));
|
|
99
120
|
if (method === "POST") {
|
|
100
121
|
m.config = structuredClone((b.config ?? {}));
|
|
101
122
|
m.instance_id = nextId("I");
|
|
123
|
+
m.updated_at = stamp();
|
|
124
|
+
attach(app, m, m.id);
|
|
102
125
|
// skip_launch: the new config isn't started (#4696). A Machine that
|
|
103
126
|
// never ran stays created; one that ran is stopped.
|
|
104
127
|
m.state = b.skip_launch === true ? (m.state === "created" ? "created" : "stopped") : settle(m);
|
|
@@ -113,6 +136,7 @@ export function createMachinesFake(options = {}) {
|
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
138
|
machines.set(app, list(app).filter((x) => x !== m));
|
|
139
|
+
attach(app, undefined, m.id);
|
|
116
140
|
return json(200, { ok: true });
|
|
117
141
|
}
|
|
118
142
|
}
|
|
@@ -144,10 +168,42 @@ export function createMachinesFake(options = {}) {
|
|
|
144
168
|
return method === "GET" ? json(200, { ips: [] }) : json(200, {});
|
|
145
169
|
if (kind === "certificates")
|
|
146
170
|
return certificates(app, method, seg.slice(4), b, searchParams);
|
|
147
|
-
if (kind === "volumes"
|
|
171
|
+
if (kind === "volumes")
|
|
172
|
+
return volumeCall(app, method, seg[4], b);
|
|
173
|
+
if (kind === "secrets")
|
|
148
174
|
return method === "GET" ? json(200, []) : json(200, {});
|
|
149
175
|
return json(404, { error: "not found" });
|
|
150
176
|
};
|
|
177
|
+
// Volumes (#4717): created with a `vol_` id, listed with their region and
|
|
178
|
+
// the Machine they are attached to, read and deleted by id.
|
|
179
|
+
function volumeCall(app, method, id, b) {
|
|
180
|
+
if (!id) {
|
|
181
|
+
if (method === "GET")
|
|
182
|
+
return json(200, vols(app));
|
|
183
|
+
if (method === "POST") {
|
|
184
|
+
const v = {
|
|
185
|
+
id: nextId("vol_"),
|
|
186
|
+
name: String(b.name ?? ""),
|
|
187
|
+
...(typeof b.region === "string" ? { region: b.region } : {}),
|
|
188
|
+
...(typeof b.size_gb === "number" ? { size_gb: b.size_gb } : {}),
|
|
189
|
+
state: "created",
|
|
190
|
+
attached_machine_id: null,
|
|
191
|
+
};
|
|
192
|
+
volumes.set(app, [...vols(app), v]);
|
|
193
|
+
return json(200, v);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const v = vols(app).find((x) => x.id === id);
|
|
197
|
+
if (!v)
|
|
198
|
+
return json(404, { error: "volume not found" });
|
|
199
|
+
if (method === "GET")
|
|
200
|
+
return json(200, v);
|
|
201
|
+
if (method === "DELETE") {
|
|
202
|
+
volumes.set(app, vols(app).filter((x) => x !== v));
|
|
203
|
+
return json(200, { ...v, state: "destroyed" });
|
|
204
|
+
}
|
|
205
|
+
return json(404, { error: `no ${method} on a volume` });
|
|
206
|
+
}
|
|
151
207
|
// Certificates, as the real API routes them (#3114): listed at
|
|
152
208
|
// `.../certificates`, created only at `.../certificates/acme`, read and
|
|
153
209
|
// deleted at `.../certificates/{hostname}`. A POST to the bare list path is
|
|
@@ -198,6 +254,7 @@ export function createMachinesFake(options = {}) {
|
|
|
198
254
|
http,
|
|
199
255
|
apps,
|
|
200
256
|
machines,
|
|
257
|
+
volumes,
|
|
201
258
|
certs,
|
|
202
259
|
execs,
|
|
203
260
|
calls,
|
|
@@ -6,17 +6,22 @@
|
|
|
6
6
|
* apply time, so catch it at synth. This is a whole-stack check: it reads all
|
|
7
7
|
* entities in the build (`ctx.entities`), so the mount and the Volume can be
|
|
8
8
|
* declared in different files.
|
|
9
|
+
*
|
|
10
|
+
* A mount given a volume id (`vol_...`) names a volume that already exists on
|
|
11
|
+
* Fly, which the stack need not declare, so it passes (#4717). `flyApply`
|
|
12
|
+
* sends it as it is, and looks a name up to send its id.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
16
|
import { readProps, entityTypeOf } from "./fly-helpers";
|
|
17
|
+
import { isVolumeId } from "../../volume-id";
|
|
13
18
|
|
|
14
19
|
const MACHINE_ENTITY_TYPE = "Fly::Machines::Machine";
|
|
15
20
|
const VOLUME_ENTITY_TYPE = "Fly::Machines::Volume";
|
|
16
21
|
|
|
17
22
|
export const fly011: PostSynthCheck = {
|
|
18
23
|
id: "FLY011",
|
|
19
|
-
description: "A machine mount's volume must resolve to a Volume declared in the stack",
|
|
24
|
+
description: "A machine mount's volume must resolve to a Volume declared in the stack, or be a volume id (vol_...)",
|
|
20
25
|
|
|
21
26
|
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
27
|
const diagnostics: PostSynthDiagnostic[] = [];
|
|
@@ -44,11 +49,13 @@ export const fly011: PostSynthCheck = {
|
|
|
44
49
|
// reference to a Volume that exists in the stack by construction, so it
|
|
45
50
|
// never dangles — skip it to avoid false positives.
|
|
46
51
|
if (typeof volume !== "string" || volume.length === 0) continue;
|
|
52
|
+
// A volume id names a volume that exists outside the stack.
|
|
53
|
+
if (isVolumeId(volume)) continue;
|
|
47
54
|
if (!declaredVolumes.has(volume)) {
|
|
48
55
|
diagnostics.push({
|
|
49
56
|
checkId: "FLY011",
|
|
50
57
|
severity: "error",
|
|
51
|
-
message: `Machine "${machineName}" mounts volume "${volume}", which is not declared as a Volume in the stack.`,
|
|
58
|
+
message: `Machine "${machineName}" mounts volume "${volume}", which is not declared as a Volume in the stack. Declare the Volume, or mount an existing volume by its id (vol_...).`,
|
|
52
59
|
entity: machineName,
|
|
53
60
|
lexicon: "fly",
|
|
54
61
|
});
|
|
@@ -89,13 +89,37 @@ export const web: Component = {
|
|
|
89
89
|
};
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The step updates the Machine in place with the release in its `config.metadata`: `chant-release-digest`, `chant-release-git-sha` (the commit, `git rev-parse HEAD` unless given), and `chant-release-previous-digest` (the release it replaced). Each migration then runs inside the Machine through the Machines API's exec, once per environment: its receipt is kept on `chant/lifecycle` at `<env>/receipts/`, and a migration fires again only when its `sha` (or command) changes. After a migration fires, the Machine restarts, and the step checks that it is started with this release and, given a `url`, that its health endpoint answers with this commit or digest. If anything fails after the Machine changed, the step puts back the config the Machine served before (on a first release, it stops the Machine) and fails.
|
|
92
|
+
The step updates the Machine in place with the release in its `config.metadata`: `chant-release-digest`, `chant-release-git-sha` (the commit, `git rev-parse HEAD` unless given), and `chant-release-previous-digest` (the release it replaced). Each migration then runs inside the Machine through the Machines API's exec, once per environment: its receipt is kept on `chant/lifecycle` at `<env>/receipts/`, and a migration fires again only when its `sha` (or command) changes. After a migration fires, the Machine restarts, and the step checks that it is started with this release and, given a `url`, that its health endpoint answers with this commit or digest. If anything fails after the Machine changed, the step puts back the config the Machine served before (on a first release, it stops the Machine) and fails. That includes an apply that fails partway: the Machines it had already updated go back too.
|
|
93
93
|
|
|
94
94
|
The step's output carries `uri` and `digest`, so `chant run --components web --env prod` records the release in the ledger. `chant components status prod --live` reads the Machine's metadata back and compares its digest with the ledger's: the row is `reconciled` when they agree and `drifted` when the Machine serves something the ledger does not name. The component joins the Machine by name: name the component after the Machine entity, or list it in `liveNames`.
|
|
95
95
|
|
|
96
96
|
Each release's Machine config is kept on `chant/lifecycle` at `<env>/fly/<app>/<machine>/<digest>.json`. `fly-rollback` puts back the config of the release the serving one replaced (or the digest given as `to`), checks it, and outputs that digest so the ledger records it again. `fly-release`'s own saga compensation does the same.
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
### Several serving Machines with Fly checks
|
|
99
|
+
|
|
100
|
+
An app served by more than one Machine, such as two Machines behind Flycast with no public address, declares them all in the plan. `fly-release` releases the one named by `machine` and gives the others their image through `images`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
{
|
|
104
|
+
kind: "fly-release",
|
|
105
|
+
plan: "dist/fly.json",
|
|
106
|
+
machine: "platform-1",
|
|
107
|
+
digest: "@Publish.digest",
|
|
108
|
+
image: "@Publish.uri",
|
|
109
|
+
images: { "platform-2": "@Publish.uri" },
|
|
110
|
+
checks: { timeoutMs: 300_000 },
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Without `checks`, the apply updates the Machines in plan order and waits for each to be `started` only, and verify reads the serving Machine alone.
|
|
115
|
+
|
|
116
|
+
With `checks`, the apply updates the Machines one at a time, as `fly deploy` does. Once a Machine is started, the step reads its Fly checks from the Machines API (`checks[].status` on the Machine) until every one is `passing`, and only then updates the next Machine. A check counts only when its `updated_at` is at or after the Machine's own `updated_at` as the update call answered it, so a `passing` left over from before the update does not count. Both times are flaps' own, so the local clock plays no part; a check with no `updated_at` counts on its status. A Machine whose config declares checks that have not reported yet is still waiting. `checks.timeoutMs` (default 300000, five minutes) bounds the wait for each Machine, and `checks.intervalMs` (default 1000) is how often it looks; `checks: true` takes both defaults. A Machine with no checks configured is done once it is started, and a scheduled Machine is not waited for. The wait comes right after each Machine's update, before migrations run, so a release whose new code can't pass its checks until a migration has run should leave `checks` off.
|
|
117
|
+
|
|
118
|
+
Under `checks`, a Machine whose checks do not pass in time fails the release with an error naming the Machine and each check that is not passing, for example `machine platform/platform-2 (e784...) did not pass its Fly checks within 300s: check servicecheck-00-http-8080 is critical (503 Service Unavailable)`. The Machines after it are left alone. Every Machine the release changed goes back to its old config, the last one changed first, and each restore waits for its Machine to be started with its checks passing before the next one. On a first release the serving Machine is stopped instead.
|
|
119
|
+
|
|
120
|
+
Under `checks`, verify then covers every Machine the release started: the serving one is started with this release and its checks passing (and its health URL answering, when given), and each other one is started with its checks passing, within `verify.timeoutMs`. A failure there restores them all in the same way.
|
|
121
|
+
|
|
122
|
+
The steps are also Op activities, for an Op that composes them itself: `flyMachineRelease`, `flyMachineExec`, `flyMachineRestart`, `flyMachineStop`, `flyMachineVerify` and `flyMachineRestore`. Wrap a migration's `flyMachineExec` in `effect()` so it fires once. `flyMachineRelease` and `flyMachineRestore` wait for Fly checks when given `checks`, and `flyMachineVerify` reads them with `checks: true`; without them they wait for `started` only.
|
|
99
123
|
|
|
100
124
|
### A source tree instead of an image
|
|
101
125
|
|
|
@@ -10,9 +10,11 @@ Beyond the App and Machine covered in `chant-fly`, the lexicon models `Volume`,
|
|
|
10
10
|
|
|
11
11
|
## Volumes and mounts
|
|
12
12
|
|
|
13
|
-
`flyApply` applies volumes before machines
|
|
13
|
+
`flyApply` applies volumes before machines. A volume is created if absent (idempotent by name); a re-apply of an existing volume is a no-op.
|
|
14
14
|
|
|
15
|
-
The
|
|
15
|
+
A mount can name its volume. The Machines API takes the volume's id in `config.mounts[].volume`, so before it creates or updates a Machine, `flyApply` looks the name up among the app's live volumes and sends `{ volume: "vol_...", name: "data", path }`, as flyctl does. A volume declared in the same apply is created first, so its id is there to look up. Fly lets several volumes share a name (one per Machine), so the lookup takes the one already attached to the Machine, else the one in the Machine's region that no Machine has. No volume of that name, or more than one that fits, fails the apply naming the candidates; give the mount the id to settle it. A mount given an id (`vol_...`) is sent as it is. `flyMachineRestore` and the `fly-release` capability do the same lookup. The build output and a release's recorded config keep the name.
|
|
16
|
+
|
|
17
|
+
The FLY011 build check enforces the link statically: every machine mount must reference a `Volume` declared in the stack, checked across files, or give a volume id. A mount that points at an undeclared volume fails `chant build` before anything reaches the API.
|
|
16
18
|
|
|
17
19
|
```ts
|
|
18
20
|
import { App, Machine, MachineConfig, MachineGuest, Volume, Fly } from "@intentius/chant-lexicon-fly";
|
package/dist/skills/chant-fly.md
CHANGED
|
@@ -72,7 +72,7 @@ Build synthesizes the flaps plan and runs the lint rules before anything reaches
|
|
|
72
72
|
| FLY002 | Guest sizing (`cpu_kind` / `cpus` / `memory_mb`) is not a valid combination |
|
|
73
73
|
| FLY004 | A secret value written inline in machine config |
|
|
74
74
|
| FLY010 | A machine config with no `image` |
|
|
75
|
-
| FLY011 | A machine mount that references a `Volume` not declared in the stack (checked across files) |
|
|
75
|
+
| FLY011 | A machine mount that references a `Volume` not declared in the stack (checked across files); a volume id (`vol_...`) passes |
|
|
76
76
|
| FLY012 | A machine `schedule` other than `hourly`, `daily`, `weekly` or `monthly` |
|
|
77
77
|
|
|
78
78
|
Fix every reported violation before applying. Secret values belong in a `Secret` or a reference, never inline (FLY004).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Fly volume's id, as the Machines API gives it (`vol_r68lmz2doox9myd4`).
|
|
3
|
+
* A Machine's `config.mounts[].volume` takes this id; chant also lets it name
|
|
4
|
+
* a volume, and `flyApply` looks the name up (#4717). Kept apart from the
|
|
5
|
+
* applier so the FLY011 lint check can use it without loading the applier.
|
|
6
|
+
*/
|
|
7
|
+
/** True when `value` is a volume id (`vol_...`), not a volume's name. Pure. */
|
|
8
|
+
export declare function isVolumeId(value: string): boolean;
|
|
9
|
+
//# sourceMappingURL=volume-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"volume-id.d.ts","sourceRoot":"","sources":["../src/volume-id.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Fly volume's id, as the Machines API gives it (`vol_r68lmz2doox9myd4`).
|
|
3
|
+
* A Machine's `config.mounts[].volume` takes this id; chant also lets it name
|
|
4
|
+
* a volume, and `flyApply` looks the name up (#4717). Kept apart from the
|
|
5
|
+
* applier so the FLY011 lint check can use it without loading the applier.
|
|
6
|
+
*/
|
|
7
|
+
/** True when `value` is a volume id (`vol_...`), not a volume's name. Pure. */
|
|
8
|
+
export function isVolumeId(value) {
|
|
9
|
+
return /^vol_[A-Za-z0-9]+$/.test(value);
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fly",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.69",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@intentius/chant-lexicon-otel": "^0.124.
|
|
61
|
+
"@intentius/chant-lexicon-otel": "^0.124.69",
|
|
62
62
|
"ws": "^8.18.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"zod": "^4.3.6"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@intentius/chant": "^0.124.
|
|
72
|
+
"@intentius/chant": "^0.124.69",
|
|
73
73
|
"zod": "^4.3.6"
|
|
74
74
|
},
|
|
75
75
|
"description": "Fly.io Machines lexicon for chant — declarative IaC in TypeScript",
|