@crvouga/dev-fleet 0.4.0 → 0.5.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 +3 -1
- package/ansible/macos/policy.json +1 -0
- package/ansible/platforms.json +111 -0
- package/ansible/ubuntu/authorize-sudo.sh +7 -16
- package/ansible/ubuntu/baseline.yml +4 -28
- package/ansible/ubuntu/disk-gc.py +186 -4
- package/ansible/ubuntu/policy.json +18 -3
- package/ansible/ubuntu/t3-desktop.yml +2 -2
- package/ansible/ubuntu/tailscale.yml +20 -9
- package/ansible/ubuntu/tasks/dpkg.yml +46 -0
- package/ansible/ubuntu/tasks/install.yml +38 -0
- package/ansible/ubuntu/tasks/packages.yml +4 -55
- package/ansible/ubuntu/tasks/release.yml +25 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -5
- package/ansible/ubuntu/tasks/snapshot.yml +50 -0
- package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
- package/bootstrap/fleet-remote-bootstrap.sh +18 -41
- package/docs/consumer-setup.md +32 -30
- package/docs/declared.md +4 -5
- package/docs/runbooks.md +3 -6
- package/docs/support-matrix.md +36 -0
- package/docs/ubuntu-sync.md +9 -7
- package/docs/worker-contract.md +28 -1
- package/package.json +2 -2
- package/schema/check-ids.json +8 -4
- package/schema/fleet.schema.json +7 -3
- package/src/cli/fleet-sync-remote.ts +1 -1
- package/src/definition.ts +18 -7
- package/src/machine/platform.ts +48 -28
- package/src/machine/support-matrix.ts +76 -0
- package/src/manager-sync.ts +15 -7
- package/src/network-definition.ts +1 -1
- package/src/node-converge.ts +1 -1
- package/src/platform/debian-version.ts +62 -0
- package/src/platform/ubuntu-audit.ts +11 -4
- package/src/platform/ubuntu-release.ts +41 -0
- package/src/remote-bootstrap.ts +2 -0
- package/src/worker-host-checks.ts +36 -10
package/schema/fleet.schema.json
CHANGED
|
@@ -253,13 +253,17 @@
|
|
|
253
253
|
"required": ["release", "architectures"],
|
|
254
254
|
"properties": {
|
|
255
255
|
"release": {
|
|
256
|
-
"
|
|
256
|
+
"type": "string",
|
|
257
|
+
"pattern": "^[0-9]{2}\\.[0-9]{2}$",
|
|
258
|
+
"description": "An Ubuntu release ansible/ubuntu/policy.json accepts (docs/support-matrix.md)."
|
|
257
259
|
},
|
|
258
260
|
"architectures": {
|
|
259
|
-
"
|
|
261
|
+
"type": "array",
|
|
262
|
+
"items": { "enum": ["amd64", "arm64"] },
|
|
263
|
+
"description": "Exactly the architectures the policy names for that release."
|
|
260
264
|
}
|
|
261
265
|
},
|
|
262
|
-
"description": "The
|
|
266
|
+
"description": "The worker release and its architectures, as the Ubuntu policy of the pinned dev-fleet names them."
|
|
263
267
|
},
|
|
264
268
|
"worker": {
|
|
265
269
|
"type": "object",
|
|
@@ -203,7 +203,7 @@ function deployWithMonitoring(
|
|
|
203
203
|
);
|
|
204
204
|
// Workers are Linux by definition.
|
|
205
205
|
ui.progress.step(node.id, 'Verifying SSH, OS and T3');
|
|
206
|
-
result.checks.push(...(await inspectBootstrap(node, run
|
|
206
|
+
result.checks.push(...(await inspectBootstrap(node, run)));
|
|
207
207
|
result.checks.push(
|
|
208
208
|
...(await workerChecks(enabled(idesOf(canonical), IDES), node, {
|
|
209
209
|
run,
|
package/src/definition.ts
CHANGED
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
type T3Definition,
|
|
18
18
|
} from './ide/definition';
|
|
19
19
|
import { parseNotify, type NotifyConfig } from './notify';
|
|
20
|
+
import { ubuntuRelease } from './platform/ubuntu-release';
|
|
20
21
|
|
|
21
22
|
export interface MachineDefinition {
|
|
22
23
|
id: string;
|
|
23
24
|
ssh: string;
|
|
24
|
-
os: 'linux'
|
|
25
|
+
os: 'linux';
|
|
25
26
|
/** Tool profiles this worker adds to the fleet-wide ones. */
|
|
26
27
|
toolProfiles?: ToolProfile[];
|
|
27
28
|
network?: {
|
|
@@ -78,7 +79,9 @@ export interface FleetDefinition {
|
|
|
78
79
|
git?: GitIdentity;
|
|
79
80
|
/** Where fleet-watch sends notifications; absent, nobody is notified. */
|
|
80
81
|
notify?: NotifyConfig;
|
|
81
|
-
|
|
82
|
+
/** An Ubuntu release ansible/ubuntu/policy.json accepts, with exactly the
|
|
83
|
+
* architectures the policy names for it. */
|
|
84
|
+
ubuntu: { release: string; architectures: string[] };
|
|
82
85
|
}
|
|
83
86
|
/** How provider accounts are watched and chosen (src/accounts/). */
|
|
84
87
|
export interface AccountPolicy {
|
|
@@ -449,14 +452,22 @@ function source(value: unknown): FleetDefinition['source'] {
|
|
|
449
452
|
return { repository: value['repository'] };
|
|
450
453
|
}
|
|
451
454
|
function baseline(ubuntu: unknown): FleetDefinition['ubuntu'] {
|
|
455
|
+
if (!record(ubuntu) || !keys(ubuntu, ['release', 'architectures']))
|
|
456
|
+
return fail();
|
|
457
|
+
const accepted =
|
|
458
|
+
typeof ubuntu['release'] === 'string'
|
|
459
|
+
? ubuntuRelease(ubuntu['release'])
|
|
460
|
+
: undefined;
|
|
452
461
|
if (
|
|
453
|
-
!
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
JSON.stringify(ubuntu['architectures']) !== '["amd64","arm64"]'
|
|
462
|
+
!accepted ||
|
|
463
|
+
JSON.stringify(ubuntu['architectures']) !==
|
|
464
|
+
JSON.stringify(accepted.architectures)
|
|
457
465
|
)
|
|
458
466
|
return fail();
|
|
459
|
-
return
|
|
467
|
+
return {
|
|
468
|
+
release: accepted.release,
|
|
469
|
+
architectures: [...accepted.architectures],
|
|
470
|
+
};
|
|
460
471
|
}
|
|
461
472
|
|
|
462
473
|
/** The configuration as every machine keeps it at
|
package/src/machine/platform.ts
CHANGED
|
@@ -1,44 +1,64 @@
|
|
|
1
|
-
import
|
|
1
|
+
import platforms from '../../ansible/platforms.json';
|
|
2
|
+
import { kernelArchitectures, ubuntuRelease } from '../platform/ubuntu-release';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** One POSIX probe for every worker, four lines: family, release, CPU and
|
|
5
|
+
* the environment it runs in (`wsl`, `container` or `machine`, a physical
|
|
6
|
+
* or virtual machine of its own). Read-only, and it needs nothing installed.
|
|
7
|
+
* `root` moves the files it reads, for a stand-in; workers use `/`.
|
|
8
|
+
*/
|
|
9
|
+
export function platformProbe(root = ''): string {
|
|
10
|
+
return `sh -c '${[
|
|
11
|
+
`if [ -r ${root}/etc/os-release ]; then . ${root}/etc/os-release; printf "%s\\n%s\\n" "$ID" "$VERSION_ID"; else printf "%s\\n\\n" "$(uname -s)"; fi`,
|
|
12
|
+
'uname -m',
|
|
13
|
+
`if grep -qiE "microsoft|wsl" ${root}/proc/sys/kernel/osrelease 2>/dev/null; then echo wsl`,
|
|
14
|
+
`elif [ -e ${root}/.dockerenv ] || [ -e ${root}/run/.containerenv ] || systemd-detect-virt --container --quiet 2>/dev/null; then echo container`,
|
|
15
|
+
'else echo machine; fi',
|
|
16
|
+
].join('; ')}'`;
|
|
17
|
+
}
|
|
18
|
+
export const PLATFORM_PROBE = platformProbe();
|
|
8
19
|
|
|
9
20
|
export interface PlatformBaseline {
|
|
10
|
-
id: 'ubuntu:release' | '
|
|
21
|
+
id: 'ubuntu:release' | 'platform:environment';
|
|
11
22
|
supported: boolean;
|
|
12
23
|
summary: string;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
/**
|
|
16
|
-
export function evaluatePlatform(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const supported =
|
|
22
|
-
/^macos\n(1[4-9]|[2-9][0-9])(\.[0-9]+)*\n(arm64|x86_64)\s*$/.test(probe);
|
|
23
|
-
return {
|
|
24
|
-
id: 'macos:release',
|
|
25
|
-
supported,
|
|
26
|
-
summary: supported
|
|
27
|
-
? 'macOS 14 or newer and a supported CPU architecture observed; macOS policy is best effort.'
|
|
28
|
-
: 'MACOS_BASELINE_MISMATCH',
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const supported = /^ubuntu\n26\.04\n(x86_64|aarch64)\s*$/.test(probe);
|
|
26
|
+
/** A worker is an accepted Ubuntu release on one of that release's CPUs. */
|
|
27
|
+
export function evaluatePlatform(probe: string): PlatformBaseline {
|
|
28
|
+
const [family, version, cpu] = probe.split('\n').map((line) => line.trim());
|
|
29
|
+
const release = family === 'ubuntu' ? ubuntuRelease(version) : undefined;
|
|
30
|
+
const supported =
|
|
31
|
+
release !== undefined && kernelArchitectures(release).includes(cpu ?? '');
|
|
32
32
|
return {
|
|
33
33
|
id: 'ubuntu:release',
|
|
34
34
|
supported,
|
|
35
35
|
summary: supported
|
|
36
|
-
?
|
|
36
|
+
? `Ubuntu ${release.release} and supported CPU architecture observed.`
|
|
37
37
|
: 'UBUNTU_BASELINE_MISMATCH',
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/** Environments the support matrix (ansible/platforms.json) refuses, by the
|
|
42
|
+
* word the probe prints for them. */
|
|
43
|
+
const REFUSED = new Map(
|
|
44
|
+
platforms.rows.flatMap((row) =>
|
|
45
|
+
'environment' in row && row.status === 'refused'
|
|
46
|
+
? [[row.environment, row.platform] as const]
|
|
47
|
+
: []
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/** WSL2 and containers run an Ubuntu userland without systemd, a display
|
|
52
|
+
* manager or a power policy of their own; they are refused by name. */
|
|
53
|
+
export function evaluateEnvironment(probe: string): PlatformBaseline {
|
|
54
|
+
const environment = probe.split('\n')[3]?.trim() ?? '';
|
|
55
|
+
const refused = REFUSED.get(environment);
|
|
56
|
+
const supported = environment === 'machine';
|
|
57
|
+
return {
|
|
58
|
+
id: 'platform:environment',
|
|
59
|
+
supported,
|
|
60
|
+
summary: supported
|
|
61
|
+
? 'A machine of its own observed: not WSL2 and not a container.'
|
|
62
|
+
: `WORKER_ENVIRONMENT_UNSUPPORTED: ${refused ?? 'unknown environment'}`,
|
|
63
|
+
};
|
|
44
64
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import macos from '../../ansible/macos/policy.json';
|
|
2
|
+
import platforms from '../../ansible/platforms.json';
|
|
3
|
+
import { UBUNTU_RELEASES } from '../platform/ubuntu-release';
|
|
4
|
+
|
|
5
|
+
export type SupportStatus = 'supported' | 'best effort' | 'refused';
|
|
6
|
+
export interface SupportRow {
|
|
7
|
+
role: string;
|
|
8
|
+
platform: string;
|
|
9
|
+
versions: string;
|
|
10
|
+
architectures: string;
|
|
11
|
+
status: SupportStatus;
|
|
12
|
+
note: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Every platform and what dev-fleet does with it, from the policies the code
|
|
16
|
+
* reads: the supported rows from the macOS and Ubuntu policies, the rest from
|
|
17
|
+
* ansible/platforms.json. docs/support-matrix.md is this table. */
|
|
18
|
+
export function supportMatrix(): SupportRow[] {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
role: 'manager',
|
|
22
|
+
platform: 'macOS',
|
|
23
|
+
versions: `${macos.minimumMajorVersion} or newer`,
|
|
24
|
+
architectures: macos.architectures.join(', '),
|
|
25
|
+
status: 'supported',
|
|
26
|
+
note: 'ansible/macos/policy.json',
|
|
27
|
+
},
|
|
28
|
+
...UBUNTU_RELEASES.map((entry): SupportRow => ({
|
|
29
|
+
role: 'worker',
|
|
30
|
+
platform: 'Ubuntu',
|
|
31
|
+
versions: `${entry.release} (${entry.codename})`,
|
|
32
|
+
architectures: entry.architectures.join(', '),
|
|
33
|
+
status: 'supported',
|
|
34
|
+
note: 'ansible/ubuntu/policy.json; a machine of its own, physical or virtual',
|
|
35
|
+
})),
|
|
36
|
+
...platforms.rows.map((row): SupportRow => ({
|
|
37
|
+
role: row.role,
|
|
38
|
+
platform: row.platform,
|
|
39
|
+
versions: row.versions,
|
|
40
|
+
architectures: row.architectures,
|
|
41
|
+
status: row.status as SupportStatus,
|
|
42
|
+
note: row.note,
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderSupportMatrix(rows = supportMatrix()): string {
|
|
48
|
+
const header = [
|
|
49
|
+
'Role',
|
|
50
|
+
'Platform',
|
|
51
|
+
'Versions',
|
|
52
|
+
'Architectures',
|
|
53
|
+
'Status',
|
|
54
|
+
'Note',
|
|
55
|
+
];
|
|
56
|
+
const cells = rows.map((row) => [
|
|
57
|
+
row.role,
|
|
58
|
+
row.platform,
|
|
59
|
+
row.versions,
|
|
60
|
+
row.architectures,
|
|
61
|
+
row.status,
|
|
62
|
+
row.note,
|
|
63
|
+
]);
|
|
64
|
+
const widths = header.map((title, index) =>
|
|
65
|
+
Math.max(title.length, ...cells.map((line) => line[index]!.length))
|
|
66
|
+
);
|
|
67
|
+
const line = (values: string[]) =>
|
|
68
|
+
`| ${values.map((value, index) => value.padEnd(widths[index]!)).join(' | ')} |`;
|
|
69
|
+
return [
|
|
70
|
+
line(header),
|
|
71
|
+
`| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`,
|
|
72
|
+
...cells.map(line),
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (import.meta.main) process.stdout.write(`${renderSupportMatrix()}\n`);
|
package/src/manager-sync.ts
CHANGED
|
@@ -16,10 +16,9 @@ import { sshArgs, sshFailure } from './ssh';
|
|
|
16
16
|
import type { FleetDefinition, MachineDefinition } from './definition';
|
|
17
17
|
import { publicHubUrl } from './monitoring/beszel';
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
evaluateEnvironment,
|
|
20
20
|
evaluatePlatform,
|
|
21
21
|
PLATFORM_PROBE,
|
|
22
|
-
type MachinePlatform,
|
|
23
22
|
} from './machine/platform';
|
|
24
23
|
|
|
25
24
|
export interface ManagerNode extends NodeReport {
|
|
@@ -51,14 +50,14 @@ export type DeployNode = (
|
|
|
51
50
|
/** Minimal read-only POSIX inspection requires no workspace, Node, Bun or Git. */
|
|
52
51
|
export async function inspectBootstrap(
|
|
53
52
|
node: FleetNode,
|
|
54
|
-
run: Runner
|
|
55
|
-
os: MachinePlatform = 'linux'
|
|
53
|
+
run: Runner
|
|
56
54
|
): Promise<Check[]> {
|
|
57
55
|
const result = await run([...sshArgs(node), PLATFORM_PROBE], {
|
|
58
56
|
timeoutMs: 20_000,
|
|
59
57
|
});
|
|
60
58
|
if (!result.ok) return [sshFailure(result)];
|
|
61
|
-
const baseline = evaluatePlatform(
|
|
59
|
+
const baseline = evaluatePlatform(result.stdout);
|
|
60
|
+
const environment = evaluateEnvironment(result.stdout);
|
|
62
61
|
return [
|
|
63
62
|
{
|
|
64
63
|
id: 'ssh:fresh-session',
|
|
@@ -75,6 +74,15 @@ export async function inspectBootstrap(
|
|
|
75
74
|
'Review the release/architecture migration in source. No operating-system upgrade or downgrade is performed.',
|
|
76
75
|
},
|
|
77
76
|
},
|
|
77
|
+
{
|
|
78
|
+
id: environment.id,
|
|
79
|
+
status: environment.supported ? 'pass' : 'fail',
|
|
80
|
+
summary: environment.summary,
|
|
81
|
+
remedy: {
|
|
82
|
+
action:
|
|
83
|
+
'Run the worker on a physical or virtual machine of its own; docs/support-matrix.md lists what is supported.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
78
86
|
];
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -124,7 +132,7 @@ export async function synchronizeManager(
|
|
|
124
132
|
// worker is then verified against.
|
|
125
133
|
if (managerRun) await managerRun;
|
|
126
134
|
const workers = await Promise.all(
|
|
127
|
-
input.definition.workers.
|
|
135
|
+
input.definition.workers.map((machine) => {
|
|
128
136
|
input.progress?.(
|
|
129
137
|
machine.id,
|
|
130
138
|
'Inspecting enrolled identity and fresh SSH access'
|
|
@@ -329,7 +337,7 @@ async function inspectManagerNode(
|
|
|
329
337
|
},
|
|
330
338
|
});
|
|
331
339
|
else {
|
|
332
|
-
checks.push(...(await inspectBootstrap(node, run
|
|
340
|
+
checks.push(...(await inspectBootstrap(node, run)));
|
|
333
341
|
checks.push(...identityChecks(node, peer));
|
|
334
342
|
if (peer) checks.push(keyExpiryCheck(peer));
|
|
335
343
|
}
|
package/src/node-converge.ts
CHANGED
|
@@ -23,7 +23,7 @@ async function main(): Promise<Check[]> {
|
|
|
23
23
|
summary: 'PLATFORM_UNSUPPORTED',
|
|
24
24
|
remedy: {
|
|
25
25
|
action:
|
|
26
|
-
'Workers
|
|
26
|
+
'Workers run an Ubuntu release docs/support-matrix.md lists. The macOS manager converges itself locally in fleet-sync.',
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Debian version ordering, as `dpkg --compare-versions` applies it:
|
|
2
|
+
* epoch, then upstream version, then revision. Negative when `a` sorts
|
|
3
|
+
* before `b`, zero when they are the same version, positive after. */
|
|
4
|
+
export function compareDebianVersions(a: string, b: string): number {
|
|
5
|
+
const [epochA, upstreamA, revisionA] = split(a);
|
|
6
|
+
const [epochB, upstreamB, revisionB] = split(b);
|
|
7
|
+
return (
|
|
8
|
+
epochA - epochB ||
|
|
9
|
+
comparePart(upstreamA, upstreamB) ||
|
|
10
|
+
comparePart(revisionA, revisionB)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function split(version: string): [number, string, string] {
|
|
15
|
+
const colon = version.indexOf(':');
|
|
16
|
+
const epoch = colon > 0 ? Number(version.slice(0, colon)) || 0 : 0;
|
|
17
|
+
const rest = colon > 0 ? version.slice(colon + 1) : version;
|
|
18
|
+
const dash = rest.lastIndexOf('-');
|
|
19
|
+
return dash > 0
|
|
20
|
+
? [epoch, rest.slice(0, dash), rest.slice(dash + 1)]
|
|
21
|
+
: [epoch, rest, '0'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `~` sorts before everything, even the end of the string; letters sort
|
|
25
|
+
* before every other character. */
|
|
26
|
+
function order(char: string | undefined): number {
|
|
27
|
+
if (char === undefined) return 0;
|
|
28
|
+
if (char === '~') return -1;
|
|
29
|
+
if (/[A-Za-z]/.test(char)) return char.charCodeAt(0);
|
|
30
|
+
return char.charCodeAt(0) + 256;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Alternating runs: text, then digits, then text again, starting with text. */
|
|
34
|
+
function runs(part: string): string[] {
|
|
35
|
+
return part
|
|
36
|
+
.match(/\D*\d*/g)!
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.flatMap((run) => {
|
|
39
|
+
const digits = /\d*$/.exec(run)![0];
|
|
40
|
+
return [run.slice(0, run.length - digits.length), digits];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compareText(a: string, b: string): number {
|
|
45
|
+
for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
|
|
46
|
+
const difference = order(a[i]) - order(b[i]);
|
|
47
|
+
if (difference) return difference;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function comparePart(a: string, b: string): number {
|
|
53
|
+
const left = runs(a);
|
|
54
|
+
const right = runs(b);
|
|
55
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 2) {
|
|
56
|
+
const difference =
|
|
57
|
+
compareText(left[i] ?? '', right[i] ?? '') ||
|
|
58
|
+
Number(left[i + 1] || 0) - Number(right[i + 1] || 0);
|
|
59
|
+
if (difference) return difference;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import policy from '../../ansible/ubuntu/policy.json';
|
|
2
1
|
import type { Runner } from '../types';
|
|
2
|
+
import { packagePins, UBUNTU_RELEASES, ubuntuRelease } from './ubuntu-release';
|
|
3
3
|
|
|
4
4
|
export interface UbuntuAudit {
|
|
5
5
|
supported: boolean;
|
|
@@ -11,7 +11,13 @@ export interface UbuntuAudit {
|
|
|
11
11
|
passwordlessSudo: boolean;
|
|
12
12
|
rebootPending: boolean;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// Every package any accepted release pins, so one audit serves them all.
|
|
15
|
+
const PACKAGES = [
|
|
16
|
+
...new Set([
|
|
17
|
+
...UBUNTU_RELEASES.flatMap((release) => Object.keys(packagePins(release))),
|
|
18
|
+
'tailscale',
|
|
19
|
+
]),
|
|
20
|
+
];
|
|
15
21
|
|
|
16
22
|
/** Read-only audit: never emit package sources, environment, accounts or raw errors. */
|
|
17
23
|
export async function auditUbuntu(run: Runner): Promise<UbuntuAudit> {
|
|
@@ -32,14 +38,15 @@ export async function auditUbuntu(run: Runner): Promise<UbuntuAudit> {
|
|
|
32
38
|
const version =
|
|
33
39
|
/^VERSION_ID="?([0-9]+\.[0-9]+)"?$/m.exec(release.stdout)?.[1] ?? null;
|
|
34
40
|
const reported = arch.stdout.trim();
|
|
35
|
-
const
|
|
41
|
+
const accepted = ubuntuRelease(version);
|
|
42
|
+
const architecture = accepted?.architectures.includes(reported)
|
|
36
43
|
? reported
|
|
37
44
|
: null;
|
|
38
45
|
return {
|
|
39
46
|
supported:
|
|
40
47
|
release.ok &&
|
|
41
48
|
/^ID=ubuntu$/m.test(release.stdout) &&
|
|
42
|
-
|
|
49
|
+
accepted !== undefined &&
|
|
43
50
|
architecture !== null,
|
|
44
51
|
release: version,
|
|
45
52
|
architecture,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import policy from '../../ansible/ubuntu/policy.json';
|
|
2
|
+
|
|
3
|
+
/** One Ubuntu release workers may run. The accepted releases are named once,
|
|
4
|
+
* in ansible/ubuntu/policy.json; nothing else spells a release or codename. */
|
|
5
|
+
export interface UbuntuRelease {
|
|
6
|
+
release: string;
|
|
7
|
+
codename: string;
|
|
8
|
+
/** Debian architecture names, as `dpkg --print-architecture` reports them. */
|
|
9
|
+
architectures: string[];
|
|
10
|
+
/** Pins of this release that differ from the policy's `packages`. */
|
|
11
|
+
packages?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const UBUNTU_RELEASES: readonly UbuntuRelease[] = policy.releases;
|
|
15
|
+
|
|
16
|
+
/** The package pins of one release: the policy's, with the release's own
|
|
17
|
+
* pins over them (as tasks/release.yml combines them for Ansible). */
|
|
18
|
+
export function packagePins(
|
|
19
|
+
release: UbuntuRelease | undefined
|
|
20
|
+
): Record<string, string> {
|
|
21
|
+
return { ...policy.packages, ...release?.packages };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `uname -m` for each Debian architecture name. */
|
|
25
|
+
const KERNEL_ARCHITECTURES: Record<string, string> = {
|
|
26
|
+
amd64: 'x86_64',
|
|
27
|
+
arm64: 'aarch64',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function ubuntuRelease(
|
|
31
|
+
version: string | null | undefined
|
|
32
|
+
): UbuntuRelease | undefined {
|
|
33
|
+
return UBUNTU_RELEASES.find((entry) => entry.release === version);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The `uname -m` names one release runs on. */
|
|
37
|
+
export function kernelArchitectures(release: UbuntuRelease): string[] {
|
|
38
|
+
return release.architectures.map(
|
|
39
|
+
(name) => KERNEL_ARCHITECTURES[name] ?? name
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/remote-bootstrap.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { record } from './config';
|
|
24
24
|
import { installedDefinition, type FleetDefinition } from './definition';
|
|
25
25
|
import { cleanCheck, validCheck } from './protocol';
|
|
26
|
+
import { UBUNTU_RELEASES } from './platform/ubuntu-release';
|
|
26
27
|
|
|
27
28
|
export interface BootstrapPayload {
|
|
28
29
|
revision: string;
|
|
@@ -127,6 +128,7 @@ export async function bootstrapRemote(
|
|
|
127
128
|
payload.digest,
|
|
128
129
|
node.id,
|
|
129
130
|
transfer.mode,
|
|
131
|
+
UBUNTU_RELEASES.map((entry) => entry.release).join(' '),
|
|
130
132
|
]
|
|
131
133
|
.map(shellQuote)
|
|
132
134
|
.join(' ');
|
|
@@ -5,6 +5,8 @@ import { verifyResourceSafety } from './platform/resource-verify';
|
|
|
5
5
|
import policy from '../ansible/ubuntu/policy.json';
|
|
6
6
|
import { userInfo } from 'node:os';
|
|
7
7
|
import { T3_HEALTH_SCRIPT, runningVersionCheck } from './platform/t3-service';
|
|
8
|
+
import { compareDebianVersions } from './platform/debian-version';
|
|
9
|
+
import { packagePins, ubuntuRelease } from './platform/ubuntu-release';
|
|
8
10
|
import type { Check, Runner } from './types';
|
|
9
11
|
|
|
10
12
|
function check(id: string, ok: boolean, summary: string): Check {
|
|
@@ -22,6 +24,34 @@ function check(id: string, ok: boolean, summary: string): Check {
|
|
|
22
24
|
: {}),
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/** A pin is a minimum (docs/worker-contract.md): Ubuntu's security updates
|
|
28
|
+
* may move a package past it, which is reported but never undone. */
|
|
29
|
+
export function pinnedPackage(
|
|
30
|
+
name: string,
|
|
31
|
+
pinned: string,
|
|
32
|
+
installed: string | undefined
|
|
33
|
+
): Check {
|
|
34
|
+
const order =
|
|
35
|
+
installed === undefined ? -1 : compareDebianVersions(installed, pinned);
|
|
36
|
+
if (order <= 0)
|
|
37
|
+
return check(
|
|
38
|
+
`package:${name}`,
|
|
39
|
+
order === 0,
|
|
40
|
+
order === 0
|
|
41
|
+
? 'Installed at the version the policy pins.'
|
|
42
|
+
: `PACKAGE_BELOW_PIN ${name} ${installed ?? 'missing'}, pinned ${pinned}`
|
|
43
|
+
);
|
|
44
|
+
return {
|
|
45
|
+
id: `worker:package:${name}`,
|
|
46
|
+
status: 'warn',
|
|
47
|
+
summary: `PACKAGE_NEWER_THAN_PIN ${name} ${installed}, pinned ${pinned}`,
|
|
48
|
+
remedy: {
|
|
49
|
+
action:
|
|
50
|
+
'An unattended security update moved this package past its pin, and it is kept. Raise the pin in the Ubuntu policy when the snapshot moves.',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
/** Observed unit state for a failed service check, e.g.
|
|
26
56
|
* `SERVICE_NOT_INSTALLED beszel-agent.service (inactive)`. */
|
|
27
57
|
export function serviceState(name: string, show: string): string {
|
|
@@ -56,17 +86,13 @@ export async function workerHostChecks(
|
|
|
56
86
|
audit.passwordlessSudo,
|
|
57
87
|
'Designated worker user has noninteractive sudo.'
|
|
58
88
|
),
|
|
59
|
-
...Object.entries(
|
|
60
|
-
|
|
61
|
-
`package:${name}`,
|
|
62
|
-
audit.packages[name] === version,
|
|
63
|
-
'Exact declarative Ubuntu package version.'
|
|
64
|
-
)
|
|
89
|
+
...Object.entries(packagePins(ubuntuRelease(audit.release))).map(
|
|
90
|
+
([name, version]) => pinnedPackage(name, version, audit.packages[name])
|
|
65
91
|
),
|
|
66
|
-
|
|
67
|
-
'
|
|
68
|
-
|
|
69
|
-
|
|
92
|
+
pinnedPackage(
|
|
93
|
+
'tailscale',
|
|
94
|
+
policy.tailscale.version,
|
|
95
|
+
audit.packages['tailscale']
|
|
70
96
|
),
|
|
71
97
|
];
|
|
72
98
|
for (const name of policy.alwaysOnServices) {
|