@homeflare/alchemy 0.39.0 → 0.41.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 +10 -8
- package/dist/caddy/index.js.map +2 -2
- package/dist/ceph/auth-entity.d.ts +34 -0
- package/dist/ceph/auth-entity.d.ts.map +1 -0
- package/dist/ceph/ceph-argv-config.d.ts +10 -0
- package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
- package/dist/ceph/ceph-argv.d.ts +19 -0
- package/dist/ceph/ceph-argv.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-form.d.ts +54 -0
- package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-parse.d.ts +56 -0
- package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
- package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
- package/dist/ceph/ceph-config-options.d.ts +23 -0
- package/dist/ceph/ceph-config-options.d.ts.map +1 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
- package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
- package/dist/ceph/ceph-transport.d.ts +37 -0
- package/dist/ceph/ceph-transport.d.ts.map +1 -0
- package/dist/ceph/fake-ceph-dial.d.ts +35 -0
- package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
- package/dist/ceph/index.d.ts +13 -0
- package/dist/ceph/index.d.ts.map +1 -0
- package/dist/ceph/index.js +1610 -0
- package/dist/ceph/index.js.map +38 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/fake-runner.d.ts.map +1 -1
- package/dist/launchd/index.d.ts +1 -1
- package/dist/launchd/index.d.ts.map +1 -1
- package/dist/launchd/index.js +7 -1
- package/dist/launchd/index.js.map +5 -5
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +21 -0
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/linux/directory-lifecycle.d.ts.map +1 -1
- package/dist/linux/fake-linux-host.d.ts.map +1 -1
- package/dist/linux/index.d.ts +1 -1
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +16 -4
- package/dist/linux/index.js.map +7 -7
- package/dist/linux/ssh-runner.d.ts.map +1 -1
- package/dist/linux/sudo-runner.d.ts.map +1 -1
- package/dist/opnsense/alias-form.d.ts +14 -1
- package/dist/opnsense/alias-form.d.ts.map +1 -1
- package/dist/opnsense/alias.d.ts +1 -1
- package/dist/opnsense/alias.d.ts.map +1 -1
- package/dist/opnsense/group-form.d.ts +8 -2
- package/dist/opnsense/group-form.d.ts.map +1 -1
- package/dist/opnsense/group.d.ts +1 -1
- package/dist/opnsense/group.d.ts.map +1 -1
- package/dist/opnsense/index.js +15 -7
- package/dist/opnsense/index.js.map +7 -7
- package/dist/opnsense/wire.d.ts +42 -12
- package/dist/opnsense/wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +62 -39
- package/dist/proxmox/index.js.map +8 -7
- package/dist/proxmox/qemu-volume.d.ts +12 -2
- package/dist/proxmox/qemu-volume.d.ts.map +1 -1
- package/dist/proxmox/storage-form.d.ts +28 -3
- package/dist/proxmox/storage-form.d.ts.map +1 -1
- package/dist/proxmox/storage-plugin-options.d.ts +2 -0
- package/dist/proxmox/storage-plugin-options.d.ts.map +1 -0
- package/dist/proxmox/storage-wire.d.ts +28 -1
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts +6 -1
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js.map +2 -2
- package/dist/talos/credentials-write.d.ts +46 -0
- package/dist/talos/credentials-write.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +51 -9
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/fake-process.d.ts +56 -0
- package/dist/talos/fake-process.d.ts.map +1 -0
- package/dist/talos/index.d.ts +19 -4
- package/dist/talos/index.d.ts.map +1 -1
- package/dist/talos/index.js +357 -173
- package/dist/talos/index.js.map +16 -10
- package/dist/talos/kubeconfig-attrs.d.ts +4 -0
- package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
- package/dist/talos/kubeconfig.d.ts +41 -23
- package/dist/talos/kubeconfig.d.ts.map +1 -1
- package/dist/talos/machine-config-fixtures.d.ts +60 -0
- package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
- package/dist/talos/machine-config-poll.d.ts +43 -0
- package/dist/talos/machine-config-poll.d.ts.map +1 -0
- package/dist/talos/machine-config-read.d.ts +49 -0
- package/dist/talos/machine-config-read.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +18 -0
- package/dist/talos/resource.d.ts.map +1 -1
- package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
- package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +87 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -1
- package/dist/talos/talos-cluster-health.d.ts +43 -1
- package/dist/talos/talos-cluster-health.d.ts.map +1 -1
- package/dist/talos/talos-errors.d.ts +68 -0
- package/dist/talos/talos-errors.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +51 -15
- package/dist/talos/talos-machine-config.d.ts.map +1 -1
- package/dist/talos/talosctl.d.ts +7 -0
- package/dist/talos/talosctl.d.ts.map +1 -1
- package/dist/talos/values.d.ts +23 -3
- package/dist/talos/values.d.ts.map +1 -1
- package/dist/unifi/acl-rule-drift.d.ts +22 -0
- package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
- package/dist/unifi/acl-rule-form.d.ts +90 -0
- package/dist/unifi/acl-rule-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
- package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
- package/dist/unifi/acl-rule-ordering.d.ts +54 -0
- package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
- package/dist/unifi/acl-rule.d.ts +49 -0
- package/dist/unifi/acl-rule.d.ts.map +1 -0
- package/dist/unifi/dns-policy-drift.d.ts +21 -0
- package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
- package/dist/unifi/dns-policy-form.d.ts +97 -0
- package/dist/unifi/dns-policy-form.d.ts.map +1 -0
- package/dist/unifi/dns-policy.d.ts +55 -0
- package/dist/unifi/dns-policy.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-drift.d.ts +26 -0
- package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-form.d.ts +102 -0
- package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
- package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
- package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
- package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
- package/dist/unifi/firewall-policy.d.ts +57 -0
- package/dist/unifi/firewall-policy.d.ts.map +1 -0
- package/dist/unifi/index.d.ts +7 -0
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +588 -3
- package/dist/unifi/index.js.map +24 -4
- package/dist/unifi/paginate.d.ts +0 -12
- package/dist/unifi/paginate.d.ts.map +1 -1
- package/dist/unifi/providers.d.ts +1 -1
- package/dist/unifi/providers.d.ts.map +1 -1
- package/dist/unifi/scan-source.d.ts +11 -0
- package/dist/unifi/scan-source.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
- package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
- package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
- package/dist/unifi/traffic-matching-list.d.ts +51 -0
- package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
- package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
- package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
- package/dist/unifi/wifi-broadcast.d.ts +73 -0
- package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
- package/docs/distilled-interim.md +7 -0
- package/docs/launchd.md +5 -3
- package/docs/provider-standard.md +5 -1
- package/docs/talos-argocd-dogfood.md +160 -0
- package/docs/unifi-acl-rule.md +122 -0
- package/docs/unifi-dns-policy.md +75 -0
- package/docs/unifi-firewall-policy.md +109 -0
- package/docs/unifi-traffic-matching-list.md +82 -0
- package/docs/unifi-wifi.md +120 -0
- package/docs/unifi.md +10 -5
- package/package.json +7 -3
- package/src/ceph/auth-entity.ts +74 -0
- package/src/ceph/ceph-argv-config.ts +78 -0
- package/src/ceph/ceph-argv.test.ts +226 -0
- package/src/ceph/ceph-argv.ts +189 -0
- package/src/ceph/ceph-auth-form.test.ts +103 -0
- package/src/ceph/ceph-auth-form.ts +107 -0
- package/src/ceph/ceph-auth-parse.test.ts +155 -0
- package/src/ceph/ceph-auth-parse.ts +121 -0
- package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
- package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
- package/src/ceph/ceph-auth-reconcile.ts +119 -0
- package/src/ceph/ceph-config-options.test.ts +44 -0
- package/src/ceph/ceph-config-options.ts +36 -0
- package/src/ceph/ceph-key-leak-assert.ts +35 -0
- package/src/ceph/ceph-no-output-logging.test.ts +52 -0
- package/src/ceph/ceph-transport.test.ts +125 -0
- package/src/ceph/ceph-transport.ts +117 -0
- package/src/ceph/fake-ceph-dial.ts +55 -0
- package/src/ceph/index.ts +12 -0
- package/src/index.ts +1 -0
- package/src/launchd/fake-runner.ts +2 -0
- package/src/launchd/index.ts +8 -1
- package/src/launchd/local-runner.ts +16 -1
- package/src/launchd/runner.ts +22 -0
- package/src/launchd/sudo-runner.ts +3 -0
- package/src/linux/directory-lifecycle.ts +18 -3
- package/src/linux/directory-sudo.test.ts +59 -0
- package/src/linux/directory.test.ts +34 -8
- package/src/linux/fake-linux-host.ts +2 -0
- package/src/linux/index.ts +1 -0
- package/src/linux/ssh-runner.ts +4 -0
- package/src/linux/sudo-runner.ts +3 -0
- package/src/opnsense/alias-form.test.ts +24 -12
- package/src/opnsense/alias-form.ts +20 -7
- package/src/opnsense/alias.test.ts +13 -6
- package/src/opnsense/alias.ts +16 -10
- package/src/opnsense/group-form.test.ts +12 -8
- package/src/opnsense/group-form.ts +13 -4
- package/src/opnsense/group.test.ts +18 -7
- package/src/opnsense/group.ts +10 -3
- package/src/opnsense/wire.ts +57 -12
- package/src/proxmox/qemu-volume.test.ts +153 -4
- package/src/proxmox/qemu-volume.ts +71 -14
- package/src/proxmox/storage-form.ts +48 -8
- package/src/proxmox/storage-plugin-options.ts +45 -0
- package/src/proxmox/storage-update.test.ts +183 -0
- package/src/proxmox/storage-wire.ts +54 -7
- package/src/proxmox/storage.ts +18 -6
- package/src/release/index.ts +1 -0
- package/src/talos/credentials-write.test.ts +81 -0
- package/src/talos/credentials-write.ts +95 -0
- package/src/talos/credentials.test.ts +157 -0
- package/src/talos/credentials.ts +160 -90
- package/src/talos/fake-process.ts +94 -0
- package/src/talos/index.ts +39 -4
- package/src/talos/kubeconfig-attrs.ts +55 -0
- package/src/talos/kubeconfig.test.ts +256 -0
- package/src/talos/kubeconfig.ts +170 -90
- package/src/talos/machine-config-fixtures.ts +86 -0
- package/src/talos/machine-config-poll.test.ts +57 -0
- package/src/talos/machine-config-poll.ts +82 -0
- package/src/talos/machine-config-read.ts +118 -0
- package/src/talos/resource.ts +19 -0
- package/src/talos/secrets-gate.test.ts +63 -0
- package/src/talos/talos-bootstrap-etcd.ts +71 -0
- package/src/talos/talos-bootstrap.test.ts +218 -0
- package/src/talos/talos-bootstrap.ts +151 -45
- package/src/talos/talos-cluster-health.test.ts +143 -0
- package/src/talos/talos-cluster-health.ts +91 -28
- package/src/talos/talos-errors.ts +85 -0
- package/src/talos/talos-machine-config.test.ts +189 -0
- package/src/talos/talos-machine-config.ts +102 -88
- package/src/talos/talosctl.ts +8 -0
- package/src/talos/values.test.ts +34 -6
- package/src/talos/values.ts +41 -7
- package/src/unifi/acl-rule-drift.ts +57 -0
- package/src/unifi/acl-rule-form.ts +135 -0
- package/src/unifi/acl-rule-ordering-form.ts +79 -0
- package/src/unifi/acl-rule-ordering.test.ts +141 -0
- package/src/unifi/acl-rule-ordering.ts +70 -0
- package/src/unifi/acl-rule.test.ts +183 -0
- package/src/unifi/acl-rule.ts +61 -0
- package/src/unifi/discriminated-filter-decode.test.ts +134 -0
- package/src/unifi/dns-policy-drift.ts +48 -0
- package/src/unifi/dns-policy-form.ts +142 -0
- package/src/unifi/dns-policy.test.ts +154 -0
- package/src/unifi/dns-policy.ts +67 -0
- package/src/unifi/firewall-policy-drift.test.ts +151 -0
- package/src/unifi/firewall-policy-drift.ts +55 -0
- package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
- package/src/unifi/firewall-policy-form.ts +146 -0
- package/src/unifi/firewall-policy-ordering-form.ts +111 -0
- package/src/unifi/firewall-policy-ordering.test.ts +185 -0
- package/src/unifi/firewall-policy-ordering.ts +87 -0
- package/src/unifi/firewall-policy.test.ts +183 -0
- package/src/unifi/firewall-policy.ts +73 -0
- package/src/unifi/index.ts +63 -0
- package/src/unifi/paginate.test.ts +24 -0
- package/src/unifi/paginate.ts +15 -3
- package/src/unifi/providers.ts +19 -1
- package/src/unifi/scan-source.ts +64 -0
- package/src/unifi/traffic-matching-list-drift.ts +40 -0
- package/src/unifi/traffic-matching-list-form.ts +77 -0
- package/src/unifi/traffic-matching-list.test.ts +202 -0
- package/src/unifi/traffic-matching-list.ts +78 -0
- package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
- package/src/unifi/wifi-broadcast-drift.ts +69 -0
- package/src/unifi/wifi-broadcast-form.ts +211 -0
- package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
- package/src/unifi/wifi-broadcast.test.ts +155 -0
- package/src/unifi/wifi-broadcast.ts +90 -0
- package/src/unifi/write-op-reference.test.ts +8 -54
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Talos.MachineConfig` acceptance tests from docs/plans/2026-09-26-talos-secrets-flow.md — fully
|
|
3
|
+
* offline, fake `bao`/`talosctl` (fake-process.ts via machine-config-fixtures.ts), no real
|
|
4
|
+
* process ever spawns. Bounded-poll-per-mode tests live in machine-config-poll.test.ts.
|
|
5
|
+
*/
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { existsSync, statSync } from 'node:fs';
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
10
|
+
import {
|
|
11
|
+
CONFIG_TEXT,
|
|
12
|
+
PIN,
|
|
13
|
+
dispatcher,
|
|
14
|
+
hasTag,
|
|
15
|
+
props,
|
|
16
|
+
run,
|
|
17
|
+
wrapperFor,
|
|
18
|
+
} from './machine-config-fixtures.ts';
|
|
19
|
+
import type { FakeCall } from './fake-process.ts';
|
|
20
|
+
import { diffMachineConfig, readMachineConfig } from './machine-config-read.ts';
|
|
21
|
+
import { type MachineConfigAttributes, reconcileMachineConfig } from './talos-machine-config.ts';
|
|
22
|
+
import { extractMachineConfigSpec } from './values.ts';
|
|
23
|
+
|
|
24
|
+
describe('reconcileMachineConfig — digest gate', () => {
|
|
25
|
+
it('fails closed on a mismatch and never spawns talosctl', async () => {
|
|
26
|
+
const calls: FakeCall[] = [];
|
|
27
|
+
await assert.rejects(
|
|
28
|
+
run(
|
|
29
|
+
reconcileMachineConfig(props(), undefined),
|
|
30
|
+
dispatcher({ configText: 'not the pinned text' }),
|
|
31
|
+
calls,
|
|
32
|
+
),
|
|
33
|
+
hasTag('TalosConfigDigestMismatch'),
|
|
34
|
+
);
|
|
35
|
+
assert.ok(
|
|
36
|
+
calls.every((c) => c.command !== 'talosctl'),
|
|
37
|
+
'no talosctl call on a digest mismatch',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('reconcileMachineConfig — apply argv and --insecure', () => {
|
|
43
|
+
it('CREATE (output undefined) passes --insecure; argv never carries --dry-run', async () => {
|
|
44
|
+
const calls: FakeCall[] = [];
|
|
45
|
+
await run(reconcileMachineConfig(props(), undefined), dispatcher({}), calls);
|
|
46
|
+
const apply = calls.find((c) => c.command === 'talosctl' && c.args[0] === 'apply-config');
|
|
47
|
+
assert.ok(apply);
|
|
48
|
+
assert.ok(apply.args.includes('--insecure'));
|
|
49
|
+
assert.ok(!calls.some((c) => c.args.includes('--dry-run')));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('UPDATE (output defined) never passes --insecure; base argv is exactly apply-config/--file/--mode', async () => {
|
|
53
|
+
const calls: FakeCall[] = [];
|
|
54
|
+
const output: MachineConfigAttributes = {
|
|
55
|
+
configDigest: PIN,
|
|
56
|
+
converged: 'read-back',
|
|
57
|
+
mode: 'auto',
|
|
58
|
+
node: props().node,
|
|
59
|
+
};
|
|
60
|
+
await run(reconcileMachineConfig(props(), output), dispatcher({}), calls);
|
|
61
|
+
const apply = calls.find((c) => c.command === 'talosctl' && c.args[0] === 'apply-config');
|
|
62
|
+
assert.ok(apply);
|
|
63
|
+
assert.ok(!apply.args.includes('--insecure'));
|
|
64
|
+
const filePath = apply.args[apply.args.indexOf('--file') + 1];
|
|
65
|
+
assert.deepEqual(apply.args.slice(0, 5), [
|
|
66
|
+
'apply-config',
|
|
67
|
+
'--file',
|
|
68
|
+
filePath,
|
|
69
|
+
'--mode',
|
|
70
|
+
'auto',
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('the machine-config temp file exists (0600) exactly when talosctl spawns, and is gone after', async () => {
|
|
75
|
+
let seenAtSpawn: { existed: boolean; mode: number } | undefined;
|
|
76
|
+
let path = '';
|
|
77
|
+
const handler = (call: FakeCall) => {
|
|
78
|
+
if (call.command === 'talosctl' && call.args[0] === 'apply-config') {
|
|
79
|
+
path = call.args[call.args.indexOf('--file') + 1] ?? '';
|
|
80
|
+
seenAtSpawn = { existed: existsSync(path), mode: statSync(path).mode & 0o777 };
|
|
81
|
+
}
|
|
82
|
+
return dispatcher({})(call);
|
|
83
|
+
};
|
|
84
|
+
await run(reconcileMachineConfig(props(), undefined), handler);
|
|
85
|
+
assert.ok(seenAtSpawn?.existed, 'temp file must exist the instant talosctl spawns');
|
|
86
|
+
assert.equal(seenAtSpawn?.mode, 0o600);
|
|
87
|
+
assert.equal(existsSync(path), false, 'temp file must be gone once reconcile returns');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('readMachineConfig — extracted spec only, no swallowing', () => {
|
|
92
|
+
it('converges even when wrapper metadata differs, since only spec is hashed', async () => {
|
|
93
|
+
const result = await run(
|
|
94
|
+
readMachineConfig(props()),
|
|
95
|
+
dispatcher({ liveWrapper: () => ({ stdout: wrapperFor(CONFIG_TEXT, '999') }) }),
|
|
96
|
+
);
|
|
97
|
+
assert.ok(result);
|
|
98
|
+
assert.equal(result.converged, 'read-back');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('reports false (not an error), branded Unowned, when a successful read genuinely differs', async () => {
|
|
102
|
+
const result = await run(
|
|
103
|
+
readMachineConfig(props()),
|
|
104
|
+
dispatcher({
|
|
105
|
+
liveWrapper: () => ({ stdout: wrapperFor('machine:\n type: controlplane\n') }),
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
assert.ok(result);
|
|
109
|
+
assert.equal(result.converged, false);
|
|
110
|
+
// ⛔ fix-first #1c — a digest mismatch on the engine's cold-start adoption probe must never
|
|
111
|
+
// read as plain (silently-owned) attrs, or a mistyped node pointing at a sibling row's
|
|
112
|
+
// configured node gets `apply-config` run onto it with no `adopt()` in sight.
|
|
113
|
+
assert.equal(Unowned.is(result), true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('propagates the AUTHENTICATED failure when the maintenance probe also fails, never converged: false', async () => {
|
|
117
|
+
await assert.rejects(
|
|
118
|
+
run(
|
|
119
|
+
readMachineConfig(props()),
|
|
120
|
+
dispatcher({ liveWrapper: () => ({ exitCode: 1, stderr: 'unreachable' }) }),
|
|
121
|
+
),
|
|
122
|
+
(error: unknown) => error instanceof Error && error.message.includes('unreachable'),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('readMachineConfig — cold-start adoption answer (fix-first #1)', () => {
|
|
128
|
+
it('returns undefined (not created) when the authenticated read fails but the node answers insecurely', async () => {
|
|
129
|
+
const result = await run(
|
|
130
|
+
readMachineConfig(props()),
|
|
131
|
+
dispatcher({
|
|
132
|
+
liveWrapper: () => ({
|
|
133
|
+
exitCode: 1,
|
|
134
|
+
stderr: 'x509: certificate signed by unknown authority',
|
|
135
|
+
}),
|
|
136
|
+
maintenanceReachable: true,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
assert.equal(result, undefined);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('diffMachineConfig', () => {
|
|
144
|
+
it('plans noop when the live digest matches, update when it does not', async () => {
|
|
145
|
+
const output: MachineConfigAttributes = {
|
|
146
|
+
configDigest: PIN,
|
|
147
|
+
converged: 'read-back',
|
|
148
|
+
mode: 'auto',
|
|
149
|
+
node: props().node,
|
|
150
|
+
};
|
|
151
|
+
const noop = await run(diffMachineConfig(props(), output), dispatcher({}));
|
|
152
|
+
assert.equal(noop?.action, 'noop');
|
|
153
|
+
const update = await run(
|
|
154
|
+
diffMachineConfig(props(), output),
|
|
155
|
+
dispatcher({
|
|
156
|
+
liveWrapper: () => ({ stdout: wrapperFor('machine:\n type: controlplane\n') }),
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
assert.equal(update?.action, 'update');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('plans update when the live resource is gone (node reverted to maintenance mode)', async () => {
|
|
163
|
+
const output: MachineConfigAttributes = {
|
|
164
|
+
configDigest: PIN,
|
|
165
|
+
converged: 'read-back',
|
|
166
|
+
mode: 'auto',
|
|
167
|
+
node: props().node,
|
|
168
|
+
};
|
|
169
|
+
const result = await run(
|
|
170
|
+
diffMachineConfig(props(), output),
|
|
171
|
+
dispatcher({
|
|
172
|
+
liveWrapper: () => ({
|
|
173
|
+
exitCode: 1,
|
|
174
|
+
stderr: 'x509: certificate signed by unknown authority',
|
|
175
|
+
}),
|
|
176
|
+
maintenanceReachable: true,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
assert.equal(result?.action, 'update');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Sanity: the wrapper fixture itself round-trips through extractMachineConfigSpec (belt & suspenders
|
|
184
|
+
// with values.test.ts, which owns the exhaustive shape tests).
|
|
185
|
+
describe('fixture sanity', () => {
|
|
186
|
+
it('wrapperFor embeds exactly CONFIG_TEXT under spec', () => {
|
|
187
|
+
assert.equal(extractMachineConfigSpec(wrapperFor(CONFIG_TEXT)), CONFIG_TEXT);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -2,38 +2,51 @@
|
|
|
2
2
|
* `Talos.MachineConfig` — apply one machine configuration document to one node.
|
|
3
3
|
*
|
|
4
4
|
* ★ REASONED FROM talosctl apply-config (Talos v1.13 CLI reference): `-f/--file`, `-n/--nodes`,
|
|
5
|
-
* `-m/--mode`, `-i/--insecure` for maintenance mode
|
|
5
|
+
* `-m/--mode`, `-i/--insecure` for maintenance mode.
|
|
6
6
|
*
|
|
7
|
-
* ⛔ THE CONFIG BODY IS NEVER AN ATTRIBUTE
|
|
8
|
-
*
|
|
7
|
+
* ⛔ THE CONFIG BODY IS NEVER AN ATTRIBUTE, A PROP, OR ARGV — ONLY ITS DIGEST. K-A3
|
|
8
|
+
* (docs/plans/2026-09-26-talos-secrets-flow.md, option O-A) moved the source of truth from a
|
|
9
|
+
* repo file (the shipped `configFile` + `STACK_DIR`, gone with this rewrite) to OpenBao: props
|
|
10
|
+
* carry a `configKey` (a vault path) and a `configDigest` PINNED IN GIT, never the bytes. The
|
|
11
|
+
* source is write-only from Alchemy's point of view — content is read once per reconcile,
|
|
12
|
+
* verified against the pin, written to a session-temp file, and never returned from any
|
|
13
|
+
* handler.
|
|
14
|
+
* ⛔ NEVER `--dry-run`. Talos's dry-run prints a full, unredacted old/new config diff — with an
|
|
15
|
+
* empty old config (the first apply) that diff IS the whole config, CA private key and
|
|
16
|
+
* bootstrap token included (REASONED, `internal/app/machined/.../v1alpha1_server.go:258-272` +
|
|
17
|
+
* `configdiff.go`; docs/plans/2026-09-26-talos-stack-first-boot.md). This file never builds
|
|
18
|
+
* that flag.
|
|
19
|
+
* ⛔ `insecure` IS NOT A PROP. A fixed value fails in both directions — `true` breaks every later
|
|
20
|
+
* update, `false` breaks the very first apply against a node with no talosconfig auth yet — so
|
|
21
|
+
* CREATE (`output === undefined`) applies `--insecure` and UPDATE never does (resource.ts).
|
|
9
22
|
*/
|
|
10
23
|
import { Resource } from 'alchemy';
|
|
11
|
-
import { isResolved } from 'alchemy/Diff';
|
|
12
24
|
import type { Input } from 'alchemy/Input';
|
|
13
25
|
import * as Provider from 'alchemy/Provider';
|
|
14
26
|
import * as Effect from 'effect/Effect';
|
|
15
|
-
import { mintTalosconfig } from './credentials.ts';
|
|
16
|
-
import
|
|
27
|
+
import { mintKvTempFile, mintTalosconfig, readKvValue } from './credentials.ts';
|
|
28
|
+
import { diffMachineConfig, liveSpecDigest, readMachineConfig } from './machine-config-read.ts';
|
|
29
|
+
import { type PollPolicy, confirmConverged } from './machine-config-poll.ts';
|
|
30
|
+
import type { ApplyMode, TalosRequirements, WithTarget } from './resource.ts';
|
|
31
|
+
import { TalosConfigDigestMismatch } from './talos-errors.ts';
|
|
17
32
|
import { talosctl } from './talosctl.ts';
|
|
18
|
-
import { configDigest
|
|
33
|
+
import { configDigest } from './values.ts';
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export type ApplyMode = 'auto' | 'no-reboot' | 'reboot' | 'staged' | 'try';
|
|
35
|
+
export type { ApplyMode } from './resource.ts';
|
|
23
36
|
|
|
24
37
|
export interface MachineConfigProps extends WithTarget {
|
|
25
38
|
/** Node IP or hostname — the `-n` target. */
|
|
26
39
|
node: string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
/** talosctl `-m` mode. Default `auto` per published CLI. */
|
|
30
|
-
mode?: ApplyMode;
|
|
40
|
+
/** OpenBao KV path under `target.mount` holding this node's rendered config, e.g. `nodes/10001`. */
|
|
41
|
+
configKey: string;
|
|
31
42
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
43
|
+
* sha256(canonicalText(content)) of the seeded KV value (values.ts `configDigest`), pinned in
|
|
44
|
+
* git by the operator. ⛔ Verified against the live KV content before ANY talosctl spawn — a
|
|
45
|
+
* mismatch fails closed (`TalosConfigDigestMismatch`), nothing is applied.
|
|
35
46
|
*/
|
|
36
|
-
|
|
47
|
+
configDigest: string;
|
|
48
|
+
/** talosctl `-m` mode. Default `auto` per published CLI. */
|
|
49
|
+
mode?: ApplyMode;
|
|
37
50
|
/** Ordering edge — e.g. wait for Proxmox.Vm. */
|
|
38
51
|
after?: readonly unknown[];
|
|
39
52
|
}
|
|
@@ -42,8 +55,13 @@ export interface MachineConfigAttributes {
|
|
|
42
55
|
node: string;
|
|
43
56
|
configDigest: string;
|
|
44
57
|
mode: ApplyMode;
|
|
45
|
-
/**
|
|
46
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Which convergence claim was made. `'read-back'`/`'accepted'` only come from `reconcile` (see
|
|
60
|
+
* machine-config-poll.ts); `false` only comes from `read`, meaning a live digest was
|
|
61
|
+
* successfully fetched and genuinely differs — never a stand-in for a failed read, which
|
|
62
|
+
* propagates as its own error instead (docs/plans/2026-09-26-talos-stack-first-boot.md).
|
|
63
|
+
*/
|
|
64
|
+
converged: 'read-back' | 'accepted' | false;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
export interface TalosMachineConfig extends Resource<
|
|
@@ -56,77 +74,67 @@ export interface TalosMachineConfig extends Resource<
|
|
|
56
74
|
|
|
57
75
|
export const TalosMachineConfig = Resource<TalosMachineConfig>('Talos.MachineConfig');
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The read path (`readMachineConfig`, `diffMachineConfig`, `liveSpecDigest`) lives in
|
|
79
|
+
* machine-config-read.ts — see that file's own header for the ownership-aware three-way answer
|
|
80
|
+
* `read` now gives (fix-first #1/#2, PR 307 red team).
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** `pollPolicies` is a test seam only — see machine-config-poll.ts's `confirmConverged`. */
|
|
84
|
+
export const reconcileMachineConfig = (
|
|
85
|
+
props: MachineConfigProps,
|
|
86
|
+
output: MachineConfigAttributes | undefined,
|
|
87
|
+
pollPolicies?: { readonly immediate: PollPolicy; readonly rebootish: PollPolicy },
|
|
88
|
+
) =>
|
|
89
|
+
Effect.scoped(
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const mode = props.mode ?? 'auto';
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
// ⛔ VERIFY BEFORE ANY talosctl SPAWN. A mismatch fails closed — see talos-errors.ts.
|
|
94
|
+
const raw = yield* readKvValue(props.target.mount, props.configKey, ['config']);
|
|
95
|
+
const gotDigest = configDigest(raw);
|
|
96
|
+
if (gotDigest !== props.configDigest) {
|
|
97
|
+
return yield* Effect.fail(
|
|
98
|
+
new TalosConfigDigestMismatch({
|
|
99
|
+
expected: props.configDigest,
|
|
100
|
+
got: gotDigest,
|
|
101
|
+
key: props.configKey,
|
|
102
|
+
mount: props.target.mount,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
78
106
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const wanted = yield* loadDigest(props);
|
|
82
|
-
const credential = yield* mintTalosconfig(props.target);
|
|
83
|
-
const live = yield* liveDigest(props, credential.talosconfigPath);
|
|
84
|
-
return {
|
|
85
|
-
configDigest: wanted,
|
|
86
|
-
converged: live === wanted,
|
|
87
|
-
mode: props.mode ?? 'auto',
|
|
88
|
-
node: props.node,
|
|
89
|
-
};
|
|
90
|
-
});
|
|
107
|
+
const credential = yield* mintTalosconfig(props.target);
|
|
108
|
+
const { path } = yield* mintKvTempFile(raw, `${props.target.cluster}-${props.node}-config`);
|
|
91
109
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
const isCreate = output === undefined;
|
|
111
|
+
const args = [
|
|
112
|
+
'apply-config',
|
|
113
|
+
'--file',
|
|
114
|
+
path,
|
|
115
|
+
'--mode',
|
|
116
|
+
mode,
|
|
117
|
+
...(isCreate ? ['--insecure'] : []),
|
|
118
|
+
];
|
|
119
|
+
yield* talosctl(args, { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
99
120
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const args = [
|
|
106
|
-
'apply-config',
|
|
107
|
-
'--file',
|
|
108
|
-
path,
|
|
109
|
-
'--mode',
|
|
110
|
-
props.mode ?? 'auto',
|
|
111
|
-
...(props.insecure === true ? ['--insecure'] : []),
|
|
112
|
-
];
|
|
113
|
-
yield* talosctl(args, { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
114
|
-
const live = yield* liveDigest(props, credential.talosconfigPath);
|
|
115
|
-
if (live !== wanted) {
|
|
116
|
-
return yield* Effect.die(
|
|
117
|
-
new Error(
|
|
118
|
-
`${props.node}: apply-config returned no error but machineconfig digest still differs. ` +
|
|
119
|
-
'Read back rather than trusting the exit code alone.',
|
|
121
|
+
const converged = yield* confirmConverged(
|
|
122
|
+
props.node,
|
|
123
|
+
mode,
|
|
124
|
+
liveSpecDigest(props, credential.talosconfigPath).pipe(
|
|
125
|
+
Effect.map((live) => live === props.configDigest),
|
|
120
126
|
),
|
|
127
|
+
pollPolicies,
|
|
121
128
|
);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
configDigest: props.configDigest,
|
|
132
|
+
converged,
|
|
133
|
+
mode,
|
|
134
|
+
node: props.node,
|
|
135
|
+
} satisfies MachineConfigAttributes;
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
130
138
|
|
|
131
139
|
const handlers = {
|
|
132
140
|
delete: () => Effect.void,
|
|
@@ -136,10 +144,16 @@ const handlers = {
|
|
|
136
144
|
}: {
|
|
137
145
|
news: Input<MachineConfigProps>;
|
|
138
146
|
output: MachineConfigAttributes | undefined;
|
|
139
|
-
}) =>
|
|
147
|
+
}) => diffMachineConfig(news, output),
|
|
140
148
|
list: () => Effect.succeed([]),
|
|
141
|
-
read: ({ olds }: { olds: MachineConfigProps }) =>
|
|
142
|
-
reconcile: ({
|
|
149
|
+
read: ({ olds }: { olds: MachineConfigProps }) => readMachineConfig(olds),
|
|
150
|
+
reconcile: ({
|
|
151
|
+
news,
|
|
152
|
+
output,
|
|
153
|
+
}: {
|
|
154
|
+
news: MachineConfigProps;
|
|
155
|
+
output: MachineConfigAttributes | undefined;
|
|
156
|
+
}) => reconcileMachineConfig(news, output),
|
|
143
157
|
};
|
|
144
158
|
|
|
145
159
|
export const TalosMachineConfigProvider = () =>
|
package/src/talos/talosctl.ts
CHANGED
|
@@ -30,6 +30,13 @@ export type TalosRunOptions = {
|
|
|
30
30
|
readonly talosconfigPath: string;
|
|
31
31
|
readonly nodes?: readonly string[];
|
|
32
32
|
readonly endpoints?: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* `-i/--insecure` — a maintenance-mode node has no established PKI yet. Used by the CREATE apply
|
|
35
|
+
* (resource.ts's own header) and by talos-machine-config.ts's maintenance-mode probe (fix-first
|
|
36
|
+
* #1, PR 307 red team): `--talosconfig` is still passed alongside it, matching the CREATE path,
|
|
37
|
+
* since insecure mode ignores the file's certs rather than requiring their absence.
|
|
38
|
+
*/
|
|
39
|
+
readonly insecure?: boolean;
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
/** Run `talosctl <args…>`. Returns trimmed stdout, or '' when empty. */
|
|
@@ -42,6 +49,7 @@ export const talosctl = (args: readonly string[], options: TalosRunOptions) =>
|
|
|
42
49
|
options.talosconfigPath,
|
|
43
50
|
...(options.nodes === undefined ? [] : ['--nodes', options.nodes.join(',')]),
|
|
44
51
|
...(options.endpoints === undefined ? [] : ['--endpoints', options.endpoints.join(',')]),
|
|
52
|
+
...(options.insecure === true ? ['--insecure'] : []),
|
|
45
53
|
];
|
|
46
54
|
const result = yield* ChildProcess.make('talosctl', argv, {
|
|
47
55
|
detached: false,
|
package/src/talos/values.test.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import assert from 'node:assert/strict';
|
|
14
14
|
import { describe, it } from 'node:test';
|
|
15
|
-
import { configDigest,
|
|
15
|
+
import { configDigest, extractMachineConfigSpec, kubeconfigMetadata, sha256 } from './values.ts';
|
|
16
16
|
|
|
17
17
|
/** Plain words, base64-encoded here rather than in the source. See the ⛔ above. */
|
|
18
18
|
const b64 = (words: string) => Buffer.from(words).toString('base64');
|
|
@@ -69,12 +69,40 @@ describe('kubeconfigMetadata', () => {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
describe('
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
describe('extractMachineConfigSpec', () => {
|
|
73
|
+
/** Shape REASONED from talosctl's `get machineconfig -o yaml` — see values.ts's own header. */
|
|
74
|
+
const wrapper = (version: string) =>
|
|
75
|
+
`node: 192.0.2.10\nmetadata:\n version: "${version}"\nspec: "machine:\\n type: worker\\n"\n`;
|
|
76
|
+
|
|
77
|
+
it('hashes only the spec text, not the wrapper metadata', () => {
|
|
78
|
+
const specA = extractMachineConfigSpec(wrapper('7'));
|
|
79
|
+
const specB = extractMachineConfigSpec(wrapper('9')); // metadata differs, spec does not
|
|
80
|
+
assert.ok(specA !== undefined && specB !== undefined);
|
|
81
|
+
assert.equal(specA, specB);
|
|
82
|
+
assert.equal(configDigest(specA), configDigest(specB));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('unwraps a single-element array response the same way', () => {
|
|
86
|
+
const asArrayYaml =
|
|
87
|
+
'- node: 192.0.2.10\n metadata:\n version: "7"\n spec: "machine:\\n type: worker\\n"\n';
|
|
88
|
+
const asDoc = extractMachineConfigSpec(wrapper('7'));
|
|
89
|
+
const asArray = extractMachineConfigSpec(asArrayYaml);
|
|
90
|
+
assert.ok(asDoc !== undefined && asArray !== undefined);
|
|
91
|
+
assert.equal(asDoc, asArray);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns undefined for unparsable YAML or a missing/non-string spec', () => {
|
|
95
|
+
assert.equal(extractMachineConfigSpec(':::not yaml:::'), undefined);
|
|
96
|
+
assert.equal(extractMachineConfigSpec('node: 192.0.2.10\n'), undefined);
|
|
97
|
+
assert.equal(extractMachineConfigSpec('spec:\n nested: true\n'), undefined);
|
|
75
98
|
});
|
|
76
99
|
|
|
77
|
-
it('
|
|
78
|
-
|
|
100
|
+
it('refuses more than one document rather than guessing which one (fix-first #2)', () => {
|
|
101
|
+
// Shape of an unfiltered `get machineconfig`: `persistent` then `v1alpha1`, sorted by id — a
|
|
102
|
+
// bare `doc[0]` here would silently pick `persistent`, the shipped bug this guard replaces.
|
|
103
|
+
const twoDocs =
|
|
104
|
+
'- node: 192.0.2.10\n metadata:\n id: persistent\n spec: "not the applied config"\n' +
|
|
105
|
+
'- node: 192.0.2.10\n metadata:\n id: v1alpha1\n spec: "machine:\\n type: worker\\n"\n';
|
|
106
|
+
assert.equal(extractMachineConfigSpec(twoDocs), undefined);
|
|
79
107
|
});
|
|
80
108
|
});
|
package/src/talos/values.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Coercions and digests for Talos resources — every function here exists because a trap does.
|
|
3
3
|
*
|
|
4
|
-
* ⚠️ NOTHING IN THIS FILE TOUCHES A LIVE CLUSTER. Digests are computed from
|
|
5
|
-
*
|
|
4
|
+
* ⚠️ NOTHING IN THIS FILE TOUCHES A LIVE CLUSTER OR OpenBao. Digests are computed from KV content
|
|
5
|
+
* or talosctl output already fetched by the caller, and kubeconfig YAML already on disk at
|
|
6
|
+
* reconcile time; tests use fixtures only.
|
|
6
7
|
*/
|
|
7
8
|
import { createHash } from 'node:crypto';
|
|
8
9
|
|
|
@@ -13,9 +14,46 @@ export const canonicalText = (text: string) => text.trimEnd();
|
|
|
13
14
|
export const sha256 = (text: string) =>
|
|
14
15
|
createHash('sha256').update(canonicalText(text)).digest('hex');
|
|
15
16
|
|
|
16
|
-
/** Digest of a machine config
|
|
17
|
+
/** Digest of a machine config's canonical text. ⛔ Never persist the text itself. */
|
|
17
18
|
export const configDigest = (text: string) => sha256(text);
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Extract the raw config text under `spec` from `talosctl get machineconfig -o yaml`'s wrapper.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ THE WHOLE WRAPPER CAN NEVER MATCH THE PINNED DIGEST, AND HASHING IT WAS THE SHIPPED BUG.
|
|
24
|
+
* REASONED from talosctl source (`cmd/talosctl/.../output/yaml.go:63`,
|
|
25
|
+
* `resources/config/machine_config.go:40-50`): the resource envelope nests a `node:` line and
|
|
26
|
+
* resource metadata (version, timestamps) around the config, which is itself a raw YAML STRING
|
|
27
|
+
* under `spec`. Metadata changes on every observation, so `configDigest(wrapper)` never equals
|
|
28
|
+
* `configDigest(seededConfigText)` even when nothing changed — every reconcile would die, every
|
|
29
|
+
* diff would plan `update`, verify would never go quiet (all three MEASURED against the shipped
|
|
30
|
+
* code's shape, docs/plans/2026-09-26-talos-stack-first-boot.md).
|
|
31
|
+
* ⚠️ Returns `undefined` for any shape without a string `spec`, so a caller cannot mistake
|
|
32
|
+
* "unparsable wrapper" for "empty config" — those must fail differently (the caller's job).
|
|
33
|
+
* ⛔ MORE THAN ONE DOCUMENT IS REFUSED, NEVER `doc[0]` (fix-first #2, PR 307 red team). An unfiltered
|
|
34
|
+
* `get machineconfig` lists BOTH the `persistent` and `v1alpha1` resources, sorted by id
|
|
35
|
+
* (REASONED, cosi-project/runtime `inmem/collection.go:122-124`), so a bare `doc[0]` is always
|
|
36
|
+
* `persistent` — never the config this package applies. The caller (machine-config-read.ts) now
|
|
37
|
+
* always requests the single named resource (`get machineconfig v1alpha1`); a second document
|
|
38
|
+
* showing up here means that request shape changed without this guard changing with it, and
|
|
39
|
+
* silently picking one would repeat the exact shipped bug.
|
|
40
|
+
*/
|
|
41
|
+
export const extractMachineConfigSpec = (wrapperYaml: string): string | undefined => {
|
|
42
|
+
let doc: unknown;
|
|
43
|
+
try {
|
|
44
|
+
doc = Bun.YAML.parse(wrapperYaml);
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(doc)) {
|
|
49
|
+
if (doc.length !== 1) return undefined;
|
|
50
|
+
doc = doc[0];
|
|
51
|
+
}
|
|
52
|
+
if (doc === null || typeof doc !== 'object') return undefined;
|
|
53
|
+
const spec = (doc as { spec?: unknown }).spec;
|
|
54
|
+
return typeof spec === 'string' ? spec : undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
19
57
|
type KubeconfigDoc = {
|
|
20
58
|
clusters?: {
|
|
21
59
|
cluster?: { server?: string; 'certificate-authority-data'?: string };
|
|
@@ -64,7 +102,3 @@ export const kubeconfigMetadata = (
|
|
|
64
102
|
endpoint,
|
|
65
103
|
};
|
|
66
104
|
};
|
|
67
|
-
|
|
68
|
-
/** Resolve a config file path relative to the declaring stack file. */
|
|
69
|
-
export const resolveConfigPath = (stackDir: string, configFile: string) =>
|
|
70
|
-
configFile.startsWith('/') ? configFile : `${stackDir}/${configFile}`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Unifi.AclRule`'s `matches` AND `driftOf` (B6) — both built on the ONE field list below, the
|
|
3
|
+
* same `fieldDrift`-is-the-only-comparison shape `network-drift.ts`'s header explains (MEDIUM-4,
|
|
4
|
+
* red team, 2026-09-26): `matches(attrs, props) === (driftOf(live, props).length === 0)` is true
|
|
5
|
+
* BY CONSTRUCTION.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ `stripNullish: true` ON EVERY FIELD BY DEFAULT (`drift.ts`'s `defaultEqual`) — same "UniFi's
|
|
8
|
+
* JSON answers `null`, a declaration omits" tolerance `network-drift.ts` documents.
|
|
9
|
+
* ⛔ `enforcingDeviceFilter`/`protocolFilter` RUN THROUGH `acl-rule-form.ts`'s normalizers on BOTH
|
|
10
|
+
* sides before comparing — never applied to only `live` or only `declared` (that specific gap is
|
|
11
|
+
* what MEDIUM-4's own mutant found in `network-drift.ts`; see its header).
|
|
12
|
+
*/
|
|
13
|
+
import type * as aclRules from '@distilled.cloud/unifi-network/access_control_acl_rules';
|
|
14
|
+
import { makeDriftOf } from './drift.ts';
|
|
15
|
+
import {
|
|
16
|
+
type AclRuleAttributes,
|
|
17
|
+
type AclRuleProps,
|
|
18
|
+
attributesOf,
|
|
19
|
+
normalizeDeviceFilter,
|
|
20
|
+
normalizeProtocolFilter,
|
|
21
|
+
} from './acl-rule-form.ts';
|
|
22
|
+
|
|
23
|
+
const fieldDrift = makeDriftOf<AclRuleAttributes, AclRuleProps>([
|
|
24
|
+
{ field: 'action', live: (a) => a.action, declared: (p) => p.action },
|
|
25
|
+
{ field: 'description', live: (a) => a.description, declared: (p) => p.description },
|
|
26
|
+
{
|
|
27
|
+
field: 'destinationFilter',
|
|
28
|
+
live: (a) => a.destinationFilter,
|
|
29
|
+
declared: (p) => p.destinationFilter,
|
|
30
|
+
},
|
|
31
|
+
{ field: 'enabled', live: (a) => a.enabled, declared: (p) => p.enabled },
|
|
32
|
+
{
|
|
33
|
+
field: 'enforcingDeviceFilter',
|
|
34
|
+
live: (a) => normalizeDeviceFilter(a.enforcingDeviceFilter),
|
|
35
|
+
declared: (p) => normalizeDeviceFilter(p.enforcingDeviceFilter),
|
|
36
|
+
},
|
|
37
|
+
{ field: 'name', live: (a) => a.name, declared: (p) => p.name },
|
|
38
|
+
{ field: 'sourceFilter', live: (a) => a.sourceFilter, declared: (p) => p.sourceFilter },
|
|
39
|
+
{ field: 'type', live: (a) => a.type, declared: (p) => p.type },
|
|
40
|
+
{
|
|
41
|
+
field: 'protocolFilter',
|
|
42
|
+
live: (a) => normalizeProtocolFilter(a.protocolFilter),
|
|
43
|
+
declared: (p) => normalizeProtocolFilter(p.protocolFilter),
|
|
44
|
+
},
|
|
45
|
+
{ field: 'networkIdFilter', live: (a) => a.networkIdFilter, declared: (p) => p.networkIdFilter },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export const matches = (attributes: AclRuleAttributes, props: AclRuleProps): boolean =>
|
|
49
|
+
fieldDrift(attributes, props).length === 0;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-field live-vs-declared drift straight from one live read — the task's own API
|
|
53
|
+
* (`driftOf(live, props)`), so `homeflare-network`'s pre-import drift check never has to derive
|
|
54
|
+
* `attributesOf` itself just to call this.
|
|
55
|
+
*/
|
|
56
|
+
export const driftOf = (live: aclRules.ACLRule, props: AclRuleProps) =>
|
|
57
|
+
fieldDrift(attributesOf(live, props), props);
|