@crvouga/dev-fleet 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -13
- package/ansible/macos/manager-privilege.yml +37 -0
- package/ansible/macos/manager.yml +51 -6
- package/ansible/macos/policy.json +6 -1
- package/ansible/macos/worker-host-key.yml +33 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/ubuntu/baseline.yml +6 -0
- package/ansible/ubuntu/recovery.yml +2 -0
- package/ansible/ubuntu/tailscale.yml +2 -0
- package/bootstrap/worker-seed.yaml +61 -0
- package/docs/authentication.md +2 -0
- package/docs/consumer-setup.md +34 -35
- package/docs/declared.md +24 -24
- package/docs/manager-recovery.md +78 -0
- package/docs/manager-setup.md +27 -0
- package/docs/operations.md +8 -0
- package/docs/runbooks.md +15 -1
- package/docs/state.md +38 -6
- package/docs/topology.md +19 -11
- package/docs/ubuntu-sync.md +21 -15
- package/package.json +2 -2
- package/schema/check-ids.json +36 -0
- package/schema/fleet.schema.json +22 -2
- package/src/check-owners.ts +164 -0
- package/src/cli/fleet-manifest.ts +102 -0
- package/src/cli/fleet-node-lifecycle.ts +455 -0
- package/src/cli/fleet-node-seed.ts +151 -0
- package/src/cli/fleet-restore.ts +205 -0
- package/src/cli/fleet-state.ts +57 -5
- package/src/cli/fleet-sync-forward.ts +2 -0
- package/src/cli/fleet-sync-manager.ts +101 -18
- package/src/cli/fleet-sync-remote.ts +21 -6
- package/src/cli/fleet-sync-state.ts +30 -11
- package/src/cli/fleet-sync-vault.ts +75 -26
- package/src/cli/fleet-sync.ts +54 -5
- package/src/cli/fleet-validate.ts +239 -0
- package/src/cli/register-fleet.ts +167 -0
- package/src/cli/registry.ts +47 -1
- package/src/definition.ts +444 -237
- package/src/machine/state.ts +33 -1
- package/src/manager-sync.ts +24 -4
- package/src/platform/manager-host.ts +184 -0
- package/src/repair-catalog.ts +64 -0
- package/src/repair-plan.ts +25 -13
- package/src/secrets.ts +3 -0
- package/src/state/check.ts +7 -0
- package/src/state/holder.ts +50 -0
- package/src/state/litestream.ts +6 -3
- package/src/state/migrations/0001-documents.sql +13 -0
- package/src/state/migrations/0002-lease.sql +7 -0
- package/src/state/store.ts +125 -26
- package/src/state/sync.ts +131 -10
- package/src/worker-credentials.ts +201 -0
- package/src/worker-manifest.ts +201 -0
- package/src/worker-policy.ts +21 -35
- package/src/worker-seed.ts +138 -0
|
@@ -12,6 +12,24 @@ import { fleetReport, type FleetReportOptions } from './fleet-report';
|
|
|
12
12
|
import { fleetWatch, watchServices } from './fleet-watch';
|
|
13
13
|
import type { NodeReport } from '../types';
|
|
14
14
|
import { registerGithubVerify } from './github-verify';
|
|
15
|
+
import { fleetRestore, type FleetRestoreOptions } from './fleet-restore';
|
|
16
|
+
import { fleetValidate, type FleetValidateOptions } from './fleet-validate';
|
|
17
|
+
import {
|
|
18
|
+
fleetManifest,
|
|
19
|
+
manifestServices,
|
|
20
|
+
type FleetManifestOptions,
|
|
21
|
+
} from './fleet-manifest';
|
|
22
|
+
import {
|
|
23
|
+
fleetNodeSeed,
|
|
24
|
+
seedServices,
|
|
25
|
+
type NodeSeedOptions,
|
|
26
|
+
} from './fleet-node-seed';
|
|
27
|
+
import {
|
|
28
|
+
fleetNodeReplace,
|
|
29
|
+
fleetNodeRetire,
|
|
30
|
+
lifecycleServices,
|
|
31
|
+
type NodeLifecycleOptions,
|
|
32
|
+
} from './fleet-node-lifecycle';
|
|
15
33
|
|
|
16
34
|
type Complete = (outcome: CommandOutcome) => void;
|
|
17
35
|
|
|
@@ -186,6 +204,14 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
186
204
|
collect,
|
|
187
205
|
[]
|
|
188
206
|
)
|
|
207
|
+
.option(
|
|
208
|
+
'--vault-password-login',
|
|
209
|
+
'Opt-in: log a worker without a token of its own in with the shared Vault user, on its own terminal'
|
|
210
|
+
)
|
|
211
|
+
.option(
|
|
212
|
+
'--canary <id>',
|
|
213
|
+
'The worker that proves a new revision first when it is reachable; defaults to canary in fleet.json, then the first worker'
|
|
214
|
+
)
|
|
189
215
|
.option(
|
|
190
216
|
'--all',
|
|
191
217
|
'Explicit spelling of the default enrolled fleet scope; never discovered peers'
|
|
@@ -199,6 +225,10 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
199
225
|
'Accepted for compatibility; fleet-sync applies by default (schedule writes still need --apply --yes)'
|
|
200
226
|
)
|
|
201
227
|
.option('--yes', 'Accepted for compatibility with --apply')
|
|
228
|
+
.option(
|
|
229
|
+
'--take-over',
|
|
230
|
+
'Make this machine the manager while another one still holds the manager lease'
|
|
231
|
+
)
|
|
202
232
|
.option(
|
|
203
233
|
'--schedule <action>',
|
|
204
234
|
'Manager-only schedule: enable, pause, resume, disable, status'
|
|
@@ -277,6 +307,14 @@ function registerState({ program, complete }: Registration) {
|
|
|
277
307
|
'Replace this machine’s copy with the bucket’s version'
|
|
278
308
|
)
|
|
279
309
|
.option('--yes', 'With --restore: discard changes the bucket does not hold')
|
|
310
|
+
.option(
|
|
311
|
+
'--timestamp <time>',
|
|
312
|
+
'With --restore: the version the bucket held at this time, uploaded again as the current one (a damaged replica)'
|
|
313
|
+
)
|
|
314
|
+
.option(
|
|
315
|
+
'--take-over',
|
|
316
|
+
'With --init or --import: change the fleet state while another manager still holds the manager lease'
|
|
317
|
+
)
|
|
280
318
|
.addHelpText(
|
|
281
319
|
'after',
|
|
282
320
|
'\nRead-only without a flag. The bucket is the one named under "state" in fleet.json. Every command reads its credentials from DEVFLEET_BUCKET_ACCESS_KEY_ID and DEVFLEET_BUCKET_SECRET_ACCESS_KEY; --init also asks on the terminal. They are never taken from arguments.'
|
|
@@ -332,6 +370,131 @@ function registerWatch({ program, defaults, complete }: Registration) {
|
|
|
332
370
|
});
|
|
333
371
|
}
|
|
334
372
|
|
|
373
|
+
function registerRestore({ program, complete }: Registration) {
|
|
374
|
+
program
|
|
375
|
+
.command('fleet-restore')
|
|
376
|
+
.description(
|
|
377
|
+
'Bring a replacement manager back from the bucket (docs/manager-recovery.md)'
|
|
378
|
+
)
|
|
379
|
+
.option(
|
|
380
|
+
'--dry-run',
|
|
381
|
+
'Verify every prerequisite and list what would be restored; change nothing'
|
|
382
|
+
)
|
|
383
|
+
.action(async (_options, command: Command) => {
|
|
384
|
+
complete(
|
|
385
|
+
await fleetRestore(command.optsWithGlobals<FleetRestoreOptions>())
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function registerValidate({ program, complete }: Registration) {
|
|
391
|
+
program
|
|
392
|
+
.command('fleet-validate')
|
|
393
|
+
.description(
|
|
394
|
+
'Validate fleet.json and name the field that breaks a rule; contacts nothing'
|
|
395
|
+
)
|
|
396
|
+
.option(
|
|
397
|
+
'--file <path>',
|
|
398
|
+
'The fleet.json to validate; defaults to the project’s'
|
|
399
|
+
)
|
|
400
|
+
.option(
|
|
401
|
+
'--ansible',
|
|
402
|
+
'Also render the Ansible inventory and syntax-check every playbook with the pinned Ansible'
|
|
403
|
+
)
|
|
404
|
+
.action(async (_options, command: Command) => {
|
|
405
|
+
complete(
|
|
406
|
+
await fleetValidate(command.optsWithGlobals<FleetValidateOptions>())
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function registerLifecycle({ program, defaults, complete }: Registration) {
|
|
412
|
+
const action =
|
|
413
|
+
(name: 'replace' | 'retire') =>
|
|
414
|
+
async (_options: unknown, command: Command) => {
|
|
415
|
+
const options = command.optsWithGlobals<
|
|
416
|
+
NodeLifecycleOptions & CliOptions
|
|
417
|
+
>();
|
|
418
|
+
const settings = await withSettings(defaults, options);
|
|
419
|
+
const services = lifecycleServices(settings.config);
|
|
420
|
+
complete(
|
|
421
|
+
name === 'replace'
|
|
422
|
+
? await fleetNodeReplace(options, services)
|
|
423
|
+
: await fleetNodeRetire(options, services)
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
program
|
|
427
|
+
.command('fleet-node-replace')
|
|
428
|
+
.description(
|
|
429
|
+
'Put a fresh machine in place of a worker under the same id; writes need --apply --yes'
|
|
430
|
+
)
|
|
431
|
+
.requiredOption('--id <name>', 'The declared worker being replaced')
|
|
432
|
+
.option(
|
|
433
|
+
'--host-key <key>',
|
|
434
|
+
'The new machine’s SSH host key, "TYPE BASE64", verified out of band or printed by fleet-node-seed'
|
|
435
|
+
)
|
|
436
|
+
.option('--apply', 'Change the manager and Vault; requires --yes')
|
|
437
|
+
.option('--yes', 'Confirm an explicitly requested --apply operation')
|
|
438
|
+
.action(action('replace'));
|
|
439
|
+
program
|
|
440
|
+
.command('fleet-node-retire')
|
|
441
|
+
.description(
|
|
442
|
+
'Take a worker out of the fleet: revoke its token, forget its host key and roster entry; writes need --apply --yes'
|
|
443
|
+
)
|
|
444
|
+
.requiredOption('--id <name>', 'The declared worker to retire')
|
|
445
|
+
.option(
|
|
446
|
+
'--apply',
|
|
447
|
+
'Change the manager, Vault and the fleet state; requires --yes'
|
|
448
|
+
)
|
|
449
|
+
.option('--yes', 'Confirm an explicitly requested --apply operation')
|
|
450
|
+
.action(action('retire'));
|
|
451
|
+
program
|
|
452
|
+
.command('fleet-node-seed')
|
|
453
|
+
.description(
|
|
454
|
+
'Write cloud-init (or autoinstall) user data a new worker joins the fleet with by itself'
|
|
455
|
+
)
|
|
456
|
+
.requiredOption('--id <name>', 'The declared worker to seed')
|
|
457
|
+
.requiredOption(
|
|
458
|
+
'--output <path>',
|
|
459
|
+
'Where to write the seed (mode 600); it holds a single-use auth key and is never printed'
|
|
460
|
+
)
|
|
461
|
+
.option(
|
|
462
|
+
'--public-key <path>',
|
|
463
|
+
'The manager public key to authorize; default ~/.ssh/id_ed25519.pub'
|
|
464
|
+
)
|
|
465
|
+
.option('--autoinstall', 'An Ubuntu autoinstall file, for bare metal')
|
|
466
|
+
.option(
|
|
467
|
+
'--apply',
|
|
468
|
+
'Also pin the seed host key on this manager; requires --yes'
|
|
469
|
+
)
|
|
470
|
+
.option('--yes', 'Confirm an explicitly requested --apply operation')
|
|
471
|
+
.action(async (_options: unknown, command: Command) => {
|
|
472
|
+
const options = command.optsWithGlobals<NodeSeedOptions & CliOptions>();
|
|
473
|
+
const settings = await withSettings(defaults, options);
|
|
474
|
+
complete(await fleetNodeSeed(options, seedServices(settings.config)));
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function registerManifest({ program, defaults, complete }: Registration) {
|
|
479
|
+
program
|
|
480
|
+
.command('fleet-manifest')
|
|
481
|
+
.description(
|
|
482
|
+
'Measure what a worker is made of and check it against the pins and a fresh build'
|
|
483
|
+
)
|
|
484
|
+
.requiredOption('--id <name>', 'The enrolled worker to measure')
|
|
485
|
+
.option(
|
|
486
|
+
'--reference <path>',
|
|
487
|
+
'A fleet-manifest --json of a fresh build of the same revision'
|
|
488
|
+
)
|
|
489
|
+
.action(async (_options: unknown, command: Command) => {
|
|
490
|
+
const options = command.optsWithGlobals<
|
|
491
|
+
FleetManifestOptions & CliOptions
|
|
492
|
+
>();
|
|
493
|
+
const settings = await withSettings(defaults, options);
|
|
494
|
+
complete(await fleetManifest(options, manifestServices(settings.config)));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
335
498
|
export function registerFleet(
|
|
336
499
|
program: Command,
|
|
337
500
|
defaults: Settings,
|
|
@@ -348,6 +511,10 @@ export function registerFleet(
|
|
|
348
511
|
registerState(registration);
|
|
349
512
|
registerReport(registration);
|
|
350
513
|
registerWatch(registration);
|
|
514
|
+
registerRestore(registration);
|
|
515
|
+
registerValidate(registration);
|
|
516
|
+
registerLifecycle(registration);
|
|
517
|
+
registerManifest(registration);
|
|
351
518
|
registerRepair(registration);
|
|
352
519
|
registerNodes(registration);
|
|
353
520
|
registerAccounts(registration);
|
package/src/cli/registry.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
93
93
|
[
|
|
94
94
|
'fleet-state --restore',
|
|
95
95
|
'Replace the local fleet state with the bucket’s',
|
|
96
|
-
'Discard this manager’s copy of the fleet state and download the bucket’s version. Needs --yes when this machine has changes the bucket does not hold.',
|
|
96
|
+
'Discard this manager’s copy of the fleet state and download the bucket’s version. Needs --yes when this machine has changes the bucket does not hold. --timestamp TIME --yes restores the version the bucket held then and uploads it again: the remedy for a corrupt replica.',
|
|
97
97
|
['state', 'bucket', 'restore', 'recover', 'disposable'],
|
|
98
98
|
true,
|
|
99
99
|
],
|
|
@@ -103,6 +103,31 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
103
103
|
'Read-only: success rate, mean duration, the most frequent failing check IDs and how long each machine was not ready, from the history of fleet-sync runs kept in the fleet state. --since 30d (default), 12h or 90m.',
|
|
104
104
|
['history', 'reliability', 'report', 'failures', 'sync'],
|
|
105
105
|
],
|
|
106
|
+
[
|
|
107
|
+
'fleet-restore --dry-run',
|
|
108
|
+
'Check that a replacement manager can be restored',
|
|
109
|
+
'Read-only: verifies every prerequisite of a manager restore (fleet.json, the bucket and the state in it, the local host name, Git, Tailscale, the container engine) and lists what would be restored, in order. docs/manager-recovery.md is the runbook.',
|
|
110
|
+
['manager', 'restore', 'replace', 'lost', 'recovery', 'runbook'],
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
'fleet-restore',
|
|
114
|
+
'Restore a replacement manager from the bucket',
|
|
115
|
+
'Runs the same checks, then takes the fleet state from the bucket. fleet-sync rebuilds the rest.',
|
|
116
|
+
['manager', 'restore', 'replace', 'lost', 'recovery'],
|
|
117
|
+
true,
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
'fleet-validate',
|
|
121
|
+
'Validate the fleet configuration',
|
|
122
|
+
'Read-only: judge fleet.json without contacting anything. An invalid file names the first rule it breaks as {code, path, expected, actual}. --ansible also renders the Ansible inventory and syntax-checks every playbook with the pinned Ansible.',
|
|
123
|
+
['validate', 'lint', 'schema', 'fleet.json', 'configuration', 'ansible'],
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
'fleet-manifest',
|
|
127
|
+
'Measure a worker and compare it with a fresh build',
|
|
128
|
+
'Read-only over verified SSH: every installed package, the active mise tools and the state of each always-on service, with a digest that is the same for two workers built from the same revision. The check worker:manifest reports every difference from the declared pins and, with --reference, from a fresh build.',
|
|
129
|
+
['manifest', 'drift', 'reproducible', 'rebuild', 'packages', 'audit'],
|
|
130
|
+
],
|
|
106
131
|
[
|
|
107
132
|
'fleet-node-sync',
|
|
108
133
|
'Sync this node',
|
|
@@ -168,6 +193,27 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
168
193
|
['github', 'verify', 'pull request', 'push', 'permissions', 'workflow'],
|
|
169
194
|
true,
|
|
170
195
|
],
|
|
196
|
+
[
|
|
197
|
+
'fleet-node-replace',
|
|
198
|
+
'Replace a worker with a fresh machine',
|
|
199
|
+
'Preview, or with --apply --yes carry out, putting a fresh machine in place of a declared worker under the same id: verify the new device on the tailnet, pin its host key (--host-key), bind the roster entry to it and issue it a Vault token of its own, revoking the old one. Then fleet-sync --node ID converges it. Running it again changes nothing.',
|
|
200
|
+
['replace', 'rebuild', 'reinstall', 'host key', 'token', 'device'],
|
|
201
|
+
true,
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
'fleet-node-seed',
|
|
205
|
+
'Seed a new worker that joins by itself',
|
|
206
|
+
'Write cloud-init user data (or an Ubuntu autoinstall file with --autoinstall) for a declared worker to a private file: its account with the manager key, the privilege rule, a host key generated for it, the pinned Tailscale and a short-lived single-use auth key from the secrets provider (tailnet-auth-key). --apply --yes also pins that host key on the manager, so there is no trust on first use.',
|
|
207
|
+
['seed', 'cloud-init', 'autoinstall', 'new', 'bootstrap', 'provision'],
|
|
208
|
+
true,
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
'fleet-node-retire',
|
|
212
|
+
'Retire a worker',
|
|
213
|
+
'Preview, or with --apply --yes carry out, taking a worker out of the fleet: revoke its Vault token, forget its pinned host key, remove its roster entry and record a tombstone in the fleet state. Then remove it from fleet.json.',
|
|
214
|
+
['retire', 'remove', 'decommission', 'revoke', 'delete'],
|
|
215
|
+
true,
|
|
216
|
+
],
|
|
171
217
|
]),
|
|
172
218
|
...group('T3', [
|
|
173
219
|
[
|