@crvouga/dev-fleet 0.0.0-stage → 0.1.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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +75 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +252 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +260 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +88 -0
- package/src/index.ts +6 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +185 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +128 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +63 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +212 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { localCheckoutRevision } from '../machine/local-source';
|
|
2
|
+
import { fleet as canonicalDefinition } from '../fleet-config';
|
|
3
|
+
import { managerFailure } from '../manager-failure';
|
|
4
|
+
import { managerText, stdoutColor } from './fleet-sync-output';
|
|
5
|
+
import { managerSchedule, type ScheduleAction } from './fleet-sync-schedule';
|
|
6
|
+
import { enrollManager } from './fleet-sync-enrollment';
|
|
7
|
+
import { authorizeManager } from './fleet-sync-vault';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { existsSync, renameSync } from 'node:fs';
|
|
11
|
+
import {
|
|
12
|
+
readEnrollment,
|
|
13
|
+
isRevision,
|
|
14
|
+
saveEnrollment,
|
|
15
|
+
withMachineLock,
|
|
16
|
+
ensurePrivateRoot,
|
|
17
|
+
} from '../machine/state';
|
|
18
|
+
import { runCommand } from '../runner';
|
|
19
|
+
import type { Runner } from '../types';
|
|
20
|
+
import type { Settings } from './settings';
|
|
21
|
+
import type { CommandOutcome } from './contracts';
|
|
22
|
+
import { FleetUsageError } from '../errors';
|
|
23
|
+
import { FleetLockError } from '../machine/lock-error';
|
|
24
|
+
import { selected } from './fleet-sync-remote';
|
|
25
|
+
import { managerIdentity } from './fleet-sync-manager';
|
|
26
|
+
import { trackedScript } from '../remote-bootstrap';
|
|
27
|
+
import { rehome } from './fleet-sync-checkout';
|
|
28
|
+
import {
|
|
29
|
+
forwardingDisabled,
|
|
30
|
+
forwardToManager,
|
|
31
|
+
type ForwardSpawn,
|
|
32
|
+
} from './fleet-sync-forward';
|
|
33
|
+
|
|
34
|
+
declare const PERSONAL_SOURCE_REVISION: string | null;
|
|
35
|
+
export interface FleetSyncOptions {
|
|
36
|
+
revision?: string;
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
apply?: boolean;
|
|
39
|
+
yes?: boolean;
|
|
40
|
+
json?: boolean;
|
|
41
|
+
nonInteractive?: boolean;
|
|
42
|
+
node?: string[];
|
|
43
|
+
all?: boolean;
|
|
44
|
+
schedule?: ScheduleAction;
|
|
45
|
+
/** Internal: this revision already converged the whole fleet, so no canary
|
|
46
|
+
* has to prove it first and every worker converges at once.
|
|
47
|
+
*/
|
|
48
|
+
rolledOut?: boolean;
|
|
49
|
+
/** Internal: set for interactive terminal runs (sudo enrollment prompts). */
|
|
50
|
+
interactiveAuthorization?: boolean;
|
|
51
|
+
expectedPlanFingerprint?: string;
|
|
52
|
+
}
|
|
53
|
+
export function machineRoot(): string {
|
|
54
|
+
const root = join(homedir(), '.local/share/personal/fleet/manager');
|
|
55
|
+
// One-time move from the pre-topology name; keeps the approved revision.
|
|
56
|
+
const legacy = join(homedir(), '.local/share/personal/fleet/controller');
|
|
57
|
+
if (!existsSync(root) && existsSync(legacy)) renameSync(legacy, root);
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Public contract v3: the manager converges itself, then every enrolled worker. */
|
|
62
|
+
export async function fleetSync(
|
|
63
|
+
options: FleetSyncOptions,
|
|
64
|
+
settings: Settings,
|
|
65
|
+
run: Runner = runCommand,
|
|
66
|
+
forward?: ForwardSpawn
|
|
67
|
+
): Promise<CommandOutcome> {
|
|
68
|
+
validate(options);
|
|
69
|
+
if (!(await onManager(run)))
|
|
70
|
+
return offManager(options, settings, run, forward);
|
|
71
|
+
if (options.schedule === 'status') return scheduleStatus(settings, run);
|
|
72
|
+
const root = machineRoot();
|
|
73
|
+
await ensurePrivateRoot(root);
|
|
74
|
+
const state = await readEnrollment(root);
|
|
75
|
+
const revision = await selectedRevision(
|
|
76
|
+
options.revision,
|
|
77
|
+
state?.approvedRevision,
|
|
78
|
+
settings,
|
|
79
|
+
run
|
|
80
|
+
);
|
|
81
|
+
if (!isRevision(revision))
|
|
82
|
+
return actionRequired(
|
|
83
|
+
'FLEET_REVISION_REQUIRED',
|
|
84
|
+
'Select an approved full SHA with --revision, or use a published package containing clean source provenance.'
|
|
85
|
+
);
|
|
86
|
+
if (options.schedule)
|
|
87
|
+
return scheduleRequest(options, revision, settings, run);
|
|
88
|
+
const rehomed = await rehome(
|
|
89
|
+
options,
|
|
90
|
+
{ revision, workspace: settings.workspace },
|
|
91
|
+
run
|
|
92
|
+
);
|
|
93
|
+
if (rehomed) return rehomed;
|
|
94
|
+
const request = { ...options, revision };
|
|
95
|
+
if (request.dryRun) return selected(request, settings, run);
|
|
96
|
+
// Applies without a confirmation prompt; --dry-run is the preview.
|
|
97
|
+
request.apply = true;
|
|
98
|
+
request.yes = true;
|
|
99
|
+
if (interactive(options)) {
|
|
100
|
+
await enrollManager(settings.config, run);
|
|
101
|
+
// Needed to hand each node its own Vault token; nodes report any gap.
|
|
102
|
+
await authorizeManager({ interactive: true }, run);
|
|
103
|
+
request.interactiveAuthorization = true;
|
|
104
|
+
}
|
|
105
|
+
return applyLocked(root, state, request, { settings, run });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Records the approved revision, converges, then records what was applied. */
|
|
109
|
+
function applyLocked(
|
|
110
|
+
root: string,
|
|
111
|
+
state: Awaited<ReturnType<typeof readEnrollment>>,
|
|
112
|
+
request: FleetSyncOptions & { revision: string },
|
|
113
|
+
context: { settings: Settings; run: Runner }
|
|
114
|
+
): Promise<CommandOutcome> {
|
|
115
|
+
const { revision } = request;
|
|
116
|
+
const { settings, run } = context;
|
|
117
|
+
return withMachineLock(root, async () => {
|
|
118
|
+
const approval = {
|
|
119
|
+
...state,
|
|
120
|
+
version: 1 as const,
|
|
121
|
+
id: 'manager',
|
|
122
|
+
approvedRevision: revision,
|
|
123
|
+
};
|
|
124
|
+
await saveEnrollment(root, approval);
|
|
125
|
+
const result = await selected(
|
|
126
|
+
{ ...request, rolledOut: state?.appliedRevision === revision },
|
|
127
|
+
settings,
|
|
128
|
+
run
|
|
129
|
+
);
|
|
130
|
+
if (result.exitCode === 0)
|
|
131
|
+
await saveEnrollment(root, {
|
|
132
|
+
...approval,
|
|
133
|
+
appliedRevision: revision,
|
|
134
|
+
...(state?.appliedRevision && state.appliedRevision !== revision
|
|
135
|
+
? { previousRevision: state.appliedRevision }
|
|
136
|
+
: {}),
|
|
137
|
+
});
|
|
138
|
+
return result;
|
|
139
|
+
}).catch((error: unknown) => {
|
|
140
|
+
if (error instanceof FleetLockError)
|
|
141
|
+
return actionRequired(
|
|
142
|
+
'FLEET_MANAGER_LOCKED',
|
|
143
|
+
'Another manager run or interrupted lock exists. Inspect manager and remote processes before removing an abandoned sync.lock; never steal active package-manager locks.'
|
|
144
|
+
);
|
|
145
|
+
throw error;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Every run (previews and schedules too) happens on the manager: a worker can
|
|
149
|
+
* neither converge the manager's singletons nor observe its T3 links, so a
|
|
150
|
+
* partial run there only produces misleading failures.
|
|
151
|
+
*/
|
|
152
|
+
async function onManager(run: Runner): Promise<boolean> {
|
|
153
|
+
const { manager } = canonicalDefinition;
|
|
154
|
+
return (await managerIdentity(manager.id, run)).status === 'pass';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Hands the request to the manager; refuses (without touching any worker)
|
|
158
|
+
* when hand-off is disabled, e.g. on the manager side of a hand-off.
|
|
159
|
+
*/
|
|
160
|
+
async function offManager(
|
|
161
|
+
options: FleetSyncOptions,
|
|
162
|
+
settings: Settings,
|
|
163
|
+
run: Runner,
|
|
164
|
+
forward: ForwardSpawn | undefined
|
|
165
|
+
): Promise<CommandOutcome> {
|
|
166
|
+
const definition = canonicalDefinition;
|
|
167
|
+
const revision =
|
|
168
|
+
options.revision ??
|
|
169
|
+
packageRevision() ??
|
|
170
|
+
(await localCheckoutRevision(settings.workspace, run));
|
|
171
|
+
if (forwardingDisabled())
|
|
172
|
+
return actionRequired(
|
|
173
|
+
'FLEET_NOT_MANAGER',
|
|
174
|
+
`fleet-sync runs only on the manager (${definition.manager.id}); nothing was changed. Run it there, or from a worker that can SSH to the manager.`,
|
|
175
|
+
revision ?? null
|
|
176
|
+
);
|
|
177
|
+
const runScript = revision
|
|
178
|
+
? await trackedScript(
|
|
179
|
+
settings.workspace,
|
|
180
|
+
revision,
|
|
181
|
+
'bootstrap/fleet-manager-run.sh',
|
|
182
|
+
run
|
|
183
|
+
)
|
|
184
|
+
: null;
|
|
185
|
+
return forwardToManager(definition, options, revision, {
|
|
186
|
+
spawn: forward,
|
|
187
|
+
script: runScript,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function scheduleStatus(
|
|
192
|
+
settings: Settings,
|
|
193
|
+
run: Runner
|
|
194
|
+
): Promise<CommandOutcome> {
|
|
195
|
+
return managerSchedule(
|
|
196
|
+
{
|
|
197
|
+
action: 'status',
|
|
198
|
+
revision: '',
|
|
199
|
+
config: settings.config,
|
|
200
|
+
authorized: false,
|
|
201
|
+
},
|
|
202
|
+
run
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function actionRequired(
|
|
207
|
+
code: string,
|
|
208
|
+
remedy: string,
|
|
209
|
+
revision: string | null = null
|
|
210
|
+
): CommandOutcome {
|
|
211
|
+
const report = managerFailure(canonicalDefinition, revision, code, remedy);
|
|
212
|
+
return {
|
|
213
|
+
exitCode: 1,
|
|
214
|
+
data: report,
|
|
215
|
+
text: managerText(report, { color: stdoutColor() }),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function validate(options: FleetSyncOptions): void {
|
|
220
|
+
validateSchedule(options.schedule);
|
|
221
|
+
if (options.all && options.node?.length)
|
|
222
|
+
throw new FleetUsageError('Choose --all or --node, not both.');
|
|
223
|
+
if (options.revision !== undefined && !isRevision(options.revision))
|
|
224
|
+
throw new FleetUsageError(
|
|
225
|
+
'--revision requires an exact lowercase 40-character commit SHA.'
|
|
226
|
+
);
|
|
227
|
+
if (options.dryRun && (options.apply || options.yes))
|
|
228
|
+
throw new FleetUsageError(
|
|
229
|
+
'--dry-run cannot be combined with apply confirmation.'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
function interactive(options: FleetSyncOptions): boolean {
|
|
233
|
+
return (
|
|
234
|
+
![
|
|
235
|
+
options.dryRun,
|
|
236
|
+
options.apply,
|
|
237
|
+
options.yes,
|
|
238
|
+
options.json,
|
|
239
|
+
options.nonInteractive,
|
|
240
|
+
process.env['CI'],
|
|
241
|
+
].some(Boolean) && Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function packageRevision(): string | undefined {
|
|
246
|
+
return typeof PERSONAL_SOURCE_REVISION === 'string'
|
|
247
|
+
? PERSONAL_SOURCE_REVISION
|
|
248
|
+
: undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function scheduleRequest(
|
|
252
|
+
options: FleetSyncOptions,
|
|
253
|
+
revision: string,
|
|
254
|
+
settings: Settings,
|
|
255
|
+
run: Runner
|
|
256
|
+
): Promise<CommandOutcome> {
|
|
257
|
+
const root = machineRoot();
|
|
258
|
+
|
|
259
|
+
if (options.dryRun)
|
|
260
|
+
return actionRequired(
|
|
261
|
+
'FLEET_SCHEDULE_PREVIEW',
|
|
262
|
+
'No schedule changed. Inspect --schedule status; scheduling writes require --apply --yes.'
|
|
263
|
+
);
|
|
264
|
+
if (!options.apply || !options.yes)
|
|
265
|
+
return managerSchedule(
|
|
266
|
+
{
|
|
267
|
+
action: options.schedule!,
|
|
268
|
+
revision,
|
|
269
|
+
config: settings.config,
|
|
270
|
+
authorized: false,
|
|
271
|
+
},
|
|
272
|
+
run
|
|
273
|
+
);
|
|
274
|
+
return withMachineLock(root, () =>
|
|
275
|
+
managerSchedule(
|
|
276
|
+
{
|
|
277
|
+
action: options.schedule!,
|
|
278
|
+
revision,
|
|
279
|
+
config: settings.config,
|
|
280
|
+
authorized: Boolean(options.apply && options.yes),
|
|
281
|
+
},
|
|
282
|
+
run
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function validateSchedule(schedule: unknown): void {
|
|
288
|
+
if (
|
|
289
|
+
schedule !== undefined &&
|
|
290
|
+
!['enable', 'pause', 'resume', 'disable', 'status'].includes(
|
|
291
|
+
String(schedule)
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
throw new FleetUsageError('Unknown manager schedule action.');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function selectedRevision(
|
|
298
|
+
explicit: string | undefined,
|
|
299
|
+
approved: string | undefined,
|
|
300
|
+
settings: Settings,
|
|
301
|
+
run: Runner
|
|
302
|
+
): Promise<string | undefined> {
|
|
303
|
+
return (
|
|
304
|
+
explicit ??
|
|
305
|
+
packageRevision() ??
|
|
306
|
+
(await localCheckoutRevision(settings.workspace, run)) ??
|
|
307
|
+
approved
|
|
308
|
+
);
|
|
309
|
+
}
|
package/src/cli/fleet.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { selectNodes } from '../config';
|
|
2
|
+
import { doctor } from '../manager';
|
|
3
|
+
import { FleetUsageError } from '../errors';
|
|
4
|
+
import { onboarding } from '../onboard';
|
|
5
|
+
import { makeReport, renderReport, reportExitCode } from '../report';
|
|
6
|
+
import { runCommand } from '../runner';
|
|
7
|
+
import schema from '../../schema/roster.schema.json';
|
|
8
|
+
import type { FleetConfig, FleetNode, NodeReport, Runner } from '../types';
|
|
9
|
+
import type { CommandOutcome } from './contracts';
|
|
10
|
+
import type { Settings } from './settings';
|
|
11
|
+
import { capabilities, timeout, validateOptions } from './options';
|
|
12
|
+
import type { CliOptions } from './options';
|
|
13
|
+
import { addNode, nodeFromOptions, readRoster } from './roster';
|
|
14
|
+
import { executeTailnet } from './tailnet';
|
|
15
|
+
import { probeSource } from './probe-source';
|
|
16
|
+
import { executeRepairCommand } from './repair';
|
|
17
|
+
|
|
18
|
+
export interface FleetServices {
|
|
19
|
+
run: Runner;
|
|
20
|
+
source: () => Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SERVICES: FleetServices = { run: runCommand, source: probeSource };
|
|
24
|
+
|
|
25
|
+
export async function executeFleet(
|
|
26
|
+
command: string,
|
|
27
|
+
options: CliOptions,
|
|
28
|
+
settings: Settings,
|
|
29
|
+
services: FleetServices = SERVICES
|
|
30
|
+
): Promise<CommandOutcome> {
|
|
31
|
+
validateOptions(command, options);
|
|
32
|
+
if (['repair', 'repairs'].includes(command))
|
|
33
|
+
return executeRepairCommand(command, options, settings, services);
|
|
34
|
+
if (['discover', 'health'].includes(command))
|
|
35
|
+
return executeTailnet(command, options, settings, services);
|
|
36
|
+
if (command === 'schema') return result(schema);
|
|
37
|
+
if (command === 'add') return register(settings, options);
|
|
38
|
+
const roster = options.local ? null : await readRoster(settings.config);
|
|
39
|
+
const nodes = resolveNodes(command, options, roster);
|
|
40
|
+
if (['nodes', 'onboard'].includes(command))
|
|
41
|
+
return rosterResult(command, nodes, roster !== null);
|
|
42
|
+
return diagnose(
|
|
43
|
+
nodes,
|
|
44
|
+
options,
|
|
45
|
+
{ ...settings, missingRoster: !roster },
|
|
46
|
+
services
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rosterResult(
|
|
51
|
+
command: string,
|
|
52
|
+
nodes: FleetNode[],
|
|
53
|
+
configured: boolean
|
|
54
|
+
): CommandOutcome {
|
|
55
|
+
return result({
|
|
56
|
+
version: 1,
|
|
57
|
+
command,
|
|
58
|
+
ok: true,
|
|
59
|
+
...(command === 'nodes' ? { configured } : {}),
|
|
60
|
+
nodes:
|
|
61
|
+
command === 'onboard'
|
|
62
|
+
? nodes.map((node) => onboarding(node, 'devfleet'))
|
|
63
|
+
: nodes,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveNodes(
|
|
68
|
+
command: string,
|
|
69
|
+
options: CliOptions,
|
|
70
|
+
roster: FleetConfig | null
|
|
71
|
+
): FleetNode[] {
|
|
72
|
+
if (!roster && !options.local && !['doctor', 'nodes'].includes(command))
|
|
73
|
+
throw new FleetUsageError(
|
|
74
|
+
'No fleet roster. Use fleet-node-add or the interactive Register action; use fleet-status --local for this machine.'
|
|
75
|
+
);
|
|
76
|
+
if (options.node?.length && !roster)
|
|
77
|
+
throw new FleetUsageError(
|
|
78
|
+
'Cannot select nodes without a roster. Use fleet-node-add first.'
|
|
79
|
+
);
|
|
80
|
+
return roster ? selectNodes(roster, options.node ?? []) : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function result(data: unknown, text?: string): CommandOutcome {
|
|
84
|
+
return { exitCode: 0, data, ...(text ? { text } : {}) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function register(
|
|
88
|
+
settings: Settings,
|
|
89
|
+
options: CliOptions
|
|
90
|
+
): Promise<CommandOutcome> {
|
|
91
|
+
const node = nodeFromOptions(options);
|
|
92
|
+
const config = await addNode(settings.config, node, options.apply === true);
|
|
93
|
+
return result({
|
|
94
|
+
version: 1,
|
|
95
|
+
command: 'add',
|
|
96
|
+
ok: true,
|
|
97
|
+
applied: options.apply === true,
|
|
98
|
+
path: settings.config,
|
|
99
|
+
node,
|
|
100
|
+
nodes: config.nodes,
|
|
101
|
+
next: options.apply
|
|
102
|
+
? 'Run fleet-nodes --onboard, then fleet-status --json.'
|
|
103
|
+
: 'Review this preview, then repeat with --apply --yes.',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function diagnose(
|
|
108
|
+
nodes: FleetNode[],
|
|
109
|
+
options: CliOptions,
|
|
110
|
+
settings: Settings & { missingRoster: boolean },
|
|
111
|
+
services: FleetServices
|
|
112
|
+
): Promise<CommandOutcome> {
|
|
113
|
+
const source = nodes.length ? await services.source() : undefined;
|
|
114
|
+
const reports = await doctor(
|
|
115
|
+
nodes,
|
|
116
|
+
{
|
|
117
|
+
root: settings.workspace,
|
|
118
|
+
local: options.local === true,
|
|
119
|
+
required: capabilities(options.require),
|
|
120
|
+
timeoutMs: timeout(options),
|
|
121
|
+
...(source ? { source } : {}),
|
|
122
|
+
},
|
|
123
|
+
services.run
|
|
124
|
+
);
|
|
125
|
+
if (!options.local && settings.missingRoster) reports.push(missingRoster());
|
|
126
|
+
const report = {
|
|
127
|
+
...makeReport('doctor', reports),
|
|
128
|
+
coverage: {
|
|
129
|
+
scope: options.local
|
|
130
|
+
? 'local'
|
|
131
|
+
: options.node?.length
|
|
132
|
+
? 'selected'
|
|
133
|
+
: 'all-configured',
|
|
134
|
+
manager: true,
|
|
135
|
+
configuredNodesTested: nodes.map((node) => node.id),
|
|
136
|
+
rosterAvailable: !settings.missingRoster,
|
|
137
|
+
note: 'Directed SSH checks from this manager only. T3 relay/peer and VS Code forwarding require operator tests.',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
exitCode: reportExitCode(report, options.strict === true),
|
|
142
|
+
data: report,
|
|
143
|
+
text: renderReport(report),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function missingRoster(): NodeReport {
|
|
148
|
+
return {
|
|
149
|
+
node: 'fleet',
|
|
150
|
+
checks: [
|
|
151
|
+
{
|
|
152
|
+
id: 'fleet:roster',
|
|
153
|
+
status: 'fail',
|
|
154
|
+
summary:
|
|
155
|
+
'No developer fleet roster is configured. Only this manager could be checked; fleet-wide health is unknown.',
|
|
156
|
+
remedy: {
|
|
157
|
+
action:
|
|
158
|
+
'Open personal --interactive and choose Register a developer computer, or run devfleet fleet-node-add --help. Then run fleet-status --json.',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { verifyT3Linked } from '../t3-linked-check';
|
|
2
|
+
import { verifyT3Endpoint } from '../t3-endpoint';
|
|
3
|
+
import type { Check, NodeReport, Runner } from '../types';
|
|
4
|
+
import { fleet as definition } from '../fleet-config';
|
|
5
|
+
import { enrolledRoster } from '../definition';
|
|
6
|
+
import type { Tailnet } from '../tailscale';
|
|
7
|
+
import type { FleetNode } from '../types';
|
|
8
|
+
import {
|
|
9
|
+
PUBLIC_HUB_URL,
|
|
10
|
+
verifyBeszel,
|
|
11
|
+
type BeszelRequest,
|
|
12
|
+
} from '../monitoring/beszel';
|
|
13
|
+
import { hubAccess } from '../monitoring/hub-access';
|
|
14
|
+
import { hubUrl } from '../topology';
|
|
15
|
+
|
|
16
|
+
export interface DashboardAccess {
|
|
17
|
+
(hub: string): Promise<{ request: BeszelRequest }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The hub always runs on the manager, reached by its MagicDNS name. */
|
|
21
|
+
export function dashboardUrl(tailnet: Tailnet): string | null {
|
|
22
|
+
const canonical = definition;
|
|
23
|
+
const visible = tailnet.computers.some(
|
|
24
|
+
(computer) =>
|
|
25
|
+
computer.dns.replace(/\.$/, '') ===
|
|
26
|
+
canonical.manager.network.tailscaleName
|
|
27
|
+
);
|
|
28
|
+
return visible ? hubUrl(canonical) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Centralized dashboard evidence for every canonical worker: the hub must answer
|
|
32
|
+
* privately, and each node must be registered, connected and reporting fresh
|
|
33
|
+
* samples. A missing manager Vault session is reported, never silently passed.
|
|
34
|
+
*/
|
|
35
|
+
export async function appendDashboardEvidence(
|
|
36
|
+
reports: NodeReport[],
|
|
37
|
+
tailnet: Tailnet,
|
|
38
|
+
options: { access?: DashboardAccess; fetcher?: typeof fetch } = {}
|
|
39
|
+
): Promise<string | null> {
|
|
40
|
+
const hub = dashboardUrl(tailnet);
|
|
41
|
+
const dashboard: NodeReport = { node: 'dashboard', checks: [] };
|
|
42
|
+
reports.push(dashboard);
|
|
43
|
+
if (!hub) {
|
|
44
|
+
dashboard.checks.push(
|
|
45
|
+
fail(
|
|
46
|
+
'beszel:hub',
|
|
47
|
+
'BESZEL_HUB_SELECTION_REQUIRED',
|
|
48
|
+
'The hub runs on the manager, which is not visible in Tailscale. Wake the manager and reconnect it to the tailnet.'
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const fetcher = options.fetcher ?? fetch;
|
|
54
|
+
dashboard.checks.push(await hubReachable(hub, fetcher));
|
|
55
|
+
dashboard.checks.push(await publicEndpoint(fetcher));
|
|
56
|
+
await nodeEvidence(reports, dashboard, hub, options.access ?? hubAccess);
|
|
57
|
+
return hub;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function nodeEvidence(
|
|
61
|
+
reports: NodeReport[],
|
|
62
|
+
dashboard: NodeReport,
|
|
63
|
+
hub: string,
|
|
64
|
+
access: DashboardAccess
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const canonical = definition;
|
|
67
|
+
try {
|
|
68
|
+
const { request } = await access(hub);
|
|
69
|
+
for (const machine of canonical.workers) {
|
|
70
|
+
const evidence = await verifyBeszel(hub, machine.id, request);
|
|
71
|
+
const report =
|
|
72
|
+
reports.find(
|
|
73
|
+
(entry) =>
|
|
74
|
+
entry.node === machine.network?.tailscaleName ||
|
|
75
|
+
entry.node === machine.id
|
|
76
|
+
) ?? dashboard;
|
|
77
|
+
report.checks.push(...evidence.checks);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
dashboard.checks.push({
|
|
81
|
+
id: 'beszel:access',
|
|
82
|
+
status: 'skip',
|
|
83
|
+
summary:
|
|
84
|
+
error instanceof Error && error.message.startsWith('BESZEL_')
|
|
85
|
+
? error.message
|
|
86
|
+
: 'BESZEL_ACCOUNT_REQUIRED',
|
|
87
|
+
remedy: {
|
|
88
|
+
action:
|
|
89
|
+
'Authenticate this manager to Vault so BESZEL_FLEET_AUTH can be read, then rerun fleet-status --workers. Node registration and metric freshness are unverified until then.',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function hubReachable(
|
|
96
|
+
hub: string,
|
|
97
|
+
fetcher: typeof fetch
|
|
98
|
+
): Promise<Check> {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetcher(`${hub}/api/health`, {
|
|
101
|
+
redirect: 'error',
|
|
102
|
+
signal: AbortSignal.timeout(10_000),
|
|
103
|
+
});
|
|
104
|
+
if (response.ok)
|
|
105
|
+
return {
|
|
106
|
+
id: 'beszel:hub',
|
|
107
|
+
status: 'pass',
|
|
108
|
+
summary: `Dashboard answers privately at ${hub}.`,
|
|
109
|
+
};
|
|
110
|
+
} catch {
|
|
111
|
+
/* Connection details are not evidence; fall through. */
|
|
112
|
+
}
|
|
113
|
+
return fail(
|
|
114
|
+
'beszel:hub',
|
|
115
|
+
'BESZEL_HUB_UNREACHABLE',
|
|
116
|
+
`The private dashboard did not answer at ${hub}/api/health. The hub runs on the manager: keep it awake with Docker Desktop running, then run devfleet fleet-sync there.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The Cloudflare hostname is convenience access; the private hub stays canonical,
|
|
121
|
+
* so an offline connector warns instead of failing the audit.
|
|
122
|
+
*/
|
|
123
|
+
async function publicEndpoint(fetcher: typeof fetch): Promise<Check> {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetcher(`${PUBLIC_HUB_URL}/api/health`, {
|
|
126
|
+
redirect: 'error',
|
|
127
|
+
signal: AbortSignal.timeout(10_000),
|
|
128
|
+
});
|
|
129
|
+
if (response.ok)
|
|
130
|
+
return {
|
|
131
|
+
id: 'beszel:public-endpoint',
|
|
132
|
+
status: 'pass',
|
|
133
|
+
summary: `Dashboard answers publicly at ${PUBLIC_HUB_URL}.`,
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
/* Connection details are not evidence; fall through. */
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
id: 'beszel:public-endpoint',
|
|
140
|
+
status: 'warn',
|
|
141
|
+
summary: 'BESZEL_PUBLIC_ENDPOINT_OFFLINE',
|
|
142
|
+
remedy: {
|
|
143
|
+
action: `${PUBLIC_HUB_URL} did not answer. Create the Cloudflare tunnel and store its token in the fleet's secrets, then run fleet-sync on the manager so the hub starts its Cloudflare connector. The private dashboard is unaffected.`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fail(id: string, summary: string, action: string): Check {
|
|
149
|
+
return { id, status: 'fail', summary, remedy: { action } };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Explicit developer scope; the ordinary health command still audits all peers. */
|
|
153
|
+
export function managedHealthScope(
|
|
154
|
+
tailnet: Tailnet,
|
|
155
|
+
roster: FleetNode[],
|
|
156
|
+
includeManager = true
|
|
157
|
+
) {
|
|
158
|
+
const canonical = definition;
|
|
159
|
+
const declared = roster.filter((node) =>
|
|
160
|
+
canonical.workers.some((entry) => entry.id === node.id)
|
|
161
|
+
);
|
|
162
|
+
const nodes = declared.length
|
|
163
|
+
? enrolledRoster(canonical, { version: 1, nodes: declared }).nodes
|
|
164
|
+
: [];
|
|
165
|
+
const computers = tailnet.computers.filter(
|
|
166
|
+
(peer) =>
|
|
167
|
+
(includeManager && peer.self) ||
|
|
168
|
+
canonical.workers.some(
|
|
169
|
+
(node) =>
|
|
170
|
+
peer.dns === node.network?.tailscaleName ||
|
|
171
|
+
peer.dns.split('.')[0] === node.ssh ||
|
|
172
|
+
nodes.some(
|
|
173
|
+
(entry) => entry.id === node.id && entry.tailscaleId === peer.id
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
const missing = canonical.workers.filter(
|
|
178
|
+
(node) => !nodes.some((entry) => entry.id === node.id)
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
nodes,
|
|
182
|
+
tailnet: {
|
|
183
|
+
...tailnet,
|
|
184
|
+
computers,
|
|
185
|
+
checks: [
|
|
186
|
+
...tailnet.checks,
|
|
187
|
+
...missing.map((node) => ({
|
|
188
|
+
id: `fleet:enrollment:${node.id}`,
|
|
189
|
+
status: 'fail' as const,
|
|
190
|
+
summary:
|
|
191
|
+
'Canonical developer node lacks a verified SSH/check-out registration.',
|
|
192
|
+
remedy: {
|
|
193
|
+
action: `Enroll ${node.id} with devfleet fleet-node-add, or use interactive fleet-sync enrollment.`,
|
|
194
|
+
},
|
|
195
|
+
})),
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
excluded: tailnet.computers
|
|
199
|
+
.filter((peer) => !computers.includes(peer))
|
|
200
|
+
.map((peer) => peer.dns),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function appendManagerLinks(
|
|
205
|
+
reports: NodeReport[],
|
|
206
|
+
registered: FleetNode[],
|
|
207
|
+
run: Runner
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
for (const node of registered) {
|
|
210
|
+
const machine = definition.workers.find((entry) => entry.id === node.id);
|
|
211
|
+
const report = reports.find(
|
|
212
|
+
(entry) =>
|
|
213
|
+
entry.node === machine?.network?.tailscaleName || entry.node === node.id
|
|
214
|
+
);
|
|
215
|
+
if (!report) continue;
|
|
216
|
+
report.checks.push(...(await verifyT3Endpoint(node, run)));
|
|
217
|
+
report.checks.push(await verifyT3Linked(node, run));
|
|
218
|
+
}
|
|
219
|
+
}
|