@crvouga/dev-fleet 0.1.0 → 0.3.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 +265 -37
- package/agents/setup.md +42 -12
- package/ansible/macos/manager.yml +10 -7
- package/ansible/macos/policy.json +2 -10
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/monitoring/README.md +86 -25
- package/ansible/monitoring/agent.yml +40 -2
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +5 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/baseline.yml +1 -1
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/policy.json +13 -3
- package/ansible/ubuntu/t3-desktop.yml +19 -0
- package/ansible/ubuntu/t3-health.sh +93 -22
- package/ansible/ubuntu/t3-idle.py +42 -0
- package/ansible/ubuntu/t3-policy.yml +4 -11
- package/ansible/ubuntu/tailscale.yml +8 -5
- package/ansible/ubuntu/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/resources.yml +61 -1
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/ansible/uv.lock +190 -0
- package/bootstrap/bootstrap.sh +25 -18
- package/bootstrap/doctor.ts +46 -1
- package/bootstrap/fleet-manager-run.sh +42 -15
- package/bootstrap/fleet-remote-bootstrap.sh +85 -12
- package/bootstrap/global-config.ts +64 -6
- package/bootstrap/install-launcher.sh +3 -0
- package/bootstrap/native-t3.ts +22 -11
- package/bootstrap/npm.ts +10 -2
- package/bootstrap/release-version.ts +157 -0
- package/cliproxy/README.md +3 -3
- package/cliproxy/scripts/accounts.py +216 -0
- package/cliproxy/scripts/checkpoint.py +29 -1
- package/cliproxy/scripts/cli.py +1 -1
- package/cliproxy/scripts/clientkeys.py +99 -0
- package/cliproxy/scripts/lease.py +80 -0
- package/cliproxy/scripts/ops +137 -25
- package/cliproxy/scripts/restore-verify +15 -0
- package/docs/accounts.md +154 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +91 -0
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +400 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +69 -0
- package/docs/ubuntu-sync.md +357 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +5 -0
- package/llms.txt +36 -0
- package/mise.lock +29 -0
- package/mise.toml +15 -2
- package/package.json +11 -2
- package/schema/fleet.schema.json +137 -15
- package/src/accounts/checks.ts +220 -0
- package/src/accounts/context.ts +76 -0
- package/src/accounts/propagate.ts +62 -0
- package/src/accounts/roster.ts +85 -0
- package/src/accounts/selection.ts +67 -0
- package/src/accounts/sessions.ts +120 -0
- package/src/agents/account-relay.ts +176 -26
- package/src/agents/fleet-verify.ts +9 -0
- package/src/agents/launch.ts +84 -5
- package/src/agents/native-verify.ts +293 -22
- package/src/agents/provider-errors.ts +9 -2
- package/src/agents/registry.ts +7 -1
- package/src/cli/agent-guide.ts +39 -5
- package/src/cli/app.ts +11 -4
- package/src/cli/cli.ts +7 -0
- package/src/cli/command-index.ts +7 -0
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-checkout.ts +56 -17
- package/src/cli/fleet-sync-enrollment.ts +19 -23
- package/src/cli/fleet-sync-forward.ts +1 -1
- package/src/cli/fleet-sync-manager.ts +84 -12
- package/src/cli/fleet-sync-monitoring.ts +5 -5
- package/src/cli/fleet-sync-output.ts +7 -3
- package/src/cli/fleet-sync-remote.ts +31 -18
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync.ts +159 -57
- package/src/cli/fleet.ts +4 -2
- package/src/cli/managed-health.ts +4 -4
- package/src/cli/node-sync.ts +7 -0
- package/src/cli/program.ts +20 -4
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +86 -1
- package/src/cli/registry.ts +41 -1
- package/src/cli/roster.ts +46 -30
- package/src/cli/settings.ts +15 -12
- package/src/cli/source-operations.ts +27 -3
- package/src/cli/tailnet.ts +2 -2
- package/src/definition.ts +116 -4
- package/src/diagnostics.ts +1 -1
- package/src/fleet-config.ts +105 -19
- package/src/ide/definition.ts +86 -0
- package/src/ide/ide.ts +43 -0
- package/src/ide/index.ts +5 -0
- package/src/ide/orchestrate.ts +50 -0
- package/src/ide/t3.ts +111 -0
- package/src/machine/source.ts +3 -3
- package/src/machine/state.ts +15 -27
- package/src/manager-sync.ts +9 -18
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +15 -3
- package/src/monitoring/beszel.ts +11 -4
- package/src/monitoring/enroll.ts +2 -1
- package/src/monitoring/hub-access.ts +4 -3
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +8 -8
- package/src/monitoring/hub.ts +3 -3
- package/src/monitoring/manager-hub.ts +30 -14
- package/src/node-converge-linux.ts +30 -7
- package/src/package-info.ts +45 -0
- package/src/platform/ansible.ts +19 -15
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- package/src/platform/resource-verify.ts +8 -4
- package/src/platform/t3-policy.ts +33 -6
- package/src/platform/t3-service.ts +104 -0
- package/src/platform/types.ts +0 -4
- package/src/platform/ubuntu-audit.ts +5 -2
- package/src/platform/windows.ts +0 -14
- package/src/remote-bootstrap.ts +17 -19
- package/src/remote-entry.ts +15 -1
- package/src/repair-catalog.ts +1 -1
- package/src/runner.ts +1 -1
- package/src/service-auth.ts +15 -5
- 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/t3/accounts.ts +7 -1
- package/src/t3/models.ts +133 -0
- package/src/t3/pairing.ts +31 -0
- package/src/t3/profile.ts +203 -0
- package/src/t3/version.ts +119 -0
- package/src/t3-endpoint.ts +64 -8
- package/src/t3-linked-check.ts +24 -2
- package/src/toolchain-checks.ts +25 -0
- package/src/transport.ts +3 -2
- package/src/types.ts +4 -0
- package/src/worker-access-checks.ts +56 -3
- package/src/worker-host-checks.ts +41 -13
- package/src/workstation-checks.ts +1 -1
- package/src/index.ts +0 -6
- package/src/test-fixtures.ts +0 -36
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig } from '../config';
|
|
1
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { loadRoster } from './roster';
|
|
4
3
|
import { enrolledRoster } from '../definition';
|
|
5
4
|
import { FleetUsageError } from '../errors';
|
|
6
5
|
import { dirname } from 'node:path';
|
|
@@ -10,6 +9,7 @@ import { matchComputers } from '../tailnet-matching';
|
|
|
10
9
|
import { sshArgs, sshFailure } from '../ssh';
|
|
11
10
|
import { ensurePrivateRoot } from '../machine/state';
|
|
12
11
|
import type { FleetNode, Runner } from '../types';
|
|
12
|
+
import { STATE_ROSTER, readRosterText, writeRosterText } from '../state/roster';
|
|
13
13
|
|
|
14
14
|
/** Interactive enrollment/resume only. Existing paths and explicit identities
|
|
15
15
|
* are preserved. Tailnet candidates never enter the declared membership.
|
|
@@ -20,7 +20,7 @@ export async function enrollManager(path: string, run: Runner): Promise<void> {
|
|
|
20
20
|
const saved =
|
|
21
21
|
original === null
|
|
22
22
|
? []
|
|
23
|
-
: enrolledRoster(canonical, await
|
|
23
|
+
: enrolledRoster(canonical, await loadRoster(path)).nodes;
|
|
24
24
|
const nodes: FleetNode[] = canonical.workers.map((node) => ({
|
|
25
25
|
id: node.id,
|
|
26
26
|
ssh: node.ssh,
|
|
@@ -55,8 +55,6 @@ export async function enrollManager(path: string, run: Runner): Promise<void> {
|
|
|
55
55
|
process.stderr.write(
|
|
56
56
|
`Enrolling verified SSH connections for canonical targets:\n${enrolled.map((node) => ` ${node.id} → ${node.ssh}; device ${enrollmentIdentity(node)}`).join('\n')}\nMissing targets remain visible and are never replaced by discovered peers.\n`
|
|
57
57
|
);
|
|
58
|
-
await ensurePrivateRoot(dirname(path));
|
|
59
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
60
58
|
await persistEnrollment(path, original, merged);
|
|
61
59
|
}
|
|
62
60
|
|
|
@@ -95,13 +93,8 @@ function ambiguous(ids: string[], matches: Map<string, FleetNode[]>): boolean {
|
|
|
95
93
|
ids.length > 1 || (ids.length === 1 && matches.get(ids[0]!)?.length !== 1)
|
|
96
94
|
);
|
|
97
95
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return await readFile(path, 'utf8');
|
|
101
|
-
} catch (error) {
|
|
102
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
96
|
+
function existingEnrollment(path: string): Promise<string | null> {
|
|
97
|
+
return readRosterText(path);
|
|
105
98
|
}
|
|
106
99
|
export function mergeEnrollment(
|
|
107
100
|
saved: FleetNode[],
|
|
@@ -123,22 +116,25 @@ async function persistEnrollment(
|
|
|
123
116
|
original: string | null,
|
|
124
117
|
nodes: FleetNode[]
|
|
125
118
|
): Promise<void> {
|
|
126
|
-
const
|
|
127
|
-
await mkdir(lock, { mode: 0o700 });
|
|
128
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
129
|
-
try {
|
|
119
|
+
const write = async (): Promise<void> => {
|
|
130
120
|
if ((await existingEnrollment(path)) !== original)
|
|
131
121
|
throw new FleetUsageError(
|
|
132
122
|
'Enrollment changed during approval. Inspect it and rerun; no connection was overwritten.'
|
|
133
123
|
);
|
|
134
|
-
await
|
|
135
|
-
|
|
136
|
-
JSON.stringify({ version: 1, nodes }, null, 2) + '\n'
|
|
137
|
-
{ flag: 'wx', mode: 0o600 }
|
|
124
|
+
await writeRosterText(
|
|
125
|
+
path,
|
|
126
|
+
JSON.stringify({ version: 1, nodes }, null, 2) + '\n'
|
|
138
127
|
);
|
|
139
|
-
|
|
128
|
+
};
|
|
129
|
+
// The fleet state serializes its own writers; a roster file needs a lock.
|
|
130
|
+
if (path === STATE_ROSTER) return write();
|
|
131
|
+
await ensurePrivateRoot(dirname(path));
|
|
132
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
133
|
+
const lock = `${path}.enroll.lock`;
|
|
134
|
+
await mkdir(lock, { mode: 0o700 });
|
|
135
|
+
try {
|
|
136
|
+
await write();
|
|
140
137
|
} finally {
|
|
141
|
-
await rm(temporary, { force: true });
|
|
142
138
|
await rm(lock, { recursive: true });
|
|
143
139
|
}
|
|
144
140
|
}
|
|
@@ -91,7 +91,7 @@ export function managerCommand(
|
|
|
91
91
|
revision,
|
|
92
92
|
...forwardedFlags(options, undefined),
|
|
93
93
|
]
|
|
94
|
-
: ['
|
|
94
|
+
: ['devfleet', 'fleet-sync', ...forwardedFlags(options, revision)];
|
|
95
95
|
const color = process.stdout.isTTY && !process.env['NO_COLOR'];
|
|
96
96
|
// A login shell gives the manager's normal PATH (mise, Docker, Tailscale).
|
|
97
97
|
const script = `export ${NO_FORWARD_ENV}=1${color ? ' FORCE_COLOR=1' : ''}; exec ${args.map(shellQuote).join(' ')}`;
|
|
@@ -6,11 +6,18 @@ import { convergeProxy } from '../proxy-converge';
|
|
|
6
6
|
import { attempt } from '../node-converge-shared';
|
|
7
7
|
import { quotaLimited, verifyFleetProviders } from '../agents/fleet-verify';
|
|
8
8
|
import { verifyNativeAgents } from '../agents/native-verify';
|
|
9
|
+
import { accountContext, nodeAccountChecks } from '../accounts/context';
|
|
9
10
|
import { convergeManagerHub } from '../monitoring/manager-hub';
|
|
10
11
|
import { enrollManagerMonitoring } from './fleet-sync-monitoring';
|
|
11
12
|
import { installConfig } from '../fleet-config';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { IDES } from '../ide';
|
|
14
|
+
import { enabled, managerChecks } from '../ide/orchestrate';
|
|
15
|
+
import { idesOf } from '../ide/definition';
|
|
16
|
+
import macos from '../../ansible/macos/policy.json';
|
|
17
|
+
import { ansibleCommand } from '../platform/ansible';
|
|
18
|
+
import { PACKAGE_ROOT } from '../package-info';
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
14
21
|
|
|
15
22
|
/** The manager converges itself locally (never over SSH) before any worker:
|
|
16
23
|
* it must be the declared manager, stay awake on AC power, and host every
|
|
@@ -32,9 +39,17 @@ export async function convergeManager(
|
|
|
32
39
|
if (identity.status === 'pass') {
|
|
33
40
|
// Launchers and scheduled jobs on the manager read the installed copy.
|
|
34
41
|
if (input.apply) installConfig();
|
|
35
|
-
checks.push(await managerPower(run));
|
|
36
|
-
checks.push(await
|
|
37
|
-
|
|
42
|
+
checks.push(await managerPower(run, input.workspace));
|
|
43
|
+
checks.push(await managerDocker(run));
|
|
44
|
+
// Each enabled IDE: its settings, pinned version and policy.
|
|
45
|
+
checks.push(
|
|
46
|
+
...(await managerChecks(enabled(idesOf(definition), IDES), {
|
|
47
|
+
id,
|
|
48
|
+
workspace: input.workspace,
|
|
49
|
+
apply: input.apply,
|
|
50
|
+
run,
|
|
51
|
+
}))
|
|
52
|
+
);
|
|
38
53
|
if (input.apply) {
|
|
39
54
|
// Same provider profiles and the same Vault gateway credential as every
|
|
40
55
|
// worker: the instances above launch from these profiles, and the check
|
|
@@ -49,10 +64,17 @@ export async function convergeManager(
|
|
|
49
64
|
);
|
|
50
65
|
checks.push(...providers);
|
|
51
66
|
// Same agents, versions and shared accounts as every worker.
|
|
67
|
+
const accounts = await accountContext();
|
|
68
|
+
checks.push(
|
|
69
|
+
...(await attempt('accounts:verification', () =>
|
|
70
|
+
nodeAccountChecks(accounts)
|
|
71
|
+
))
|
|
72
|
+
);
|
|
52
73
|
checks.push(
|
|
53
74
|
...(await attempt('agent:verification', () =>
|
|
54
75
|
verifyNativeAgents(id, input.workspace, run, {
|
|
55
76
|
outOfQuota: quotaLimited(providers),
|
|
77
|
+
...(accounts ? { accounts } : {}),
|
|
56
78
|
})
|
|
57
79
|
))
|
|
58
80
|
);
|
|
@@ -170,12 +192,22 @@ export async function managerIdentity(id: string, run: Runner): Promise<Check> {
|
|
|
170
192
|
};
|
|
171
193
|
}
|
|
172
194
|
|
|
173
|
-
/** Workers depend on the manager's services, so it never sleeps on AC power.
|
|
174
|
-
|
|
195
|
+
/** Workers depend on the manager's services, so it never sleeps on AC power.
|
|
196
|
+
* Measures every setting `ansible/macos/manager.yml` declares, from the same
|
|
197
|
+
* policy file the playbook applies.
|
|
198
|
+
*/
|
|
199
|
+
export async function managerPower(
|
|
200
|
+
run: Runner,
|
|
201
|
+
workspace = '.'
|
|
202
|
+
): Promise<Check> {
|
|
175
203
|
const result = await run(['pmset', '-g', 'custom']);
|
|
176
204
|
const ac = result.ok ? (result.stdout.split('AC Power:')[1] ?? '') : '';
|
|
177
|
-
const
|
|
178
|
-
|
|
205
|
+
const observed = (key: string) =>
|
|
206
|
+
new RegExp(`^\\s*${key}\\s+(\\S+)`, 'm').exec(ac)?.[1];
|
|
207
|
+
const drifted = Object.entries(macos.pmset)
|
|
208
|
+
.filter(([key, value]) => observed(key) !== value)
|
|
209
|
+
.map(([key]) => key);
|
|
210
|
+
return drifted.length === 0
|
|
179
211
|
? {
|
|
180
212
|
id: 'manager:power',
|
|
181
213
|
status: 'pass',
|
|
@@ -184,10 +216,50 @@ export async function managerPower(run: Runner): Promise<Check> {
|
|
|
184
216
|
: {
|
|
185
217
|
id: 'manager:power',
|
|
186
218
|
status: 'fail',
|
|
187
|
-
summary: '
|
|
219
|
+
summary: drifted.includes('sleep')
|
|
220
|
+
? 'MANAGER_SLEEPS_ON_POWER'
|
|
221
|
+
: `MANAGER_POWER_POLICY_DRIFT ${drifted.join(' ')}`,
|
|
188
222
|
remedy: {
|
|
189
|
-
action:
|
|
190
|
-
'Workers lose the CLI proxy and Beszel hub while the manager sleeps. Apply the manager power policy once: ansible-playbook -K ansible/macos/manager.yml',
|
|
223
|
+
action: `Workers lose the CLI proxy and Beszel hub while the manager sleeps. Apply the manager power policy once, as an administrator: cd ${workspace} && ${ansibleCommand().join(' ')} -K ansible/macos/manager.yml`,
|
|
191
224
|
},
|
|
192
225
|
};
|
|
193
226
|
}
|
|
227
|
+
|
|
228
|
+
/** The Docker Desktop release `mise.toml` pins, as the engine names itself. */
|
|
229
|
+
export function pinnedDockerDesktop(root = PACKAGE_ROOT): string {
|
|
230
|
+
const { vars } = Bun.TOML.parse(
|
|
231
|
+
readFileSync(join(root, 'mise.toml'), 'utf8')
|
|
232
|
+
) as { vars: Record<string, string> };
|
|
233
|
+
return `Docker Desktop ${vars['docker_desktop_version']} (${vars['docker_desktop_build']})`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The manager's services run in Docker Desktop, which no playbook installs
|
|
237
|
+
* and which updates itself: the pinned release is measured, and a different
|
|
238
|
+
* one warns instead of failing the run.
|
|
239
|
+
*/
|
|
240
|
+
export async function managerDocker(
|
|
241
|
+
run: Runner,
|
|
242
|
+
pinned = pinnedDockerDesktop()
|
|
243
|
+
): Promise<Check> {
|
|
244
|
+
const result = await run(
|
|
245
|
+
['docker', 'version', '--format', '{{.Server.Platform.Name}}'],
|
|
246
|
+
{ timeoutMs: 20_000 }
|
|
247
|
+
);
|
|
248
|
+
const observed = result.ok ? result.stdout.trim() : '';
|
|
249
|
+
if (observed === pinned)
|
|
250
|
+
return {
|
|
251
|
+
id: 'manager:docker',
|
|
252
|
+
status: 'pass',
|
|
253
|
+
summary: `The manager runs the pinned ${pinned}.`,
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
id: 'manager:docker',
|
|
257
|
+
status: 'warn',
|
|
258
|
+
summary: observed
|
|
259
|
+
? 'MANAGER_DOCKER_DESKTOP_DRIFT'
|
|
260
|
+
: 'MANAGER_DOCKER_DESKTOP_UNOBSERVED',
|
|
261
|
+
remedy: {
|
|
262
|
+
action: `The manager's services are tested on ${pinned}. Install that release and turn off its automatic updates, or expect differences; fleet-sync does not install or replace Docker Desktop.`,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { FleetNode, Runner, Check } from '../types';
|
|
|
2
2
|
import type { FleetDefinition } from '../definition';
|
|
3
3
|
import { hubUrl, roleOf } from '../topology';
|
|
4
4
|
import { discoverTailnet } from '../tailscale';
|
|
5
|
-
import {
|
|
5
|
+
import { fleetIdentity, hubAccess } from '../monitoring/hub-access';
|
|
6
6
|
import { enrollBeszel } from '../monitoring/enroll';
|
|
7
7
|
import { verifyBeszel } from '../monitoring/beszel';
|
|
8
8
|
import { installRemoteAgent } from '../monitoring/remote';
|
|
@@ -142,15 +142,15 @@ async function hubReachable(hub: string, run: Runner): Promise<boolean> {
|
|
|
142
142
|
return probe.ok;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
const
|
|
146
|
-
BESZEL_ACCOUNT_REQUIRED: `The hub rejected the fleet login ${
|
|
145
|
+
const remedies = (): Record<string, string> => ({
|
|
146
|
+
BESZEL_ACCOUNT_REQUIRED: `The hub rejected the fleet login ${fleetIdentity()}. A new hub on the manager creates it on first start from BESZEL_FLEET_AUTH; an older hub needs it once, by its superuser, as a verified users record with that Vault password. Never paste the password on a command line.`,
|
|
147
147
|
BESZEL_HUB_UNREACHABLE:
|
|
148
148
|
'The hub runs on the manager; keep it awake and Docker Desktop running, then rerun fleet-sync from the manager so it converges the hub first.',
|
|
149
149
|
BESZEL_NODE_TAILNET_REQUIRED:
|
|
150
150
|
'The worker is not visible on the tailnet. Sign it in to Tailscale and record its tailscaleId in the local roster, then rerun fleet-sync.',
|
|
151
151
|
BESZEL_NOT_ENROLLED:
|
|
152
152
|
'Only workers declared in fleet.json run the agent over SSH; fleet-sync installs the manager’s own agent locally after its hub.',
|
|
153
|
-
};
|
|
153
|
+
});
|
|
154
154
|
function unavailable(code: string): Check[] {
|
|
155
155
|
return [
|
|
156
156
|
{
|
|
@@ -159,7 +159,7 @@ function unavailable(code: string): Check[] {
|
|
|
159
159
|
summary: code,
|
|
160
160
|
remedy: {
|
|
161
161
|
action:
|
|
162
|
-
|
|
162
|
+
remedies()[code] ??
|
|
163
163
|
"Verify the manager's private hub is running and the manager has BESZEL_FLEET_AUTH through existing Vault authorization. The normal hub account must access these systems. Inspect duplicate registration/host conflicts before retry; no token or raw response is printed.",
|
|
164
164
|
},
|
|
165
165
|
},
|
|
@@ -19,9 +19,10 @@ const COLUMNS: { title: string; matches: (id: string) => boolean }[] = [
|
|
|
19
19
|
matches: (id) =>
|
|
20
20
|
[
|
|
21
21
|
't3:service',
|
|
22
|
-
't3:policy',
|
|
22
|
+
't3:policy-staged',
|
|
23
|
+
't3:policy-loaded',
|
|
24
|
+
't3:running-version',
|
|
23
25
|
't3:providers',
|
|
24
|
-
't3:antigravity-key',
|
|
25
26
|
't3:private-route',
|
|
26
27
|
'worker:t3-recovery',
|
|
27
28
|
'worker:t3-health',
|
|
@@ -30,7 +31,10 @@ const COLUMNS: { title: string; matches: (id: string) => boolean }[] = [
|
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
33
|
title: 'LINK',
|
|
33
|
-
matches: (id) =>
|
|
34
|
+
matches: (id) =>
|
|
35
|
+
['t3:manager-endpoint', 't3:manager-origin', 't3:paired-access'].includes(
|
|
36
|
+
id
|
|
37
|
+
),
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
title: 'PROXY',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { verifyT3Linked } from '../t3-linked-check';
|
|
2
|
-
import { T3_RESTARTED_SUMMARY } from '../platform/t3-policy';
|
|
3
1
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import {
|
|
2
|
+
import { IDES } from '../ide';
|
|
3
|
+
import { enabled, workerChecks } from '../ide/orchestrate';
|
|
4
|
+
import { idesOf } from '../ide/definition';
|
|
5
|
+
import { rememberEnvironment } from '../t3/pairing';
|
|
5
6
|
import { authorizePrivilege } from './fleet-sync-privilege';
|
|
6
7
|
import { authorizeVault } from './fleet-sync-vault';
|
|
7
8
|
import { managerFailure } from '../manager-failure';
|
|
@@ -11,7 +12,8 @@ import { saveReport } from './fleet-sync-report';
|
|
|
11
12
|
import { synchronizeMonitoring } from './fleet-sync-monitoring';
|
|
12
13
|
import { convergeManager } from './fleet-sync-manager';
|
|
13
14
|
import { FleetUsageError } from '../errors';
|
|
14
|
-
import {
|
|
15
|
+
import { selectNodes } from '../config';
|
|
16
|
+
import { loadRoster } from './roster';
|
|
15
17
|
import { enrolledRoster } from '../definition';
|
|
16
18
|
import type { FleetDefinition } from '../definition';
|
|
17
19
|
import { fleet as definition } from '../fleet-config';
|
|
@@ -19,23 +21,27 @@ import { synchronizeManager } from '../manager-sync';
|
|
|
19
21
|
import { inspectBootstrap } from '../manager-sync';
|
|
20
22
|
import { prepareBootstrap, bootstrapRemote } from '../remote-bootstrap';
|
|
21
23
|
import { machineRoot } from './fleet-sync';
|
|
22
|
-
import type { Runner } from '../types';
|
|
23
|
-
import type { DeployNode } from '../manager-sync';
|
|
24
|
+
import type { Check, Runner } from '../types';
|
|
25
|
+
import type { DeployNode, ManagerReport } from '../manager-sync';
|
|
24
26
|
import type { FleetSyncOptions } from './fleet-sync';
|
|
25
27
|
import type { Settings } from './settings';
|
|
26
28
|
import type { CommandOutcome } from './contracts';
|
|
27
29
|
|
|
30
|
+
/** `settle` runs once the fleet's result is known and before it is reported:
|
|
31
|
+
* it records the result and returns the check that says so.
|
|
32
|
+
*/
|
|
28
33
|
export async function selected(
|
|
29
34
|
options: FleetSyncOptions,
|
|
30
35
|
settings: Settings,
|
|
31
|
-
run: Runner
|
|
36
|
+
run: Runner,
|
|
37
|
+
settle?: (report: ManagerReport) => Promise<Check>
|
|
32
38
|
): Promise<CommandOutcome> {
|
|
33
39
|
if (!options.revision)
|
|
34
40
|
throw new FleetUsageError('An approved source revision is required.');
|
|
35
41
|
const canonical = definition;
|
|
36
42
|
let roster;
|
|
37
43
|
try {
|
|
38
|
-
roster = enrolledRoster(canonical, await
|
|
44
|
+
roster = enrolledRoster(canonical, await loadRoster(settings.config));
|
|
39
45
|
} catch {
|
|
40
46
|
return enrollmentRequired(canonical, options.revision);
|
|
41
47
|
}
|
|
@@ -87,8 +93,13 @@ export async function selected(
|
|
|
87
93
|
),
|
|
88
94
|
},
|
|
89
95
|
run,
|
|
90
|
-
deployWithMonitoring(payload, canonical, run,
|
|
96
|
+
deployWithMonitoring(payload, canonical, run, {
|
|
97
|
+
...ui,
|
|
98
|
+
workspace: settings.workspace,
|
|
99
|
+
roster: settings.config,
|
|
100
|
+
})
|
|
91
101
|
).finally(() => ui.progress.stop());
|
|
102
|
+
if (settle) report.checks.push(await settle(report));
|
|
92
103
|
return outcome(report, planFingerprint, started);
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -151,6 +162,9 @@ function deployWithMonitoring(
|
|
|
151
162
|
ui: {
|
|
152
163
|
progress: SyncProgress;
|
|
153
164
|
interactiveAuthorization: boolean;
|
|
165
|
+
/** The manager's checkout, and the roster an identity is recorded in. */
|
|
166
|
+
workspace: string;
|
|
167
|
+
roster: string;
|
|
154
168
|
}
|
|
155
169
|
): DeployNode {
|
|
156
170
|
return async (node, revision) => {
|
|
@@ -188,16 +202,15 @@ function deployWithMonitoring(
|
|
|
188
202
|
// Workers are Linux by definition.
|
|
189
203
|
ui.progress.step(node.id, 'Verifying SSH, OS and T3');
|
|
190
204
|
result.checks.push(...(await inspectBootstrap(node, run, 'linux')));
|
|
191
|
-
result.checks.push(...(await verifyT3Endpoint(node, run)));
|
|
192
|
-
// A worker T3 restarted by this sync (idle policy heal) needs a minute or
|
|
193
|
-
// two before the manager's link reconnects.
|
|
194
|
-
const restarted = result.checks.some(
|
|
195
|
-
(check) => check.summary === T3_RESTARTED_SUMMARY
|
|
196
|
-
);
|
|
197
205
|
result.checks.push(
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
...(await workerChecks(enabled(idesOf(canonical), IDES), node, {
|
|
207
|
+
run,
|
|
208
|
+
workspace: ui.workspace,
|
|
209
|
+
earlier: result.checks,
|
|
210
|
+
remember: async (id, identity) => {
|
|
211
|
+
await rememberEnvironment(ui.roster, id, identity);
|
|
212
|
+
},
|
|
213
|
+
}))
|
|
201
214
|
);
|
|
202
215
|
if (
|
|
203
216
|
result.checks.some(
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Enrollment } from '../machine/state';
|
|
2
|
+
import { replicaCheck } from '../state/check';
|
|
3
|
+
import { storeEnrollment } from '../state/enrollment';
|
|
4
|
+
import { adoptEarlier } from '../state/legacy';
|
|
5
|
+
import { databasePath, stateRoot } from '../state/paths';
|
|
6
|
+
import { writeDocument } from '../state/store';
|
|
7
|
+
import { pending, push, type Standing, type StateContext } from '../state/sync';
|
|
8
|
+
import type { Check } from '../types';
|
|
9
|
+
|
|
10
|
+
/** What one fleet-sync run records in the fleet state: the revision it was
|
|
11
|
+
* approved for and the configuration it runs with, then what it applied, then
|
|
12
|
+
* an upload so the bucket holds all of it.
|
|
13
|
+
*/
|
|
14
|
+
export interface RunRecord {
|
|
15
|
+
/** Before any machine changes: this revision is the approved one. */
|
|
16
|
+
approve(): void;
|
|
17
|
+
/** Once the result is known. The check says whether the bucket holds it. */
|
|
18
|
+
settle(converged: boolean): Promise<Check>;
|
|
19
|
+
/** For a run that stopped before its result: its approval is still uploaded. */
|
|
20
|
+
abandon(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runRecord(
|
|
24
|
+
previous: Enrollment | null,
|
|
25
|
+
revision: string,
|
|
26
|
+
state: {
|
|
27
|
+
context: StateContext;
|
|
28
|
+
standing: Standing;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
/** The configuration this run reads from a project's fleet.json. Stored
|
|
31
|
+
* so that a run started anywhere else (the schedule, a hand-off from a
|
|
32
|
+
* worker, a replacement manager) uses the one last applied.
|
|
33
|
+
*/
|
|
34
|
+
definition?: string;
|
|
35
|
+
}
|
|
36
|
+
): RunRecord {
|
|
37
|
+
const env = state.env ?? process.env;
|
|
38
|
+
const approval: Enrollment = {
|
|
39
|
+
...previous,
|
|
40
|
+
version: 1,
|
|
41
|
+
id: 'manager',
|
|
42
|
+
approvedRevision: revision,
|
|
43
|
+
};
|
|
44
|
+
let settled = false;
|
|
45
|
+
/** Uploads only what this run changed; otherwise the bucket stands as it
|
|
46
|
+
* did when the run started.
|
|
47
|
+
*/
|
|
48
|
+
const publish = (): Promise<Standing> =>
|
|
49
|
+
pending(state.context)
|
|
50
|
+
? push(state.context)
|
|
51
|
+
: Promise.resolve(state.standing);
|
|
52
|
+
return {
|
|
53
|
+
approve: () => {
|
|
54
|
+
storeEnrollment(approval, env);
|
|
55
|
+
if (state.definition === undefined) return;
|
|
56
|
+
adoptEarlier(env);
|
|
57
|
+
writeDocument(
|
|
58
|
+
databasePath(stateRoot(env)),
|
|
59
|
+
'definition',
|
|
60
|
+
state.definition
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
async settle(converged) {
|
|
64
|
+
settled = true;
|
|
65
|
+
if (converged)
|
|
66
|
+
storeEnrollment(
|
|
67
|
+
{
|
|
68
|
+
...approval,
|
|
69
|
+
appliedRevision: revision,
|
|
70
|
+
...(previous?.appliedRevision &&
|
|
71
|
+
previous.appliedRevision !== revision
|
|
72
|
+
? { previousRevision: previous.appliedRevision }
|
|
73
|
+
: {}),
|
|
74
|
+
},
|
|
75
|
+
env
|
|
76
|
+
);
|
|
77
|
+
// The fleet converged either way: a bucket problem is reported, not fatal.
|
|
78
|
+
return replicaCheck(await publish().catch(() => 'unreachable' as const), {
|
|
79
|
+
advisory: true,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
async abandon() {
|
|
83
|
+
if (!settled) await publish().catch(() => undefined);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|