@crvouga/dev-fleet 0.0.0-stage → 0.2.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 +357 -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 +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -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/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -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 +78 -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 +312 -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/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -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/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -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 +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -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 +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -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 +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -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 +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -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 +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -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 +290 -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 +174 -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 +173 -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 +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -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 +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -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 +132 -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 +66 -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 +210 -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/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -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 +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -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,121 @@
|
|
|
1
|
+
import { record } from '../config';
|
|
2
|
+
import type { Check } from '../types';
|
|
3
|
+
import {
|
|
4
|
+
LOCAL_HUB,
|
|
5
|
+
api,
|
|
6
|
+
superuserToken,
|
|
7
|
+
type HubLogin,
|
|
8
|
+
type Session,
|
|
9
|
+
} from './hub-viewer';
|
|
10
|
+
|
|
11
|
+
/** A scheduled backup older than this means the schedule is not running. */
|
|
12
|
+
export const BACKUP_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
function backupCheck(
|
|
15
|
+
status: Check['status'],
|
|
16
|
+
summary: string,
|
|
17
|
+
action?: string
|
|
18
|
+
): Check {
|
|
19
|
+
return {
|
|
20
|
+
id: 'beszel:backup',
|
|
21
|
+
status,
|
|
22
|
+
summary,
|
|
23
|
+
...(action ? { remedy: { action } } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SETTINGS =
|
|
28
|
+
'Open the hub as its superuser (/_/ on the manager), Settings > Backups.';
|
|
29
|
+
|
|
30
|
+
/** Reads, never changes, the hub's own backup settings and its list of
|
|
31
|
+
* backups through the stock PocketBase API of the pinned release (superuser
|
|
32
|
+
* only: GET /api/settings and GET /api/backups). Backups are configured in
|
|
33
|
+
* the hub's UI; this says whether they are scheduled, kept off the manager
|
|
34
|
+
* and recent. Storage names, keys and endpoints are never reported.
|
|
35
|
+
*/
|
|
36
|
+
export async function verifyHubBackups(
|
|
37
|
+
login: HubLogin | null,
|
|
38
|
+
fetcher: typeof fetch = fetch,
|
|
39
|
+
base: string = LOCAL_HUB,
|
|
40
|
+
now: number = Date.now()
|
|
41
|
+
): Promise<Check> {
|
|
42
|
+
if (!login)
|
|
43
|
+
return backupCheck(
|
|
44
|
+
'skip',
|
|
45
|
+
'BESZEL_BACKUP_UNVERIFIED',
|
|
46
|
+
"The fleet admin login is unavailable, so the hub's backups were not read. Log the manager's Vault session in and rerun fleet-sync."
|
|
47
|
+
);
|
|
48
|
+
let settings: unknown;
|
|
49
|
+
let backups: unknown;
|
|
50
|
+
try {
|
|
51
|
+
const anonymous: Session = { fetcher, base, token: '' };
|
|
52
|
+
const session = {
|
|
53
|
+
...anonymous,
|
|
54
|
+
token: await superuserToken(anonymous, login),
|
|
55
|
+
};
|
|
56
|
+
settings = await api(session, 'GET', '/api/settings');
|
|
57
|
+
backups = await api(session, 'GET', '/api/backups');
|
|
58
|
+
} catch {
|
|
59
|
+
return backupCheck(
|
|
60
|
+
'warn',
|
|
61
|
+
'BESZEL_BACKUP_UNVERIFIED',
|
|
62
|
+
`The hub's backup settings could not be read with the fleet admin login. ${SETTINGS} Details are withheld.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const policy = backupPolicy(settings);
|
|
66
|
+
if (!policy || !Array.isArray(backups))
|
|
67
|
+
return backupCheck(
|
|
68
|
+
'warn',
|
|
69
|
+
'BESZEL_BACKUP_UNVERIFIED',
|
|
70
|
+
`The hub answered in a form this release does not expect. ${SETTINGS}`
|
|
71
|
+
);
|
|
72
|
+
if (!policy.cron)
|
|
73
|
+
return backupCheck(
|
|
74
|
+
'warn',
|
|
75
|
+
'BESZEL_BACKUP_UNSCHEDULED',
|
|
76
|
+
`The hub takes no scheduled backups, so its history, settings and machine identities are lost with the manager. ${SETTINGS} Set a schedule and how many to keep.`
|
|
77
|
+
);
|
|
78
|
+
if (!policy.offHost)
|
|
79
|
+
return backupCheck(
|
|
80
|
+
'warn',
|
|
81
|
+
'BESZEL_BACKUP_OFFHOST_REQUIRED',
|
|
82
|
+
`Scheduled backups stay on the manager's disk, which is not disaster recovery. ${SETTINGS} Store them in existing encrypted S3-compatible storage you already approved; do not create new storage or reuse unrelated credentials.`
|
|
83
|
+
);
|
|
84
|
+
const newest = newestBackup(backups);
|
|
85
|
+
if (newest === null || now - newest > BACKUP_MAX_AGE_MS)
|
|
86
|
+
return backupCheck(
|
|
87
|
+
'warn',
|
|
88
|
+
'BESZEL_BACKUP_STALE',
|
|
89
|
+
`No backup newer than ${BACKUP_MAX_AGE_MS / 3_600_000} hours is listed although a schedule is set. ${SETTINGS} Check the schedule and the storage's credentials, then create a backup by hand.`
|
|
90
|
+
);
|
|
91
|
+
return backupCheck(
|
|
92
|
+
'pass',
|
|
93
|
+
`Scheduled backups go to off-host storage, keeping ${policy.keep}; the newest is from ${new Date(newest).toISOString()}.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function backupPolicy(
|
|
98
|
+
settings: unknown
|
|
99
|
+
): { cron: string; keep: number; offHost: boolean } | null {
|
|
100
|
+
if (!record(settings) || !record(settings['backups'])) return null;
|
|
101
|
+
const backups = settings['backups'];
|
|
102
|
+
const s3 = record(backups['s3']) ? backups['s3'] : {};
|
|
103
|
+
return {
|
|
104
|
+
cron: typeof backups['cron'] === 'string' ? backups['cron'].trim() : '',
|
|
105
|
+
keep:
|
|
106
|
+
typeof backups['cronMaxKeep'] === 'number' ? backups['cronMaxKeep'] : 0,
|
|
107
|
+
offHost: s3['enabled'] === true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** PocketBase lists `modified` as "2006-01-02 15:04:05.000Z". */
|
|
112
|
+
function newestBackup(backups: unknown[]): number | null {
|
|
113
|
+
const times = backups
|
|
114
|
+
.map((entry) =>
|
|
115
|
+
record(entry) && typeof entry['modified'] === 'string'
|
|
116
|
+
? Date.parse(entry['modified'].replace(' ', 'T'))
|
|
117
|
+
: NaN
|
|
118
|
+
)
|
|
119
|
+
.filter(Number.isFinite);
|
|
120
|
+
return times.length ? Math.max(...times) : null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { record } from '../config';
|
|
3
|
+
import type { Check } from '../types';
|
|
4
|
+
import { publicViewerIdentity } from './hub-access';
|
|
5
|
+
|
|
6
|
+
/** Loopback address of the hub container on the manager. */
|
|
7
|
+
export const LOCAL_HUB = 'http://127.0.0.1:8090';
|
|
8
|
+
|
|
9
|
+
type Fetcher = typeof fetch;
|
|
10
|
+
export interface HubLogin {
|
|
11
|
+
identity: string;
|
|
12
|
+
password: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function viewerCheck(
|
|
16
|
+
status: Check['status'],
|
|
17
|
+
summary: string,
|
|
18
|
+
action?: string
|
|
19
|
+
): Check {
|
|
20
|
+
return {
|
|
21
|
+
id: 'beszel:public-viewer',
|
|
22
|
+
status,
|
|
23
|
+
summary,
|
|
24
|
+
...(action ? { remedy: { action } } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Session {
|
|
29
|
+
fetcher: Fetcher;
|
|
30
|
+
base: string;
|
|
31
|
+
token: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function api(
|
|
35
|
+
session: Session,
|
|
36
|
+
method: string,
|
|
37
|
+
path: string,
|
|
38
|
+
body?: object
|
|
39
|
+
): Promise<unknown> {
|
|
40
|
+
const response = await session.fetcher(`${session.base}${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: session.token,
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
47
|
+
redirect: 'error',
|
|
48
|
+
signal: AbortSignal.timeout(10_000),
|
|
49
|
+
});
|
|
50
|
+
// Bodies can echo emails or tokens: only the status is ever surfaced.
|
|
51
|
+
if (!response.ok) throw new Error(`HTTP_${response.status}`);
|
|
52
|
+
return response.json() as Promise<unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function superuserToken(
|
|
56
|
+
session: Session,
|
|
57
|
+
login: HubLogin
|
|
58
|
+
): Promise<string> {
|
|
59
|
+
const auth = await api(
|
|
60
|
+
session,
|
|
61
|
+
'POST',
|
|
62
|
+
'/api/collections/_superusers/auth-with-password',
|
|
63
|
+
login
|
|
64
|
+
);
|
|
65
|
+
if (!record(auth) || typeof auth['token'] !== 'string')
|
|
66
|
+
throw new Error('BESZEL_PROTOCOL');
|
|
67
|
+
return auth['token'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function findViewer(
|
|
71
|
+
session: Session
|
|
72
|
+
): Promise<{ id: string; role: unknown } | null> {
|
|
73
|
+
const filter = encodeURIComponent(`email="${publicViewerIdentity()}"`);
|
|
74
|
+
const list = await api(
|
|
75
|
+
session,
|
|
76
|
+
'GET',
|
|
77
|
+
`/api/collections/users/records?filter=${filter}&perPage=2`
|
|
78
|
+
);
|
|
79
|
+
const items =
|
|
80
|
+
record(list) && Array.isArray(list['items']) ? list['items'] : [];
|
|
81
|
+
const first: unknown = items[0];
|
|
82
|
+
if (!record(first) || typeof first['id'] !== 'string') return null;
|
|
83
|
+
return { id: first['id'], role: first['role'] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function createViewer(session: Session): Promise<void> {
|
|
87
|
+
const password = randomBytes(24).toString('base64url');
|
|
88
|
+
await api(session, 'POST', '/api/collections/users/records', {
|
|
89
|
+
email: publicViewerIdentity(),
|
|
90
|
+
username: 'public-viewer',
|
|
91
|
+
password,
|
|
92
|
+
passwordConfirm: password,
|
|
93
|
+
verified: true,
|
|
94
|
+
role: 'readonly',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Idempotently makes sure the read-only viewer exists, then re-reads it to
|
|
99
|
+
* prove its role. AUTO_LOGIN signs every visitor in as this account, so an
|
|
100
|
+
* existing account with any other role is demoted rather than trusted. The
|
|
101
|
+
* password is random, never stored or printed, and is not needed to sign in.
|
|
102
|
+
*/
|
|
103
|
+
export async function ensurePublicViewer(
|
|
104
|
+
login: HubLogin | null,
|
|
105
|
+
fetcher: Fetcher = fetch,
|
|
106
|
+
base: string = LOCAL_HUB
|
|
107
|
+
): Promise<Check> {
|
|
108
|
+
if (!login)
|
|
109
|
+
return viewerCheck(
|
|
110
|
+
'warn',
|
|
111
|
+
'BESZEL_VIEWER_UNPROVISIONED',
|
|
112
|
+
"The fleet admin login is unavailable. Log the manager's Vault session in with read access to BESZEL_FLEET_AUTH and rerun fleet-sync; until then the public dashboard shows the login page."
|
|
113
|
+
);
|
|
114
|
+
try {
|
|
115
|
+
const anonymous: Session = { fetcher, base, token: '' };
|
|
116
|
+
const session = {
|
|
117
|
+
...anonymous,
|
|
118
|
+
token: await superuserToken(anonymous, login),
|
|
119
|
+
};
|
|
120
|
+
const existing = await findViewer(session);
|
|
121
|
+
if (!existing) await createViewer(session);
|
|
122
|
+
else if (existing.role !== 'readonly')
|
|
123
|
+
await api(
|
|
124
|
+
session,
|
|
125
|
+
'PATCH',
|
|
126
|
+
`/api/collections/users/records/${existing.id}`,
|
|
127
|
+
{ role: 'readonly' }
|
|
128
|
+
);
|
|
129
|
+
const verified = await findViewer(session);
|
|
130
|
+
if (verified?.role !== 'readonly') throw new Error('BESZEL_VIEWER_ROLE');
|
|
131
|
+
return viewerCheck(
|
|
132
|
+
'pass',
|
|
133
|
+
`Visitors are signed in automatically as ${publicViewerIdentity()} (read-only).`
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
return viewerCheck(
|
|
137
|
+
'warn',
|
|
138
|
+
'BESZEL_VIEWER_UNVERIFIED',
|
|
139
|
+
`Could not create or verify the read-only ${publicViewerIdentity()} user with the fleet admin login. Create it in the hub (Settings > Users, role Read-only); never make it an admin. Details are withheld.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { record } from '../config';
|
|
2
|
+
import type { Check, Runner } from '../types';
|
|
3
|
+
import { publicHubUrl } from './beszel';
|
|
4
|
+
import { secretVia } from '../fleet-config';
|
|
5
|
+
|
|
6
|
+
/** Shared Beszel hub helpers. The hub runs only on the manager; see
|
|
7
|
+
* manager-hub.ts. Workers never host it.
|
|
8
|
+
*/
|
|
9
|
+
export function publicEndpointCheck(connected: boolean): Check {
|
|
10
|
+
return connected
|
|
11
|
+
? {
|
|
12
|
+
id: 'beszel:public-endpoint',
|
|
13
|
+
status: 'pass',
|
|
14
|
+
summary: `Cloudflare connector for ${publicHubUrl()} was started from the Vault credential.`,
|
|
15
|
+
}
|
|
16
|
+
: {
|
|
17
|
+
id: 'beszel:public-endpoint',
|
|
18
|
+
status: 'warn',
|
|
19
|
+
summary: 'BESZEL_TUNNEL_CREDENTIAL_UNAVAILABLE',
|
|
20
|
+
remedy: {
|
|
21
|
+
action: `Create the Cloudflare tunnel and store its token in the fleet's secrets, then log the manager's Vault session in with read access to secret/beszel/tunnel and rerun fleet-sync. The private dashboard is unaffected; ${publicHubUrl()} stays offline until then.`,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The dashboard connector token from this fleet's secrets provider. Null
|
|
27
|
+
* when it is unavailable. Never printed, logged or placed in argv.
|
|
28
|
+
*/
|
|
29
|
+
export function tunnelToken(run: Runner): Promise<string | null> {
|
|
30
|
+
return secretVia(run)('monitoring-tunnel-token');
|
|
31
|
+
}
|
|
32
|
+
export function failure(code: string, action: string): Check[] {
|
|
33
|
+
return [
|
|
34
|
+
{ id: 'beszel:hub', status: 'fail', summary: code, remedy: { action } },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function inspectRoute(
|
|
39
|
+
output: string,
|
|
40
|
+
hub: string,
|
|
41
|
+
target = { port: '8443', proxy: 'http://127.0.0.1:8090' }
|
|
42
|
+
): boolean | Check[] {
|
|
43
|
+
let configured = false;
|
|
44
|
+
try {
|
|
45
|
+
const value: unknown = JSON.parse(output);
|
|
46
|
+
if (!record(value)) throw new Error();
|
|
47
|
+
if (publicOrConflictingTcp(value, hub, target.port))
|
|
48
|
+
return failure(
|
|
49
|
+
'BESZEL_PRIVATE_ROUTE_CONFLICT',
|
|
50
|
+
'Existing Funnel/public or TCP routing conflicts with private Beszel access. Review the source-owned route; do not reset Serve.'
|
|
51
|
+
);
|
|
52
|
+
const web = value['Web'];
|
|
53
|
+
if (record(web)) {
|
|
54
|
+
const entry = web[new URL(hub).hostname + ':' + target.port];
|
|
55
|
+
if (entry !== undefined) {
|
|
56
|
+
if (!expectedHandler(entry, target.proxy))
|
|
57
|
+
return failure(
|
|
58
|
+
'BESZEL_PRIVATE_ROUTE_CONFLICT',
|
|
59
|
+
'Port 8443 is owned by a different Tailscale Serve handler. Review a source-controlled route migration; do not reset Serve.'
|
|
60
|
+
);
|
|
61
|
+
configured = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return failure(
|
|
66
|
+
'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
|
|
67
|
+
'Review Tailscale Serve status without exposing raw configuration.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return configured;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function publicOrConflictingTcp(
|
|
74
|
+
value: Record<string, unknown>,
|
|
75
|
+
hub: string,
|
|
76
|
+
port: string
|
|
77
|
+
): boolean {
|
|
78
|
+
const funnel = value['AllowFunnel'];
|
|
79
|
+
if (record(funnel) && funnel[new URL(hub).hostname + ':' + port] === true)
|
|
80
|
+
return true;
|
|
81
|
+
const tcp = value['TCP'];
|
|
82
|
+
if (!record(tcp) || tcp[port] === undefined) return false;
|
|
83
|
+
return !record(tcp[port]) || tcp[port]['HTTPS'] !== true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function expectedHandler(entry: unknown, proxy: string): boolean {
|
|
87
|
+
return (
|
|
88
|
+
record(entry) &&
|
|
89
|
+
record(entry['Handlers']) &&
|
|
90
|
+
record(entry['Handlers']['/']) &&
|
|
91
|
+
entry['Handlers']['/']['Proxy'] === proxy
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { userInfo } from 'node:os';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import macos from '../../ansible/macos/policy.json';
|
|
4
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
5
|
+
import { privateHub } from './beszel';
|
|
6
|
+
import type { AgentEnrollment } from './enroll';
|
|
7
|
+
import type { Check, Runner } from '../types';
|
|
8
|
+
|
|
9
|
+
/** Installs the manager's own Beszel agent locally (never over SSH) as a
|
|
10
|
+
* per-user launchd agent, so the manager is always a system on its own hub.
|
|
11
|
+
* No sudo: fleet-sync on the manager stays unattended.
|
|
12
|
+
*/
|
|
13
|
+
export async function installManagerAgent(
|
|
14
|
+
root: string,
|
|
15
|
+
{ node, enrollment }: { node: string; enrollment: AgentEnrollment },
|
|
16
|
+
run: Runner,
|
|
17
|
+
{ uid = userInfo().uid, directory }: { uid?: number; directory?: string } = {}
|
|
18
|
+
): Promise<Check[]> {
|
|
19
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
20
|
+
const result = await runAnsibleWithReceipt(
|
|
21
|
+
run,
|
|
22
|
+
'agent-darwin.yml',
|
|
23
|
+
{
|
|
24
|
+
cwd: monitoring,
|
|
25
|
+
timeoutMs: 300_000,
|
|
26
|
+
input: JSON.stringify({
|
|
27
|
+
beszel_node: node,
|
|
28
|
+
beszel_hub_url: privateHub(enrollment.hubUrl),
|
|
29
|
+
beszel_public_key: enrollment.publicKey,
|
|
30
|
+
beszel_agent_token: enrollment.token,
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'beszel-manager-agent',
|
|
35
|
+
sources: [monitoring, resolve(root, 'ansible/macos')],
|
|
36
|
+
verify: async () => agentRunning(run, uid),
|
|
37
|
+
...(directory ? { directory } : {}),
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
id: 'beszel:installation',
|
|
44
|
+
status: 'fail',
|
|
45
|
+
summary: 'BESZEL_INSTALLATION_INCOMPLETE',
|
|
46
|
+
remedy: {
|
|
47
|
+
action:
|
|
48
|
+
'Inspect signed artifact access, free disk and ~/Library/LaunchAgents on the manager. A leftover root daemon must be removed once (sudo launchctl bootout system/' +
|
|
49
|
+
macos.beszelDaemon +
|
|
50
|
+
'). Agent data under ~/Library/Application Support/personal-beszel-agent is preserved.',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const running = await agentRunning(run, uid);
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: 'beszel:service',
|
|
58
|
+
status: running ? 'pass' : 'fail',
|
|
59
|
+
summary: running
|
|
60
|
+
? 'The manager runs its own Beszel agent as an always-on launch agent; hub metrics remain a separate gate.'
|
|
61
|
+
: 'BESZEL_SERVICE_STOPPED',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function agentRunning(run: Runner, uid: number): Promise<boolean> {
|
|
67
|
+
const state = await run([
|
|
68
|
+
'/bin/launchctl',
|
|
69
|
+
'print',
|
|
70
|
+
`gui/${uid}/${macos.beszelDaemon}`,
|
|
71
|
+
]);
|
|
72
|
+
return state.ok && /state = running/.test(state.stdout);
|
|
73
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
4
|
+
import { runAnsible } from '../platform/ansible';
|
|
5
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
6
|
+
import { failure, inspectRoute, publicEndpointCheck, tunnelToken } from './hub';
|
|
7
|
+
import { publicViewerIdentity, fleetAccount } from './hub-access';
|
|
8
|
+
import { ensurePublicViewer, type HubLogin } from './hub-viewer';
|
|
9
|
+
import { verifyHubBackups } from './hub-backup';
|
|
10
|
+
import type { FleetDefinition } from '../definition';
|
|
11
|
+
import { hostId, hubUrl } from '../topology';
|
|
12
|
+
import { publicHubUrl, privateHub } from './beszel';
|
|
13
|
+
import type { Check, Runner } from '../types';
|
|
14
|
+
|
|
15
|
+
/** Bundled CLI of the Tailscale macOS app when no `tailscale` is on PATH. */
|
|
16
|
+
export const TAILSCALE_APP_CLI =
|
|
17
|
+
'/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
18
|
+
const LOCAL_HUB = 'http://127.0.0.1:8090';
|
|
19
|
+
/** The one place the route is declared; `hub-route-darwin.yml` receives it. */
|
|
20
|
+
export const HUB_ROUTE = { port: '8443', proxy: LOCAL_HUB };
|
|
21
|
+
|
|
22
|
+
/** Per-user hub paths on the manager. Data stays in the human's home (and its
|
|
23
|
+
* normal backups); it is never copied to a worker.
|
|
24
|
+
*/
|
|
25
|
+
export function managerHubPaths(home = homedir()): {
|
|
26
|
+
data: string;
|
|
27
|
+
tunnel: string;
|
|
28
|
+
} {
|
|
29
|
+
const support = join(home, 'Library', 'Application Support');
|
|
30
|
+
return {
|
|
31
|
+
data: join(support, 'personal-beszel'),
|
|
32
|
+
tunnel: join(support, 'personal-beszel-tunnel'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `tailscale` on PATH (Homebrew or the app's installed CLI), else the app
|
|
37
|
+
* bundle's executable; null when neither exists.
|
|
38
|
+
*/
|
|
39
|
+
export function tailscaleCommand(
|
|
40
|
+
path = process.env['PATH'] ?? '',
|
|
41
|
+
exists: (candidate: string) => boolean = existsSync
|
|
42
|
+
): string | null {
|
|
43
|
+
for (const directory of path.split(delimiter).filter(Boolean)) {
|
|
44
|
+
const candidate = join(directory, 'tailscale');
|
|
45
|
+
if (exists(candidate)) return candidate;
|
|
46
|
+
}
|
|
47
|
+
return exists(TAILSCALE_APP_CLI) ? TAILSCALE_APP_CLI : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ManagerHubOptions {
|
|
51
|
+
home?: string;
|
|
52
|
+
tailscale?: string | null;
|
|
53
|
+
/** Fleet login for a fresh hub; defaults to the manager's Vault session. */
|
|
54
|
+
account?: () => Promise<HubLogin | null>;
|
|
55
|
+
/** Receipt directory for skipping an unchanged hub apply. */
|
|
56
|
+
receipts?: string;
|
|
57
|
+
/** Ensures the read-only AUTO_LOGIN user; defaults to the loopback hub API. */
|
|
58
|
+
viewer?: (login: HubLogin | null) => Promise<Check>;
|
|
59
|
+
/** Reads the hub's backup settings; defaults to the loopback hub API. */
|
|
60
|
+
backups?: (login: HubLogin | null) => Promise<Check>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Converges the singleton Beszel hub locally on the manager: pinned Compose
|
|
64
|
+
* under Docker Desktop, the connector token from the manager's own Vault
|
|
65
|
+
* session (private variables file, never argv or output), a private Tailscale
|
|
66
|
+
* Serve route (its own playbook), then independent health checks locally and
|
|
67
|
+
* over the tailnet.
|
|
68
|
+
*/
|
|
69
|
+
export async function convergeManagerHub(
|
|
70
|
+
root: string,
|
|
71
|
+
definition: FleetDefinition,
|
|
72
|
+
run: Runner,
|
|
73
|
+
options: ManagerHubOptions = {}
|
|
74
|
+
): Promise<Check[]> {
|
|
75
|
+
const hub = privateHub(hubUrl(definition));
|
|
76
|
+
const refused = await preconditions(definition, run);
|
|
77
|
+
if (refused) return refused;
|
|
78
|
+
const paths = managerHubPaths(options.home);
|
|
79
|
+
const tunnel = await tunnelToken(run);
|
|
80
|
+
const account = await (options.account ?? fleetAccount)();
|
|
81
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
82
|
+
const result = await runAnsibleWithReceipt(
|
|
83
|
+
run,
|
|
84
|
+
'hub-darwin.yml',
|
|
85
|
+
{
|
|
86
|
+
cwd: monitoring,
|
|
87
|
+
input: JSON.stringify(hubVariables(hub, paths, tunnel, account)),
|
|
88
|
+
timeoutMs: 300_000,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'beszel-hub',
|
|
92
|
+
sources: [monitoring],
|
|
93
|
+
// Health is verified independently below on every run.
|
|
94
|
+
verify: async () =>
|
|
95
|
+
(
|
|
96
|
+
await run([
|
|
97
|
+
'curl',
|
|
98
|
+
'-fsS',
|
|
99
|
+
'--max-time',
|
|
100
|
+
'5',
|
|
101
|
+
'-o',
|
|
102
|
+
'/dev/null',
|
|
103
|
+
`${LOCAL_HUB}/api/health`,
|
|
104
|
+
])
|
|
105
|
+
).ok,
|
|
106
|
+
...(options.receipts ? { directory: options.receipts } : {}),
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
if (!result.ok)
|
|
110
|
+
return failure(
|
|
111
|
+
'BESZEL_HUB_INSTALLATION_INCOMPLETE',
|
|
112
|
+
'Inspect Docker Desktop, the Compose image, file sharing for ~/Library/Application Support and disk space. Existing data is never deleted.'
|
|
113
|
+
);
|
|
114
|
+
const route = await servePrivateRoute(
|
|
115
|
+
{ hub, monitoring },
|
|
116
|
+
run,
|
|
117
|
+
options.tailscale === undefined ? tailscaleCommand() : options.tailscale
|
|
118
|
+
);
|
|
119
|
+
if (route) return route;
|
|
120
|
+
const health = await verifyHealth(hub, run);
|
|
121
|
+
if (health) return health;
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
id: 'beszel:hub',
|
|
125
|
+
status: 'pass',
|
|
126
|
+
summary: `Pinned hub on the manager is healthy locally and at ${hub}; authentication and agent metrics are separate checks.`,
|
|
127
|
+
},
|
|
128
|
+
publicEndpointCheck(tunnel !== null),
|
|
129
|
+
await ensureViewer(options, account),
|
|
130
|
+
await (options.backups ?? verifyHubBackups)(account),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hubVariables(
|
|
135
|
+
hub: string,
|
|
136
|
+
paths: { data: string; tunnel: string },
|
|
137
|
+
tunnel: string | null,
|
|
138
|
+
account: HubLogin | null
|
|
139
|
+
): Record<string, string> {
|
|
140
|
+
return {
|
|
141
|
+
beszel_hub_url: hub,
|
|
142
|
+
beszel_public_url: publicHubUrl(),
|
|
143
|
+
beszel_auto_login: publicViewerIdentity(),
|
|
144
|
+
beszel_data_dir: paths.data,
|
|
145
|
+
beszel_tunnel_dir: paths.tunnel,
|
|
146
|
+
...(tunnel ? { beszel_tunnel_token: tunnel } : {}),
|
|
147
|
+
...(account
|
|
148
|
+
? {
|
|
149
|
+
beszel_fleet_identity: account.identity,
|
|
150
|
+
beszel_fleet_password: account.password,
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ensureViewer(
|
|
157
|
+
options: ManagerHubOptions,
|
|
158
|
+
account: HubLogin | null
|
|
159
|
+
): Promise<Check> {
|
|
160
|
+
return (options.viewer ?? ensurePublicViewer)(account);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Runs only on the manager, and only with Docker Desktop's engine up. */
|
|
164
|
+
async function preconditions(
|
|
165
|
+
definition: FleetDefinition,
|
|
166
|
+
run: Runner
|
|
167
|
+
): Promise<Check[] | null> {
|
|
168
|
+
const local = await run(['/usr/sbin/scutil', '--get', 'LocalHostName']);
|
|
169
|
+
if (!local.ok || hostId(local.stdout.trim()) !== definition.manager.id)
|
|
170
|
+
return failure(
|
|
171
|
+
'BESZEL_NOT_MANAGER',
|
|
172
|
+
`The Beszel hub runs only on the manager (${definition.manager.id}). Run fleet-sync from the manager; workers only run the agent.`
|
|
173
|
+
);
|
|
174
|
+
const docker = await run(
|
|
175
|
+
['docker', 'info', '--format', '{{.ServerVersion}}'],
|
|
176
|
+
{ timeoutMs: 20_000 }
|
|
177
|
+
);
|
|
178
|
+
if (!docker.ok || docker.stdout.trim() === '')
|
|
179
|
+
return failure(
|
|
180
|
+
'BESZEL_DOCKER_DESKTOP_REQUIRED',
|
|
181
|
+
'Start Docker Desktop on the manager (and enable "Start Docker Desktop when you sign in"), then rerun fleet-sync. Existing hub data is never deleted.'
|
|
182
|
+
);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `hub-route-darwin.yml` publishes the route when its port has no owner; this
|
|
187
|
+
* refuses a conflicting owner beforehand and measures the result.
|
|
188
|
+
*/
|
|
189
|
+
async function servePrivateRoute(
|
|
190
|
+
{ hub, monitoring }: { hub: string; monitoring: string },
|
|
191
|
+
run: Runner,
|
|
192
|
+
cli: string | null
|
|
193
|
+
): Promise<Check[] | null> {
|
|
194
|
+
if (!cli)
|
|
195
|
+
return failure(
|
|
196
|
+
'BESZEL_TAILSCALE_REQUIRED',
|
|
197
|
+
`Install and sign in to Tailscale on the manager (Homebrew CLI or ${TAILSCALE_APP_CLI}).`
|
|
198
|
+
);
|
|
199
|
+
const status = await run([cli, 'serve', 'status', '--json']);
|
|
200
|
+
if (!status.ok)
|
|
201
|
+
return failure(
|
|
202
|
+
'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
|
|
203
|
+
'Inspect Tailscale Serve permission and existing private routes before retry.'
|
|
204
|
+
);
|
|
205
|
+
const route = inspectRoute(status.stdout, hub, HUB_ROUTE);
|
|
206
|
+
if (Array.isArray(route)) return route;
|
|
207
|
+
if (!route) {
|
|
208
|
+
const published = await runAnsible(run, 'hub-route-darwin.yml', {
|
|
209
|
+
cwd: monitoring,
|
|
210
|
+
input: JSON.stringify({
|
|
211
|
+
beszel_tailscale_cli: cli,
|
|
212
|
+
beszel_route_port: HUB_ROUTE.port,
|
|
213
|
+
beszel_route_target: HUB_ROUTE.proxy,
|
|
214
|
+
}),
|
|
215
|
+
timeoutMs: 120_000,
|
|
216
|
+
});
|
|
217
|
+
if (!published.ok)
|
|
218
|
+
return failure(
|
|
219
|
+
'BESZEL_PRIVATE_ROUTE_REQUIRED',
|
|
220
|
+
'Allow Tailscale Serve (HTTPS certificates) for this tailnet and approve it in the Tailscale app if asked; no tailnet policy or public ingress is changed.'
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const verified = await run([cli, 'serve', 'status', '--json']);
|
|
224
|
+
if (!verified.ok || inspectRoute(verified.stdout, hub, HUB_ROUTE) !== true)
|
|
225
|
+
return failure(
|
|
226
|
+
'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
|
|
227
|
+
'The requested route was not independently observed after application. Inspect Serve permissions and configuration; installer success is insufficient.'
|
|
228
|
+
);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function verifyHealth(hub: string, run: Runner): Promise<Check[] | null> {
|
|
233
|
+
const probe = (url: string) =>
|
|
234
|
+
run(['curl', '-fsS', '--max-time', '10', '-o', '/dev/null', url], {
|
|
235
|
+
timeoutMs: 15_000,
|
|
236
|
+
});
|
|
237
|
+
if (!(await probe(`${LOCAL_HUB}/api/health`)).ok)
|
|
238
|
+
return failure(
|
|
239
|
+
'BESZEL_HUB_UNHEALTHY',
|
|
240
|
+
'The hub container did not answer /api/health locally. Inspect Docker Desktop and the personal-beszel Compose project; preserve its data.'
|
|
241
|
+
);
|
|
242
|
+
if (!(await probe(`${hub}/api/health`)).ok)
|
|
243
|
+
return failure(
|
|
244
|
+
'BESZEL_HUB_UNREACHABLE',
|
|
245
|
+
`The hub is healthy locally but not at ${hub}. Inspect Tailscale on the manager and its Serve route.`
|
|
246
|
+
);
|
|
247
|
+
return null;
|
|
248
|
+
}
|