@crvouga/dev-fleet 0.2.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 +28 -0
- package/ansible/macos/manager.yml +3 -0
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/ubuntu/policy.json +1 -0
- 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/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/bootstrap/native-t3.ts +22 -11
- 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/declared.md +3 -1
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +4 -3
- package/docs/topology.md +2 -0
- package/docs/ubuntu-sync.md +28 -22
- package/llms.txt +1 -0
- package/mise.toml +3 -1
- package/package.json +2 -2
- package/schema/fleet.schema.json +66 -0
- 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/launch.ts +84 -5
- package/src/agents/native-verify.ts +128 -2
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-manager.ts +20 -4
- package/src/cli/fleet-sync-output.ts +3 -2
- package/src/cli/fleet-sync-remote.ts +20 -13
- package/src/cli/fleet-sync.ts +36 -0
- package/src/cli/register-fleet.ts +35 -0
- package/src/cli/registry.ts +13 -0
- package/src/definition.ts +85 -3
- 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/manager-sync.ts +1 -15
- package/src/node-converge-linux.ts +28 -7
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- 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/windows.ts +0 -14
- package/src/remote-entry.ts +15 -1
- package/src/service-auth.ts +15 -5
- 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-linked-check.ts +1 -1
- package/src/transport.ts +3 -2
- package/src/worker-host-checks.ts +36 -13
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { FleetUsageError } from '../errors';
|
|
5
|
+
import { fleet } from '../fleet-config';
|
|
6
|
+
import { runCommand } from '../runner';
|
|
7
|
+
import { blocking, type Check, type Runner } from '../types';
|
|
8
|
+
import {
|
|
9
|
+
nativeLoginCheck,
|
|
10
|
+
nativeLogins,
|
|
11
|
+
providerAccountChecks,
|
|
12
|
+
rosterCurrentCheck,
|
|
13
|
+
} from '../accounts/checks';
|
|
14
|
+
import { exposedVersion, t3Settings } from '../accounts/context';
|
|
15
|
+
import { parseRoster, type ProxyRoster } from '../accounts/roster';
|
|
16
|
+
import { exposedInstances } from '../accounts/sessions';
|
|
17
|
+
import { forwardSourceOperation, ownerCheckout } from './source-operations';
|
|
18
|
+
import type { CommandOutcome } from './contracts';
|
|
19
|
+
import type { Settings } from './settings';
|
|
20
|
+
import { fleetSync } from './fleet-sync';
|
|
21
|
+
|
|
22
|
+
export interface FleetAccountsOptions {
|
|
23
|
+
diff?: boolean;
|
|
24
|
+
add?: boolean;
|
|
25
|
+
relogin?: boolean;
|
|
26
|
+
remove?: boolean;
|
|
27
|
+
removeNativeLogins?: boolean;
|
|
28
|
+
yes?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AccountsDependencies {
|
|
32
|
+
run?: Runner;
|
|
33
|
+
/** Runs a proxy command attached to the terminal (a sign-in). */
|
|
34
|
+
operate?: (args: string[]) => Promise<number>;
|
|
35
|
+
/** `fleet-sync --only accounts`. */
|
|
36
|
+
propagate?: () => Promise<CommandOutcome>;
|
|
37
|
+
home?: string;
|
|
38
|
+
settingsFile?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PROVIDER = /^[a-z][a-z0-9-]{0,31}$/;
|
|
42
|
+
|
|
43
|
+
/** `devfleet fleet-accounts`: the provider account roster the proxy holds,
|
|
44
|
+
* each account's state, and the account checks. `--add`, `--relogin` and
|
|
45
|
+
* `--remove` change one account on the proxy and propagate the result to
|
|
46
|
+
* every node at once.
|
|
47
|
+
*/
|
|
48
|
+
export async function fleetAccounts(
|
|
49
|
+
options: FleetAccountsOptions,
|
|
50
|
+
args: string[],
|
|
51
|
+
settings: Settings,
|
|
52
|
+
dependencies: AccountsDependencies = {}
|
|
53
|
+
): Promise<CommandOutcome> {
|
|
54
|
+
const home = dependencies.home ?? homedir();
|
|
55
|
+
if (options.removeNativeLogins)
|
|
56
|
+
return removeNativeLogins(Boolean(options.yes), home);
|
|
57
|
+
const change = lifecycle(options, args);
|
|
58
|
+
const changed = change
|
|
59
|
+
? await applyChange(change, settings, dependencies)
|
|
60
|
+
: undefined;
|
|
61
|
+
if (changed && !changed.data) return failedChange();
|
|
62
|
+
return report(options, settings, dependencies, { change, changed });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function report(
|
|
66
|
+
options: FleetAccountsOptions,
|
|
67
|
+
settings: Settings,
|
|
68
|
+
dependencies: AccountsDependencies,
|
|
69
|
+
{
|
|
70
|
+
change,
|
|
71
|
+
changed,
|
|
72
|
+
}: { change: string[] | null; changed: CommandOutcome | undefined }
|
|
73
|
+
): Promise<CommandOutcome> {
|
|
74
|
+
const home = dependencies.home ?? homedir();
|
|
75
|
+
const roster = await readRoster(
|
|
76
|
+
dependencies.run ?? runCommand,
|
|
77
|
+
settings.workspace
|
|
78
|
+
);
|
|
79
|
+
const checks: Check[] = providerAccountChecks(
|
|
80
|
+
roster,
|
|
81
|
+
fleet.services.proxy.accounts?.minimumUsable
|
|
82
|
+
);
|
|
83
|
+
if (options.diff)
|
|
84
|
+
checks.push(...(await thisMachine(roster, home, dependencies)));
|
|
85
|
+
const ok = !checks.some(blocking) && !changed?.exitCode;
|
|
86
|
+
return {
|
|
87
|
+
exitCode: ok ? 0 : 1,
|
|
88
|
+
data: {
|
|
89
|
+
version: 1,
|
|
90
|
+
command: 'fleet-accounts',
|
|
91
|
+
ok,
|
|
92
|
+
...listing(roster),
|
|
93
|
+
checks,
|
|
94
|
+
...(change && {
|
|
95
|
+
changed: change.slice(0, 2),
|
|
96
|
+
propagation: changed?.data,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The roster as listed: no routing prefix, no file. */
|
|
103
|
+
function listing(roster: ProxyRoster) {
|
|
104
|
+
return {
|
|
105
|
+
rosterVersion: roster.rosterVersion,
|
|
106
|
+
managementApi: roster.managementApi,
|
|
107
|
+
accounts: roster.accounts.map(
|
|
108
|
+
({ provider, index, label, state, until }) => ({
|
|
109
|
+
provider,
|
|
110
|
+
index,
|
|
111
|
+
label,
|
|
112
|
+
state,
|
|
113
|
+
until,
|
|
114
|
+
})
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Runs the change on the proxy, then propagates it to every node. A change
|
|
120
|
+
* the proxy did not complete answers with no data: nothing was propagated.
|
|
121
|
+
*/
|
|
122
|
+
async function applyChange(
|
|
123
|
+
change: string[],
|
|
124
|
+
settings: Settings,
|
|
125
|
+
dependencies: AccountsDependencies
|
|
126
|
+
): Promise<CommandOutcome> {
|
|
127
|
+
const operate =
|
|
128
|
+
dependencies.operate ??
|
|
129
|
+
((forwarded: string[]) =>
|
|
130
|
+
forwardSourceOperation(['cliproxy', ...forwarded], settings));
|
|
131
|
+
if ((await operate(change)) !== 0) return { exitCode: 1, data: null };
|
|
132
|
+
return (
|
|
133
|
+
dependencies.propagate ??
|
|
134
|
+
(() => fleetSync({ only: 'accounts', json: true }, settings))
|
|
135
|
+
)();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function failedChange(): CommandOutcome {
|
|
139
|
+
return {
|
|
140
|
+
exitCode: 1,
|
|
141
|
+
data: {
|
|
142
|
+
version: 1,
|
|
143
|
+
command: 'fleet-accounts',
|
|
144
|
+
ok: false,
|
|
145
|
+
error: 'ACCOUNT_CHANGE_FAILED',
|
|
146
|
+
summary:
|
|
147
|
+
'The proxy did not complete the change; an interrupted sign-in leaves no credential behind. Nothing was propagated.',
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `--diff`: what this machine offers against the roster. */
|
|
153
|
+
async function thisMachine(
|
|
154
|
+
roster: ProxyRoster,
|
|
155
|
+
home: string,
|
|
156
|
+
dependencies: AccountsDependencies
|
|
157
|
+
): Promise<Check[]> {
|
|
158
|
+
const exposed = await exposedInstances(
|
|
159
|
+
dependencies.settingsFile ?? t3Settings()
|
|
160
|
+
);
|
|
161
|
+
return [
|
|
162
|
+
rosterCurrentCheck(roster.rosterVersion, exposedVersion(exposed)),
|
|
163
|
+
nativeLoginCheck(nativeLogins(home)),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The proxy command a lifecycle flag stands for, or null for a listing. */
|
|
168
|
+
function lifecycle(
|
|
169
|
+
options: FleetAccountsOptions,
|
|
170
|
+
[provider, index]: string[]
|
|
171
|
+
): string[] | null {
|
|
172
|
+
const chosen = (['add', 'relogin', 'remove'] as const).filter(
|
|
173
|
+
(flag) => options[flag]
|
|
174
|
+
);
|
|
175
|
+
if (chosen.length === 0) return null;
|
|
176
|
+
if (chosen.length > 1)
|
|
177
|
+
throw new FleetUsageError('Choose one of --add, --relogin and --remove.');
|
|
178
|
+
const flag = chosen[0]!;
|
|
179
|
+
if (!provider || !PROVIDER.test(provider))
|
|
180
|
+
throw new FleetUsageError(
|
|
181
|
+
`--${flag} needs a provider, such as claude or codex.`
|
|
182
|
+
);
|
|
183
|
+
if (flag === 'add') return ['add-account', provider];
|
|
184
|
+
if (!index || !/^[1-9][0-9]{0,3}$/.test(index))
|
|
185
|
+
throw new FleetUsageError(
|
|
186
|
+
`--${flag} needs the account's index, as devfleet fleet-accounts lists it.`
|
|
187
|
+
);
|
|
188
|
+
return [flag === 'relogin' ? 'relogin' : 'remove-account', provider, index];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The roster as the proxy on this manager reports it. */
|
|
192
|
+
async function readRoster(
|
|
193
|
+
run: Runner,
|
|
194
|
+
workspace: string
|
|
195
|
+
): Promise<ProxyRoster> {
|
|
196
|
+
const checkout = ownerCheckout(workspace);
|
|
197
|
+
if (!checkout)
|
|
198
|
+
throw new FleetUsageError(
|
|
199
|
+
'The account roster is kept by the proxy on the manager. Run devfleet fleet-accounts there, after a first fleet-sync.'
|
|
200
|
+
);
|
|
201
|
+
const result = await run(
|
|
202
|
+
[
|
|
203
|
+
'python3',
|
|
204
|
+
'-B',
|
|
205
|
+
join(checkout, 'cliproxy/scripts/cli.py'),
|
|
206
|
+
'accounts',
|
|
207
|
+
'--json',
|
|
208
|
+
],
|
|
209
|
+
{ cwd: checkout, timeoutMs: 60_000 }
|
|
210
|
+
);
|
|
211
|
+
try {
|
|
212
|
+
if (!result.ok) throw new Error();
|
|
213
|
+
return parseRoster(result.stdout);
|
|
214
|
+
} catch {
|
|
215
|
+
throw new FleetUsageError(
|
|
216
|
+
'The proxy did not report its accounts. Check it with devfleet cliproxy status.'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Opt-in: deletes the provider logins agent CLIs keep outside the proxy. */
|
|
222
|
+
async function removeNativeLogins(
|
|
223
|
+
yes: boolean,
|
|
224
|
+
home: string
|
|
225
|
+
): Promise<CommandOutcome> {
|
|
226
|
+
const found = nativeLogins(home);
|
|
227
|
+
if (!yes)
|
|
228
|
+
return {
|
|
229
|
+
exitCode: found.length ? 1 : 0,
|
|
230
|
+
data: {
|
|
231
|
+
version: 1,
|
|
232
|
+
command: 'fleet-accounts',
|
|
233
|
+
ok: found.length === 0,
|
|
234
|
+
applied: false,
|
|
235
|
+
checks: [nativeLoginCheck(found)],
|
|
236
|
+
next: found.length
|
|
237
|
+
? 'Add --yes to remove these logins from this machine.'
|
|
238
|
+
: undefined,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
for (const login of found) await rm(login.path, { force: true });
|
|
242
|
+
return {
|
|
243
|
+
exitCode: 0,
|
|
244
|
+
data: {
|
|
245
|
+
version: 1,
|
|
246
|
+
command: 'fleet-accounts',
|
|
247
|
+
ok: true,
|
|
248
|
+
applied: true,
|
|
249
|
+
removed: [...new Set(found.map((login) => login.cli))].sort(),
|
|
250
|
+
checks: [nativeLoginCheck(nativeLogins(home))],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { enrolledRoster } from '../definition';
|
|
2
|
+
import { selectNodes } from '../config';
|
|
3
|
+
import { fleet as definition } from '../fleet-config';
|
|
4
|
+
import { bundleProbe } from '../transport';
|
|
5
|
+
import { machineProviders } from '../t3/providers';
|
|
6
|
+
import { accountContext, nodeAccountChecks } from '../accounts/context';
|
|
7
|
+
import { propagateAccounts } from '../accounts/propagate';
|
|
8
|
+
import { loadRoster } from './roster';
|
|
9
|
+
import { ownerCheckout } from './source-operations';
|
|
10
|
+
import type { Check, FleetNode, Runner } from '../types';
|
|
11
|
+
import type { FleetSyncOptions } from './fleet-sync';
|
|
12
|
+
import type { Settings } from './settings';
|
|
13
|
+
import type { CommandOutcome } from './contracts';
|
|
14
|
+
|
|
15
|
+
/** `fleet-sync --only accounts` on the manager. */
|
|
16
|
+
export async function syncAccounts(
|
|
17
|
+
options: FleetSyncOptions,
|
|
18
|
+
settings: Settings,
|
|
19
|
+
{ revision, run }: { revision: string; run: Runner }
|
|
20
|
+
): Promise<CommandOutcome> {
|
|
21
|
+
let nodes: FleetNode[] = [];
|
|
22
|
+
try {
|
|
23
|
+
nodes = selectNodes(
|
|
24
|
+
enrolledRoster(definition, await loadRoster(settings.config)),
|
|
25
|
+
options.node ?? []
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
// No enrolled worker yet: the manager alone.
|
|
29
|
+
}
|
|
30
|
+
return propagateAccounts({
|
|
31
|
+
nodes,
|
|
32
|
+
revision,
|
|
33
|
+
source: await bundleProbe(),
|
|
34
|
+
manager: () => managerAccounts(settings.workspace),
|
|
35
|
+
run,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The manager's own account instances, from the checkout it runs. */
|
|
40
|
+
export async function managerAccounts(workspace: string): Promise<Check[]> {
|
|
41
|
+
const checkout = ownerCheckout(workspace);
|
|
42
|
+
if (!checkout)
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
id: 't3:providers',
|
|
46
|
+
status: 'fail',
|
|
47
|
+
summary: 'No revision is applied on the manager yet.',
|
|
48
|
+
remedy: {
|
|
49
|
+
action: 'Run a full fleet-sync first.',
|
|
50
|
+
command: 'devfleet fleet-sync',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
return [
|
|
55
|
+
await machineProviders(definition.manager.id, checkout, true),
|
|
56
|
+
...(await nodeAccountChecks(await accountContext())),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
@@ -6,11 +6,13 @@ 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';
|
|
14
16
|
import macos from '../../ansible/macos/policy.json';
|
|
15
17
|
import { ansibleCommand } from '../platform/ansible';
|
|
16
18
|
import { PACKAGE_ROOT } from '../package-info';
|
|
@@ -39,8 +41,15 @@ export async function convergeManager(
|
|
|
39
41
|
if (input.apply) installConfig();
|
|
40
42
|
checks.push(await managerPower(run, input.workspace));
|
|
41
43
|
checks.push(await managerDocker(run));
|
|
42
|
-
|
|
43
|
-
checks.push(
|
|
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
|
+
);
|
|
44
53
|
if (input.apply) {
|
|
45
54
|
// Same provider profiles and the same Vault gateway credential as every
|
|
46
55
|
// worker: the instances above launch from these profiles, and the check
|
|
@@ -55,10 +64,17 @@ export async function convergeManager(
|
|
|
55
64
|
);
|
|
56
65
|
checks.push(...providers);
|
|
57
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
|
+
);
|
|
58
73
|
checks.push(
|
|
59
74
|
...(await attempt('agent:verification', () =>
|
|
60
75
|
verifyNativeAgents(id, input.workspace, run, {
|
|
61
76
|
outOfQuota: quotaLimited(providers),
|
|
77
|
+
...(accounts ? { accounts } : {}),
|
|
62
78
|
})
|
|
63
79
|
))
|
|
64
80
|
);
|
|
@@ -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',
|
|
@@ -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';
|
|
@@ -92,7 +93,11 @@ export async function selected(
|
|
|
92
93
|
),
|
|
93
94
|
},
|
|
94
95
|
run,
|
|
95
|
-
deployWithMonitoring(payload, canonical, run,
|
|
96
|
+
deployWithMonitoring(payload, canonical, run, {
|
|
97
|
+
...ui,
|
|
98
|
+
workspace: settings.workspace,
|
|
99
|
+
roster: settings.config,
|
|
100
|
+
})
|
|
96
101
|
).finally(() => ui.progress.stop());
|
|
97
102
|
if (settle) report.checks.push(await settle(report));
|
|
98
103
|
return outcome(report, planFingerprint, started);
|
|
@@ -157,6 +162,9 @@ function deployWithMonitoring(
|
|
|
157
162
|
ui: {
|
|
158
163
|
progress: SyncProgress;
|
|
159
164
|
interactiveAuthorization: boolean;
|
|
165
|
+
/** The manager's checkout, and the roster an identity is recorded in. */
|
|
166
|
+
workspace: string;
|
|
167
|
+
roster: string;
|
|
160
168
|
}
|
|
161
169
|
): DeployNode {
|
|
162
170
|
return async (node, revision) => {
|
|
@@ -194,16 +202,15 @@ function deployWithMonitoring(
|
|
|
194
202
|
// Workers are Linux by definition.
|
|
195
203
|
ui.progress.step(node.id, 'Verifying SSH, OS and T3');
|
|
196
204
|
result.checks.push(...(await inspectBootstrap(node, run, 'linux')));
|
|
197
|
-
result.checks.push(...(await verifyT3Endpoint(node, run)));
|
|
198
|
-
// A worker T3 restarted by this sync (idle policy heal) needs a minute or
|
|
199
|
-
// two before the manager's link reconnects.
|
|
200
|
-
const restarted = result.checks.some(
|
|
201
|
-
(check) => check.summary === T3_RESTARTED_SUMMARY
|
|
202
|
-
);
|
|
203
205
|
result.checks.push(
|
|
204
|
-
await
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}))
|
|
207
214
|
);
|
|
208
215
|
if (
|
|
209
216
|
result.checks.some(
|
package/src/cli/fleet-sync.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { FleetLockError } from '../machine/lock-error';
|
|
|
36
36
|
import { selected } from './fleet-sync-remote';
|
|
37
37
|
import { managerIdentity } from './fleet-sync-manager';
|
|
38
38
|
import { managerRunScript, rehome } from './fleet-sync-checkout';
|
|
39
|
+
import { syncAccounts } from './fleet-sync-accounts';
|
|
39
40
|
import { packageInfo } from '../package-info';
|
|
40
41
|
import {
|
|
41
42
|
forwardingDisabled,
|
|
@@ -53,6 +54,8 @@ export interface FleetSyncOptions {
|
|
|
53
54
|
node?: string[];
|
|
54
55
|
all?: boolean;
|
|
55
56
|
schedule?: ScheduleAction;
|
|
57
|
+
/** `accounts`: only the account-derived configuration, on every node. */
|
|
58
|
+
only?: string;
|
|
56
59
|
/** Internal: this revision already converged the whole fleet, so no canary
|
|
57
60
|
* has to prove it first and every worker converges at once.
|
|
58
61
|
*/
|
|
@@ -80,6 +83,7 @@ export async function fleetSync(
|
|
|
80
83
|
if (!(await onManager(run)))
|
|
81
84
|
return offManager(options, settings, run, forward);
|
|
82
85
|
if (options.schedule === 'status') return scheduleStatus(settings, run);
|
|
86
|
+
if (options.only === 'accounts') return onlyAccounts(options, settings, run);
|
|
83
87
|
return managerSync(options, settings, {
|
|
84
88
|
run,
|
|
85
89
|
root: machineRoot(),
|
|
@@ -289,11 +293,43 @@ function validate(options: FleetSyncOptions): void {
|
|
|
289
293
|
throw new FleetUsageError(
|
|
290
294
|
'--revision requires an exact lowercase 40-character commit SHA.'
|
|
291
295
|
);
|
|
296
|
+
validateOnly(options);
|
|
292
297
|
if (options.dryRun && (options.apply || options.yes))
|
|
293
298
|
throw new FleetUsageError(
|
|
294
299
|
'--dry-run cannot be combined with apply confirmation.'
|
|
295
300
|
);
|
|
296
301
|
}
|
|
302
|
+
function validateOnly(options: FleetSyncOptions): void {
|
|
303
|
+
if (options.only !== undefined && options.only !== 'accounts')
|
|
304
|
+
throw new FleetUsageError('--only takes one scope: accounts.');
|
|
305
|
+
if (options.only && (options.dryRun || options.schedule))
|
|
306
|
+
throw new FleetUsageError(
|
|
307
|
+
'--only accounts applies at once; it takes neither --dry-run nor --schedule.'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** `--only accounts`: workers keep the revision they run; only their
|
|
312
|
+
* accounts change.
|
|
313
|
+
*/
|
|
314
|
+
async function onlyAccounts(
|
|
315
|
+
options: FleetSyncOptions,
|
|
316
|
+
settings: Settings,
|
|
317
|
+
run: Runner
|
|
318
|
+
): Promise<CommandOutcome> {
|
|
319
|
+
const root = machineRoot();
|
|
320
|
+
await ensurePrivateRoot(root);
|
|
321
|
+
const state = await loadEnrollment(legacyHome() ? root : null);
|
|
322
|
+
const revision =
|
|
323
|
+
state?.approvedRevision ??
|
|
324
|
+
(await selectedRevision(options.revision, undefined, settings, run));
|
|
325
|
+
if (!isRevision(revision))
|
|
326
|
+
return actionRequired(
|
|
327
|
+
'FLEET_REVISION_REQUIRED',
|
|
328
|
+
'No revision is applied yet. Run a full fleet-sync first.'
|
|
329
|
+
);
|
|
330
|
+
return syncAccounts(options, settings, { revision, run });
|
|
331
|
+
}
|
|
332
|
+
|
|
297
333
|
function interactive(options: FleetSyncOptions): boolean {
|
|
298
334
|
return (
|
|
299
335
|
![
|
|
@@ -7,6 +7,7 @@ import { executeFleet } from './fleet';
|
|
|
7
7
|
import { fleetSync, type FleetSyncOptions } from './fleet-sync';
|
|
8
8
|
import { syncNode, type NodeSyncOptions } from './node-sync';
|
|
9
9
|
import { fleetState, type FleetStateOptions } from './fleet-state';
|
|
10
|
+
import { fleetAccounts, type FleetAccountsOptions } from './fleet-accounts';
|
|
10
11
|
|
|
11
12
|
type Complete = (outcome: CommandOutcome) => void;
|
|
12
13
|
|
|
@@ -198,6 +199,10 @@ function registerSync({ program, defaults, complete }: Registration) {
|
|
|
198
199
|
'--schedule <action>',
|
|
199
200
|
'Manager-only schedule: enable, pause, resume, disable, status'
|
|
200
201
|
)
|
|
202
|
+
.option(
|
|
203
|
+
'--only <scope>',
|
|
204
|
+
'accounts: only the account-derived configuration, on every node at once, without a full sync'
|
|
205
|
+
)
|
|
201
206
|
.action(async (_options, command: Command) => {
|
|
202
207
|
const options = command.optsWithGlobals<
|
|
203
208
|
FleetSyncOptions & Partial<Settings>
|
|
@@ -293,4 +298,34 @@ export function registerFleet(
|
|
|
293
298
|
registerState(registration);
|
|
294
299
|
registerRepair(registration);
|
|
295
300
|
registerNodes(registration);
|
|
301
|
+
registerAccounts(registration);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function registerAccounts({ program, defaults, complete }: Registration) {
|
|
305
|
+
program
|
|
306
|
+
.command('fleet-accounts [args...]')
|
|
307
|
+
.description(
|
|
308
|
+
'List the provider accounts signed in on the CLI proxy and their state; add, sign in again or remove one'
|
|
309
|
+
)
|
|
310
|
+
.option('--diff', 'Also compare what this machine offers with the roster')
|
|
311
|
+
.option('--add', 'Sign a new account in: --add PROVIDER')
|
|
312
|
+
.option('--relogin', 'Sign one account in again: --relogin PROVIDER INDEX')
|
|
313
|
+
.option('--remove', 'Remove one account: --remove PROVIDER INDEX')
|
|
314
|
+
.option(
|
|
315
|
+
'--remove-native-logins',
|
|
316
|
+
'Remove provider logins agent CLIs keep outside the proxy on this machine; needs --yes'
|
|
317
|
+
)
|
|
318
|
+
.option('--yes', 'Confirm --remove-native-logins')
|
|
319
|
+
.action(async (args: string[], _options, command: Command) => {
|
|
320
|
+
const options = command.optsWithGlobals<
|
|
321
|
+
FleetAccountsOptions & CliOptions
|
|
322
|
+
>();
|
|
323
|
+
complete(
|
|
324
|
+
await fleetAccounts(
|
|
325
|
+
options,
|
|
326
|
+
args,
|
|
327
|
+
await withSettings(defaults, options)
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
});
|
|
296
331
|
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -135,6 +135,19 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
135
135
|
['onboarding', 'register', 'new', 'setup', 'add'],
|
|
136
136
|
true,
|
|
137
137
|
],
|
|
138
|
+
[
|
|
139
|
+
'fleet-accounts',
|
|
140
|
+
'List provider accounts and their state',
|
|
141
|
+
'Read-only: the provider accounts signed in on the CLI proxy (provider, index, label, state, when a cooldown clears), the roster version and the proxy:account:<provider> checks. --diff also compares what this machine offers with the roster.',
|
|
142
|
+
['accounts', 'providers', 'login', 'quota', 'cooldown', 'roster'],
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'fleet-accounts --add',
|
|
146
|
+
'Add, sign in again or remove a provider account',
|
|
147
|
+
'--add PROVIDER, --relogin PROVIDER INDEX or --remove PROVIDER INDEX changes one account on the CLI proxy (signing in is the only manual step), then propagates the accounts to every node with fleet-sync --only accounts.',
|
|
148
|
+
['accounts', 'login', 'relogin', 'remove', 'add', 'sign in'],
|
|
149
|
+
true,
|
|
150
|
+
],
|
|
138
151
|
]),
|
|
139
152
|
...group('T3', [
|
|
140
153
|
[
|