@crvouga/dev-fleet 0.8.0 → 0.9.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 +65 -9
- package/agents/audit.md +18 -18
- package/ansible/agents/agents.yml +134 -0
- package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
- package/ansible/agents/templates/claude.settings.json.j2 +3 -0
- package/ansible/agents/templates/codex.config.toml.j2 +11 -0
- package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
- package/ansible/agents/templates/grok.config.toml.j2 +19 -0
- package/ansible/agents/templates/opencode.json.j2 +24 -0
- package/ansible/agents/templates/receipt.json.j2 +10 -0
- package/ansible/ansible.cfg +14 -0
- package/ansible/github/git.yml +82 -0
- package/ansible/macos/manager-privilege.yml +55 -9
- package/ansible/macos/manager.yml +154 -7
- package/ansible/macos/policy.json +13 -0
- package/ansible/macos/t3-policy.yml +5 -3
- package/ansible/macos/vault-renew.yml +85 -0
- package/ansible/monitoring/agent-darwin.yml +3 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/monitoring/hub-darwin.yml +3 -0
- package/ansible/monitoring/hub-route-darwin.yml +1 -0
- package/ansible/net/remote-probe.sh +4 -1
- package/ansible/platforms.json +2 -2
- package/ansible/pyproject.toml +5 -0
- package/ansible/ubuntu/baseline.yml +39 -13
- package/ansible/ubuntu/disk-gc.py +6 -2
- package/ansible/ubuntu/firewall.nft.j2 +50 -0
- package/ansible/ubuntu/policy.json +125 -6
- package/ansible/ubuntu/reboot.yml +89 -0
- package/ansible/ubuntu/recovery.yml +13 -3
- package/ansible/ubuntu/t3/policy.json +3 -0
- package/ansible/ubuntu/t3-desktop.yml +23 -2
- package/ansible/ubuntu/t3-headless.yml +128 -0
- package/ansible/ubuntu/t3-health.sh +24 -10
- package/ansible/ubuntu/t3-idle.py +37 -2
- package/ansible/ubuntu/t3-policy.yml +4 -1
- package/ansible/ubuntu/t3-route.yml +1 -0
- package/ansible/ubuntu/tailscale.yml +54 -8
- package/ansible/ubuntu/tasks/browser.yml +3 -0
- package/ansible/ubuntu/tasks/containers-off.yml +43 -0
- package/ansible/ubuntu/tasks/docker.yml +53 -6
- package/ansible/ubuntu/tasks/dpkg.yml +4 -0
- package/ansible/ubuntu/tasks/features.yml +22 -0
- package/ansible/ubuntu/tasks/firewall.yml +172 -0
- package/ansible/ubuntu/tasks/install.yml +2 -1
- package/ansible/ubuntu/tasks/packages.yml +4 -2
- package/ansible/ubuntu/tasks/release.yml +6 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -4
- package/ansible/ubuntu/tasks/services.yml +2 -2
- package/ansible/ubuntu/tasks/sizing.yml +49 -0
- package/ansible/ubuntu/tasks/snapshot.yml +14 -3
- package/ansible/ubuntu/tasks/ssh.yml +61 -4
- package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
- package/ansible/ubuntu/tasks/user.yml +1 -0
- package/ansible/ubuntu/worker-account.yml +117 -0
- package/ansible/uv.lock +265 -0
- package/bin/devfleet.mjs +42 -4
- package/bootstrap/fleet-remote-bootstrap.sh +91 -25
- package/bootstrap/global-config.ts +29 -3
- package/bootstrap/package-smoke.ts +127 -0
- package/bootstrap/pins.ts +189 -0
- package/bootstrap/release-notes.ts +88 -0
- package/bootstrap/release-version.ts +41 -7
- package/bootstrap/worker-features.ts +114 -0
- package/bootstrap/worker-seed.yaml +7 -3
- package/bootstrap/worker-tools.ts +39 -9
- package/cliproxy/README.md +1 -1
- package/cliproxy/docs/health-workflow.example.yml +20 -143
- package/docs/authentication.md +19 -11
- package/docs/consumer-setup.md +42 -27
- package/docs/convergence.md +67 -0
- package/docs/declared.md +61 -34
- package/docs/machine-setup.md +73 -7
- package/docs/manager-setup.md +10 -3
- package/docs/operations.md +64 -9
- package/docs/releasing.md +49 -1
- package/docs/runbooks.md +98 -7
- package/docs/state.md +23 -11
- package/docs/support-matrix.md +40 -4
- package/docs/topology.md +67 -6
- package/docs/ubuntu-sync.md +78 -28
- package/docs/worker-contract.md +122 -19
- package/docs/worker-requirements.md +107 -0
- package/examples/fleet.json +1 -0
- package/examples/workers-from-infrastructure/main.tf +132 -0
- package/examples/workers-from-infrastructure/worker-records.json +14 -0
- package/llms.txt +3 -1
- package/mise.lock +29 -0
- package/mise.toml +5 -3
- package/package.json +2 -2
- package/schema/check-ids.json +1967 -170
- package/schema/check-ids.schema.json +34 -0
- package/schema/fleet.schema.json +327 -8
- package/schema/output/check.schema.json +1 -1
- package/schema/output/fleet-state-definition.schema.json +14 -0
- package/schema/output/fleet-sync-schedule.schema.json +26 -0
- package/schema/output/fleet-sync.schema.json +18 -0
- package/schema/output/report.schema.json +3 -2
- package/schema/worker-record.schema.json +76 -0
- package/src/agents/converge.ts +140 -24
- package/src/agents/files.ts +2 -32
- package/src/agents/native-verify.ts +34 -3
- package/src/agents/secrets.ts +39 -4
- package/src/check-owners.ts +61 -2
- package/src/check-registry.ts +630 -0
- package/src/cli/agent-guide.ts +3 -3
- package/src/cli/cli.ts +4 -0
- package/src/cli/fleet-manifest.ts +9 -3
- package/src/cli/fleet-node-lifecycle.ts +313 -64
- package/src/cli/fleet-node-park.ts +238 -0
- package/src/cli/fleet-node-seed.ts +51 -35
- package/src/cli/fleet-state.ts +2 -1
- package/src/cli/fleet-sync-checkout.ts +7 -0
- package/src/cli/fleet-sync-enrollment.ts +193 -32
- package/src/cli/fleet-sync-forward.ts +4 -0
- package/src/cli/fleet-sync-manager.ts +103 -33
- package/src/cli/fleet-sync-output.ts +31 -11
- package/src/cli/fleet-sync-plan.ts +65 -9
- package/src/cli/fleet-sync-records.ts +113 -0
- package/src/cli/fleet-sync-remote.ts +151 -9
- package/src/cli/fleet-sync-schedule.ts +14 -4
- package/src/cli/fleet-sync-state.ts +28 -16
- package/src/cli/fleet-sync-vault.ts +77 -0
- package/src/cli/fleet-sync-worker.ts +214 -0
- package/src/cli/fleet-sync.ts +150 -19
- package/src/cli/fleet-validate.ts +59 -21
- package/src/cli/fleet-watch.ts +7 -1
- package/src/cli/fleet.ts +87 -5
- package/src/cli/managed-health.ts +25 -1
- package/src/cli/register-fleet.ts +115 -4
- package/src/cli/registry.ts +14 -0
- package/src/cli/tailnet.ts +43 -10
- package/src/cli/ui-data.ts +3 -1
- package/src/cli/worker-usage-report.ts +63 -0
- package/src/clock.ts +42 -0
- package/src/definition.ts +332 -9
- package/src/diagnostics.ts +5 -2
- package/src/exposure.ts +102 -0
- package/src/fleet-config.ts +3 -1
- package/src/git-identity.ts +76 -3
- package/src/ide/ide.ts +5 -0
- package/src/ide/t3.ts +12 -2
- package/src/input-hashes.ts +6 -0
- package/src/machine/machine-class.ts +59 -0
- package/src/machine/platform.ts +90 -11
- package/src/machine/state.ts +37 -15
- package/src/machine/support-matrix.ts +11 -9
- package/src/machine-health.ts +28 -5
- package/src/manager-health.ts +148 -2
- package/src/manager-sync.ts +65 -4
- package/src/monitoring/beszel.ts +8 -8
- package/src/monitoring/enroll.ts +8 -1
- package/src/monitoring/hub-access.ts +10 -4
- package/src/monitoring/systems.ts +138 -0
- package/src/network-definition.ts +13 -4
- package/src/node-converge-linux.ts +136 -27
- package/src/node-converge.ts +13 -3
- package/src/platform/ansible-receipt.ts +17 -2
- package/src/platform/ansible.ts +40 -4
- package/src/platform/manager-host.ts +25 -8
- package/src/platform/resource-verify.ts +18 -7
- package/src/platform/t3-policy.ts +24 -10
- package/src/platform/t3-service.ts +109 -34
- package/src/platform/ubuntu-release.ts +24 -0
- package/src/platform/ubuntu-verify.ts +31 -3
- package/src/platform/ubuntu-worker.ts +58 -0
- package/src/platform/worker-platform.ts +72 -0
- package/src/protocol.ts +1 -1
- package/src/public-reachability.ts +186 -0
- package/src/remote-bootstrap.ts +50 -12
- package/src/remote-operation.ts +18 -4
- package/src/repair-plan.ts +23 -12
- package/src/report.ts +2 -1
- package/src/schedule-check.ts +21 -2
- package/src/secrets.ts +4 -0
- package/src/service-auth.ts +55 -48
- package/src/ssh-connection.ts +141 -0
- package/src/ssh.ts +63 -24
- package/src/state/store.ts +4 -2
- package/src/state/sync.ts +35 -6
- package/src/state/workers.ts +140 -0
- package/src/t3/pairing.ts +59 -0
- package/src/t3/peer-link.ts +333 -0
- package/src/t3/version.ts +1 -1
- package/src/tailnet-cleanup.ts +185 -0
- package/src/tailnet-control.ts +346 -0
- package/src/tailscale.ts +1 -1
- package/src/toolchain-checks.ts +11 -1
- package/src/types.ts +4 -2
- package/src/worker-account.ts +63 -0
- package/src/worker-activity.ts +303 -0
- package/src/worker-capacity.ts +31 -0
- package/src/worker-exposure.ts +388 -0
- package/src/worker-features.ts +201 -0
- package/src/worker-host-checks.ts +71 -20
- package/src/worker-manifest.ts +27 -4
- package/src/worker-parking.ts +70 -0
- package/src/worker-policy.ts +17 -1
- package/src/worker-readiness.ts +15 -3
- package/src/worker-reboot.ts +129 -0
- package/src/worker-records.ts +305 -0
- package/src/worker-seed.ts +16 -11
- package/src/worker-sizing.ts +76 -0
- package/src/cli/schedule-logs.ts +0 -36
|
@@ -10,7 +10,8 @@ import { fleetState, type FleetStateOptions } from './fleet-state';
|
|
|
10
10
|
import { fleetAccounts, type FleetAccountsOptions } from './fleet-accounts';
|
|
11
11
|
import { fleetReport, type FleetReportOptions } from './fleet-report';
|
|
12
12
|
import { fleetWatch, watchServices } from './fleet-watch';
|
|
13
|
-
import type { NodeReport } from '../types';
|
|
13
|
+
import type { Check, NodeReport } from '../types';
|
|
14
|
+
import { fleet } from '../fleet-config';
|
|
14
15
|
import { registerGithubVerify } from './github-verify';
|
|
15
16
|
import { fleetRestore, type FleetRestoreOptions } from './fleet-restore';
|
|
16
17
|
import { fleetValidate, type FleetValidateOptions } from './fleet-validate';
|
|
@@ -30,6 +31,12 @@ import {
|
|
|
30
31
|
lifecycleServices,
|
|
31
32
|
type NodeLifecycleOptions,
|
|
32
33
|
} from './fleet-node-lifecycle';
|
|
34
|
+
import {
|
|
35
|
+
fleetNodePark,
|
|
36
|
+
fleetNodeResume,
|
|
37
|
+
parkServices,
|
|
38
|
+
type NodeParkOptions,
|
|
39
|
+
} from './fleet-node-park';
|
|
33
40
|
|
|
34
41
|
type Complete = (outcome: CommandOutcome) => void;
|
|
35
42
|
|
|
@@ -198,6 +205,10 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
198
205
|
'--revision <sha>',
|
|
199
206
|
'Converge to an exact source commit; defaults to the commit the dev-fleet package you run was published from'
|
|
200
207
|
)
|
|
208
|
+
.option(
|
|
209
|
+
'--rollback',
|
|
210
|
+
'Converge to the revision applied before the current one (fleet state); the revision, tools and T3 go back, system packages roll forward only'
|
|
211
|
+
)
|
|
201
212
|
.option(
|
|
202
213
|
'--node <id>',
|
|
203
214
|
'Explicit enrolled remote target; repeatable',
|
|
@@ -208,6 +219,10 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
208
219
|
'--vault-password-login',
|
|
209
220
|
'Opt-in: log a worker without a token of its own in with the shared Vault user, on its own terminal'
|
|
210
221
|
)
|
|
222
|
+
.option(
|
|
223
|
+
'--restart-busy',
|
|
224
|
+
'Restart a worker T3 for a new release or policy even while agent turns run, which ends them; by default the restart waits until T3 is idle'
|
|
225
|
+
)
|
|
211
226
|
.option(
|
|
212
227
|
'--canary <id>',
|
|
213
228
|
'The worker that proves a new revision first when it is reachable; defaults to canary in fleet.json, then the first worker'
|
|
@@ -225,6 +240,14 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
225
240
|
'Accepted for compatibility; fleet-sync applies by default (schedule writes still need --apply --yes)'
|
|
226
241
|
)
|
|
227
242
|
.option('--yes', 'Accepted for compatibility with --apply')
|
|
243
|
+
.option(
|
|
244
|
+
'--maintenance-reboot',
|
|
245
|
+
'Reboot each worker whose installed updates wait for a reboot: one at a time, canary first, only while it runs no agent turn, verified ready before the next'
|
|
246
|
+
)
|
|
247
|
+
.option(
|
|
248
|
+
'--force',
|
|
249
|
+
'With --maintenance-reboot: reboot a worker even while agent turns run (they end)'
|
|
250
|
+
)
|
|
228
251
|
.option(
|
|
229
252
|
'--take-over',
|
|
230
253
|
'Make this machine the manager while another one still holds the manager lease'
|
|
@@ -309,11 +332,11 @@ function registerState({ program, complete }: Registration) {
|
|
|
309
332
|
.option('--yes', 'With --restore: discard changes the bucket does not hold')
|
|
310
333
|
.option(
|
|
311
334
|
'--timestamp <time>',
|
|
312
|
-
'With --restore: the version the bucket held at this time, uploaded again as the current one (a damaged replica)'
|
|
335
|
+
'With --restore: the version the bucket held at this time, uploaded again as the current one (a damaged replica); refused while another manager holds the manager lease, unless with --take-over'
|
|
313
336
|
)
|
|
314
337
|
.option(
|
|
315
338
|
'--take-over',
|
|
316
|
-
'With --init or --
|
|
339
|
+
'With --init, --import or --restore --timestamp: change the fleet state while another manager still holds the manager lease'
|
|
317
340
|
)
|
|
318
341
|
.addHelpText(
|
|
319
342
|
'after',
|
|
@@ -361,10 +384,52 @@ function registerWatch({ program, defaults, complete }: Registration) {
|
|
|
361
384
|
const data = report.data as { nodes?: NodeReport[] };
|
|
362
385
|
return data.nodes ?? [];
|
|
363
386
|
};
|
|
387
|
+
const autoPark = async (reports: NodeReport[]): Promise<Check[]> => {
|
|
388
|
+
const checks: Check[] = [];
|
|
389
|
+
for (const report of reports) {
|
|
390
|
+
if (
|
|
391
|
+
!report.checks.some(
|
|
392
|
+
(check) => check.id === 'worker:idle' && check.status === 'warn'
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
continue;
|
|
396
|
+
const worker = fleet.workers.find(
|
|
397
|
+
(entry) =>
|
|
398
|
+
entry.id === report.node ||
|
|
399
|
+
entry.network?.tailscaleName === report.node
|
|
400
|
+
);
|
|
401
|
+
if (!worker) continue;
|
|
402
|
+
const parked = await fleetNodePark(
|
|
403
|
+
{ id: worker.id },
|
|
404
|
+
parkServices(settings.config, () =>
|
|
405
|
+
Promise.reject(new Error('fleet-watch never resumes a worker'))
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
checks.push({
|
|
409
|
+
id: 'worker:auto-park',
|
|
410
|
+
status: parked.exitCode === 0 ? 'pass' : 'warn',
|
|
411
|
+
summary:
|
|
412
|
+
parked.exitCode === 0
|
|
413
|
+
? `${worker.id} was idle longer than declared and is parked.`
|
|
414
|
+
: 'WORKER_AUTO_PARK_REFUSED',
|
|
415
|
+
...(parked.exitCode === 0
|
|
416
|
+
? {}
|
|
417
|
+
: {
|
|
418
|
+
remedy: {
|
|
419
|
+
action: `fleet-node-park refused ${worker.id}; run devfleet fleet-node-park --id ${worker.id} to see why.`,
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return checks;
|
|
425
|
+
};
|
|
364
426
|
complete(
|
|
365
427
|
await fleetWatch(
|
|
366
428
|
{ ...(options.test ? { test: true } : {}) },
|
|
367
|
-
|
|
429
|
+
{
|
|
430
|
+
...watchServices(status),
|
|
431
|
+
...(fleet.parking?.autoPark ? { autoPark } : {}),
|
|
432
|
+
}
|
|
368
433
|
)
|
|
369
434
|
);
|
|
370
435
|
});
|
|
@@ -448,6 +513,52 @@ function registerLifecycle({ program, defaults, complete }: Registration) {
|
|
|
448
513
|
)
|
|
449
514
|
.option('--yes', 'Confirm an explicitly requested --apply operation')
|
|
450
515
|
.action(action('retire'));
|
|
516
|
+
const parking =
|
|
517
|
+
(name: 'park' | 'resume') =>
|
|
518
|
+
async (_options: unknown, command: Command) => {
|
|
519
|
+
const options = command.optsWithGlobals<NodeParkOptions & CliOptions>();
|
|
520
|
+
const settings = await withSettings(defaults, options);
|
|
521
|
+
const services = parkServices(settings.config, (id) =>
|
|
522
|
+
fleetSync(
|
|
523
|
+
{
|
|
524
|
+
node: [id],
|
|
525
|
+
apply: true,
|
|
526
|
+
yes: true,
|
|
527
|
+
nonInteractive: true,
|
|
528
|
+
...(options.json ? { json: true } : {}),
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
...defaults,
|
|
532
|
+
...(options.config ? { config: options.config } : {}),
|
|
533
|
+
}
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
complete(
|
|
537
|
+
name === 'park'
|
|
538
|
+
? await fleetNodePark(options, services)
|
|
539
|
+
: await fleetNodeResume(options, services)
|
|
540
|
+
);
|
|
541
|
+
};
|
|
542
|
+
program
|
|
543
|
+
.command('fleet-node-park')
|
|
544
|
+
.description(
|
|
545
|
+
'Park a worker that is stopped or destroyed on purpose: it is no longer contacted, never the canary and does not fail the fleet'
|
|
546
|
+
)
|
|
547
|
+
.requiredOption('--id <name>', 'The declared worker to park')
|
|
548
|
+
.option(
|
|
549
|
+
'--force',
|
|
550
|
+
'Park although agent turns are under way on it, or cannot be counted'
|
|
551
|
+
)
|
|
552
|
+
.option('--dry-run', 'Say what would change; change nothing')
|
|
553
|
+
.action(parking('park'));
|
|
554
|
+
program
|
|
555
|
+
.command('fleet-node-resume')
|
|
556
|
+
.description(
|
|
557
|
+
'Bring a parked worker back: run the declared start command, then fleet-sync that worker and verify it'
|
|
558
|
+
)
|
|
559
|
+
.requiredOption('--id <name>', 'The declared worker to resume')
|
|
560
|
+
.option('--dry-run', 'Say what would change; change nothing')
|
|
561
|
+
.action(parking('resume'));
|
|
451
562
|
program
|
|
452
563
|
.command('fleet-node-seed')
|
|
453
564
|
.description(
|
package/src/cli/registry.ts
CHANGED
|
@@ -207,6 +207,20 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
207
207
|
['seed', 'cloud-init', 'autoinstall', 'new', 'bootstrap', 'provision'],
|
|
208
208
|
true,
|
|
209
209
|
],
|
|
210
|
+
[
|
|
211
|
+
'fleet-node-park',
|
|
212
|
+
'Park a worker that is off on purpose',
|
|
213
|
+
'Record in the fleet state that a declared worker is stopped or destroyed on purpose (a rented machine nobody uses): fleet-sync and fleet-status list it as parked with the time since, never contact it or make it the canary, and it does not change the exit code. Refuses while agent turns run on it, unless --force. Runs the stop command fleet.json declares under parking.stop with the worker ID. --dry-run previews.',
|
|
214
|
+
['park', 'stop', 'pause', 'idle', 'cost', 'rented', 'shutdown'],
|
|
215
|
+
true,
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
'fleet-node-resume',
|
|
219
|
+
'Resume a parked worker',
|
|
220
|
+
'Run the start command fleet.json declares under parking.start with the worker ID, mark the worker active in the fleet state, then run fleet-sync --node ID, which converges and verifies it. The exit code is the sync’s. --dry-run previews.',
|
|
221
|
+
['resume', 'start', 'unpark', 'wake', 'rented'],
|
|
222
|
+
true,
|
|
223
|
+
],
|
|
210
224
|
[
|
|
211
225
|
'fleet-node-retire',
|
|
212
226
|
'Retire a worker',
|
package/src/cli/tailnet.ts
CHANGED
|
@@ -5,13 +5,19 @@ import {
|
|
|
5
5
|
appendManagerLinks,
|
|
6
6
|
appendDashboardEvidence,
|
|
7
7
|
} from './managed-health';
|
|
8
|
-
import { discoverTailnet } from '../tailscale';
|
|
8
|
+
import { discoverTailnet, type Tailnet } from '../tailscale';
|
|
9
|
+
import type { FleetNode, NodeReport } from '../types';
|
|
10
|
+
import { appendPublicReachability } from '../public-reachability';
|
|
9
11
|
import { tailnetHealth } from '../tailnet-health';
|
|
10
12
|
import { makeReport, renderReport, reportExitCode } from '../report';
|
|
11
13
|
import { readRoster } from './roster';
|
|
12
14
|
import { publicHubUrl } from '../monitoring/beszel';
|
|
13
15
|
import { stableUrls } from './fleet-sync-output';
|
|
14
16
|
import { timeout } from './options';
|
|
17
|
+
import { appendUsage } from './worker-usage-report';
|
|
18
|
+
import { appendTailnetLeftovers } from '../tailnet-cleanup';
|
|
19
|
+
import { fleet } from '../fleet-config';
|
|
20
|
+
import { withUsage } from '../worker-activity';
|
|
15
21
|
import type { FleetServices } from './fleet';
|
|
16
22
|
import type { Settings } from './settings';
|
|
17
23
|
import type { CliOptions } from './options';
|
|
@@ -57,12 +63,21 @@ export async function executeTailnet(
|
|
|
57
63
|
);
|
|
58
64
|
if (options.managed)
|
|
59
65
|
await appendManagerLinks(nodes, registered, services.run);
|
|
66
|
+
// From the manager, outside the tailnet: optional, it never blocks readiness.
|
|
67
|
+
if (options.workers) await appendPublicReachability(nodes, services.run);
|
|
60
68
|
// The hub runs on the manager, which worker scope deliberately filters out of
|
|
61
69
|
// `tailnet`; its visibility comes from the unfiltered discovery.
|
|
62
70
|
const dashboard = options.workers
|
|
63
|
-
? await
|
|
71
|
+
? await workerFleetEvidence(nodes, discovered, registered, services)
|
|
64
72
|
: null;
|
|
65
|
-
|
|
73
|
+
// Parked workers, and what each active one is doing (uptime, agent turns).
|
|
74
|
+
const usage = options.managed
|
|
75
|
+
? await appendUsage(nodes, registered, {
|
|
76
|
+
run: services.run,
|
|
77
|
+
computers: discovered.computers,
|
|
78
|
+
})
|
|
79
|
+
: null;
|
|
80
|
+
const report = { ...makeReport('health', nodes), ...(usage ?? {}) };
|
|
66
81
|
const exitCode = reportExitCode(report, true);
|
|
67
82
|
const verified = exitCode === 0;
|
|
68
83
|
return {
|
|
@@ -77,16 +92,34 @@ export async function executeTailnet(
|
|
|
77
92
|
inspected: nodes.filter((node) => node.snapshot).length,
|
|
78
93
|
},
|
|
79
94
|
},
|
|
80
|
-
text:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
text: withUsage(
|
|
96
|
+
options.workers
|
|
97
|
+
? workerHealthText(
|
|
98
|
+
{ ...report, ok: verified },
|
|
99
|
+
coverage,
|
|
100
|
+
publicDashboard(dashboard)
|
|
101
|
+
)
|
|
102
|
+
: `${stableUrls(renderReport({ ...report, ok: verified }))}\nCoverage: ${coverage.computers} visible computers; ${coverage.excludedMobile} mobile devices excluded.\n${coverage.limitation}`,
|
|
103
|
+
usage
|
|
104
|
+
),
|
|
87
105
|
};
|
|
88
106
|
}
|
|
89
107
|
|
|
108
|
+
/** What the canonical worker fleet is checked for beyond each worker: the
|
|
109
|
+
* dashboard's evidence, and devices tagged as workers that are no declared
|
|
110
|
+
* worker (#123). Returns the private hub address.
|
|
111
|
+
*/
|
|
112
|
+
async function workerFleetEvidence(
|
|
113
|
+
nodes: NodeReport[],
|
|
114
|
+
discovered: Tailnet,
|
|
115
|
+
registered: FleetNode[],
|
|
116
|
+
services: FleetServices
|
|
117
|
+
): Promise<string | null> {
|
|
118
|
+
const dashboard = await appendDashboardEvidence(nodes, discovered);
|
|
119
|
+
await appendTailnetLeftovers(nodes, fleet, services.tailnet, registered);
|
|
120
|
+
return dashboard;
|
|
121
|
+
}
|
|
122
|
+
|
|
90
123
|
/** Shown to people: the stable public name, never the tailnet address. */
|
|
91
124
|
function publicDashboard(privateHub: string | null): string | null {
|
|
92
125
|
return privateHub ? publicHubUrl() : null;
|
package/src/cli/ui-data.ts
CHANGED
|
@@ -86,7 +86,9 @@ function nodeChecks(value: unknown): CheckRow[] {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function isStatus(value: unknown): value is string {
|
|
89
|
-
return ['pass', 'warn', 'fail', 'skip'].includes(
|
|
89
|
+
return ['pass', 'warn', 'fail', 'skip', 'not-applicable'].includes(
|
|
90
|
+
String(value)
|
|
91
|
+
);
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
export function isCancellation(error: unknown): boolean {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fleet } from '../fleet-config';
|
|
2
|
+
import { readParked } from '../state/workers';
|
|
3
|
+
import type { TailnetComputer } from '../tailscale';
|
|
4
|
+
import type { FleetNode, NodeReport, Runner } from '../types';
|
|
5
|
+
import { fleetUsage, usageSummary, type WorkerUsage } from '../worker-activity';
|
|
6
|
+
import { parkedCheck, parkedOnline } from '../worker-parking';
|
|
7
|
+
|
|
8
|
+
/** The workers fleet-status contacts: every one that is not parked. */
|
|
9
|
+
export function activeNodes(nodes: FleetNode[]): FleetNode[] {
|
|
10
|
+
const parked = readParked();
|
|
11
|
+
return nodes.filter((node) => !parked[node.id]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function reportOf(reports: NodeReport[], id: string): NodeReport {
|
|
15
|
+
const worker = fleet.workers.find((entry) => entry.id === id);
|
|
16
|
+
const found = reports.find(
|
|
17
|
+
(entry) =>
|
|
18
|
+
entry.node === id ||
|
|
19
|
+
(worker?.network && entry.node === worker.network.tailscaleName)
|
|
20
|
+
);
|
|
21
|
+
if (found) return found;
|
|
22
|
+
const added: NodeReport = { node: id, checks: [] };
|
|
23
|
+
reports.push(added);
|
|
24
|
+
return added;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Adds to fleet-status what it says about each worker beyond its health:
|
|
28
|
+
* parked workers as parked (and a warning when one is online anyway), and
|
|
29
|
+
* for every active worker it reaches its uptime, agent turns, last agent
|
|
30
|
+
* activity and `worker:idle`. Returns the `workers` of the JSON output and
|
|
31
|
+
* its `usage` summary.
|
|
32
|
+
*/
|
|
33
|
+
export async function appendUsage(
|
|
34
|
+
reports: NodeReport[],
|
|
35
|
+
nodes: FleetNode[],
|
|
36
|
+
{
|
|
37
|
+
run,
|
|
38
|
+
computers = [],
|
|
39
|
+
now = new Date(),
|
|
40
|
+
}: { run: Runner; computers?: TailnetComputer[]; now?: Date }
|
|
41
|
+
): Promise<{ workers: WorkerUsage[]; usage: ReturnType<typeof usageSummary> }> {
|
|
42
|
+
const parked = readParked();
|
|
43
|
+
for (const worker of fleet.workers) {
|
|
44
|
+
const at = parked[worker.id];
|
|
45
|
+
if (!at) continue;
|
|
46
|
+
const report = reportOf(reports, worker.id);
|
|
47
|
+
report.checks.push(parkedCheck(worker.id, at, now));
|
|
48
|
+
const online = parkedOnline(
|
|
49
|
+
worker.id,
|
|
50
|
+
worker.network?.tailscaleName,
|
|
51
|
+
computers
|
|
52
|
+
);
|
|
53
|
+
if (online) report.checks.push(online);
|
|
54
|
+
}
|
|
55
|
+
const { workers, checks } = await fleetUsage(fleet, {
|
|
56
|
+
nodes: nodes.filter((node) => !parked[node.id]),
|
|
57
|
+
parked,
|
|
58
|
+
run,
|
|
59
|
+
now,
|
|
60
|
+
});
|
|
61
|
+
for (const [id, check] of checks) reportOf(reports, id).checks.push(check);
|
|
62
|
+
return { workers, usage: usageSummary(workers) };
|
|
63
|
+
}
|
package/src/clock.ts
CHANGED
|
@@ -53,6 +53,48 @@ export function clockCheck(
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** The offset of a worker's clock from the manager's, measured by the
|
|
57
|
+
* manager around one SSH round trip: the worker read its clock between
|
|
58
|
+
* `sentAt` and `receivedAt` (milliseconds on the manager's clock), so the
|
|
59
|
+
* offset is only known to lie in that window. The check judges the smallest
|
|
60
|
+
* offset the window allows, so a slow link never reads as a skewed clock.
|
|
61
|
+
*/
|
|
62
|
+
export function clockOffsetCheck(
|
|
63
|
+
remoteSeconds: number,
|
|
64
|
+
sentAt: number,
|
|
65
|
+
receivedAt: number
|
|
66
|
+
): Check {
|
|
67
|
+
const remote = remoteSeconds * 1000;
|
|
68
|
+
const least =
|
|
69
|
+
remote > receivedAt
|
|
70
|
+
? (remote - receivedAt) / 1000
|
|
71
|
+
: remote < sentAt
|
|
72
|
+
? (sentAt - remote) / 1000
|
|
73
|
+
: 0;
|
|
74
|
+
const direction = remote > receivedAt ? 'ahead of' : 'behind';
|
|
75
|
+
const measured = `${least.toFixed(1)} s ${direction} the manager`;
|
|
76
|
+
const remedy = clockRemedy(undefined);
|
|
77
|
+
if (least > failOffsetSeconds)
|
|
78
|
+
return {
|
|
79
|
+
id: 'worker:clock-offset',
|
|
80
|
+
status: 'fail',
|
|
81
|
+
summary: `CLOCK_SKEWED ${measured}`,
|
|
82
|
+
remedy,
|
|
83
|
+
};
|
|
84
|
+
if (least > warnOffsetSeconds)
|
|
85
|
+
return {
|
|
86
|
+
id: 'worker:clock-offset',
|
|
87
|
+
status: 'warn',
|
|
88
|
+
summary: `CLOCK_DRIFTING ${measured}`,
|
|
89
|
+
remedy,
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
id: 'worker:clock-offset',
|
|
93
|
+
status: 'pass',
|
|
94
|
+
summary: `The clock is within ${warnOffsetSeconds} s of the manager's.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
56
98
|
function clockRemedy(timeService: number | undefined): { action: string } {
|
|
57
99
|
return {
|
|
58
100
|
action:
|