@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
package/src/talos/credentials.ts
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Talos client material minted from OpenBao, written to a session-temp file whose lifetime the
|
|
3
|
+
* CALLER owns — never this module.
|
|
3
4
|
*
|
|
4
|
-
* ⛔ TALOSCONFIG AND secrets.yaml HOLD CLIENT CERTIFICATES AND CLUSTER CA
|
|
5
|
-
* persists resource attributes WITHOUT encryption — StateEncoding.ts
|
|
6
|
-
* `{
|
|
7
|
-
*
|
|
5
|
+
* ⛔ TALOSCONFIG, MACHINE CONFIG AND secrets.yaml CAN ALL HOLD CLIENT CERTIFICATES AND CLUSTER CA
|
|
6
|
+
* PRIVATE KEYS. Alchemy persists resource attributes WITHOUT encryption — `StateEncoding.ts`
|
|
7
|
+
* tags a `Redacted` value as `{ __redacted__: <plaintext> }` (`REDACTED_MARKER`), it does not
|
|
8
|
+
* encrypt it — and this estate's state store is the shared Cloudflare HTTP state store, not a
|
|
9
|
+
* database anyone forgets is world-legible. None of these three may ever be a prop, an
|
|
10
|
+
* attribute or a log line; see docs/plans/2026-09-26-talos-secrets-flow.md for why the OLD
|
|
11
|
+
* header here (Postgres, `{"@redacted": …}`) was wrong on both counts — the RULE survives the
|
|
12
|
+
* correction either way.
|
|
8
13
|
*
|
|
9
|
-
* ★ MODELED ON
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* ★ MODELED ON proxmox/credentials.ts + proxmox/mint.ts: mint at call time, use the material, drop
|
|
15
|
+
* it when the scope ends. The difference is Talos wants a FILE (`--talosconfig` / `-f`), so
|
|
16
|
+
* minting here writes a temp path and registers its own delete finalizer instead of just
|
|
17
|
+
* building a header.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ C1 FIX (2026-09-26, K-A3) — THE SHIPPED `mintTalosconfig` DELETED ITS OWN FILE BEFORE ANY
|
|
20
|
+
* CALLER COULD USE IT. It wrapped its whole body in `Effect.scoped(...)` and registered the
|
|
21
|
+
* delete finalizer inside that same scope, so the scope — and the file — closed the instant
|
|
22
|
+
* `mintTalosconfig` RETURNED, before the caller ever got to pass the path to `talosctl`. Fixed
|
|
23
|
+
* by `Effect.acquireRelease`: it still deletes on every exit path, but contributes `Scope.Scope`
|
|
24
|
+
* to this module's own return type instead of discharging it, so the file survives until
|
|
25
|
+
* whichever `Effect.scoped(...)` the CALLER wraps around its own use of the path (every
|
|
26
|
+
* talos-*.ts resource file wraps its `read`/`reconcile` bodies for exactly this reason now).
|
|
12
27
|
*
|
|
13
28
|
* ⚠️ REASONED FROM THE PUBLISHED TALOS WORKFLOW, NOT MEASURED ON THIS ESTATE. There is no Talos
|
|
14
|
-
* cluster here yet; paths below follow the
|
|
29
|
+
* cluster here yet; KV paths below follow the `mount/<key>` shape O1 (the secrets-flow doc)
|
|
30
|
+
* assigns, and `bao kv get` inserts the KV-v2 `data/` API segment itself — a key already
|
|
31
|
+
* prefixed `data/` reads `<mount>/data/data/<key>`, which the OLD default did.
|
|
15
32
|
*/
|
|
16
33
|
import { closeSync, openSync, writeFileSync } from 'node:fs';
|
|
17
34
|
import * as Effect from 'effect/Effect';
|
|
35
|
+
import type * as Scope from 'effect/Scope';
|
|
18
36
|
import * as Stream from 'effect/Stream';
|
|
19
37
|
import * as ChildProcess from 'effect/unstable/process/ChildProcess';
|
|
20
38
|
import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
|
|
@@ -26,10 +44,10 @@ export type TalosTarget = {
|
|
|
26
44
|
/** Logical cluster name — safe to persist and log. */
|
|
27
45
|
readonly cluster: string;
|
|
28
46
|
/**
|
|
29
|
-
* Path under the mount for the talosconfig YAML.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
47
|
+
* Path under the mount for the talosconfig YAML. Default `'talosconfig'` — FIXED 2026-09-26;
|
|
48
|
+
* the shipped default was `'data/talosconfig'`, which `bao kv get` turned into
|
|
49
|
+
* `<mount>/data/data/talosconfig` (see the ⚠️ above). It failed closed rather than reading an
|
|
50
|
+
* empty talosconfig, but it would have blocked the very first deploy.
|
|
33
51
|
*/
|
|
34
52
|
readonly talosconfigKey?: string;
|
|
35
53
|
};
|
|
@@ -39,98 +57,150 @@ export type TalosCredential = {
|
|
|
39
57
|
readonly talosconfigPath: string;
|
|
40
58
|
};
|
|
41
59
|
|
|
42
|
-
const
|
|
60
|
+
const DEFAULT_TALOSCONFIG_KEY = 'talosconfig';
|
|
43
61
|
|
|
44
62
|
/**
|
|
45
|
-
*
|
|
63
|
+
* Default OpenBao KV path (under `target.mount`) for the admin kubeconfig `Talos.Kubeconfig`
|
|
64
|
+
* writes once at cluster bring-up. See kubeconfig.ts's own header (K-talos-first-boot, 2026-09-26)
|
|
65
|
+
* and docs/plans/2026-09-26-talos-secrets-flow.md's O1 key list.
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_KUBECONFIG_KEY = 'kubeconfig';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read one OpenBao KV-v2 value and return the first of `fields` that is a non-empty string.
|
|
46
71
|
*
|
|
47
|
-
* ★ SHELLS
|
|
48
|
-
*
|
|
72
|
+
* ★ THE ONE PLACE THIS PACKAGE SHELLS TO `bao` — talosconfig, per-node machine config and any
|
|
73
|
+
* future KV-backed Talos material (secrets.yaml, ceph entity keys) all read through this,
|
|
74
|
+
* so there is exactly one spot that must never echo a KV value on failure.
|
|
75
|
+
* ⛔ stderr ONLY ON FAILURE. stdout may hold the very secret this call exists to fetch.
|
|
49
76
|
*/
|
|
50
|
-
export const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
export const readKvValue = (
|
|
78
|
+
mount: string,
|
|
79
|
+
key: string,
|
|
80
|
+
fields: readonly string[],
|
|
81
|
+
): Effect.Effect<string, Error, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
84
|
+
const result = yield* ChildProcess.make(
|
|
85
|
+
'bao',
|
|
86
|
+
['kv', 'get', '-format=json', `${mount}/${key}`],
|
|
87
|
+
{ detached: false, extendEnv: true, stderr: 'pipe', stdin: 'ignore', stdout: 'pipe' },
|
|
88
|
+
).pipe(
|
|
89
|
+
spawner.spawn,
|
|
90
|
+
Effect.flatMap((child) =>
|
|
91
|
+
Effect.all(
|
|
92
|
+
{
|
|
93
|
+
exitCode: child.exitCode,
|
|
94
|
+
stderr: child.stderr.pipe(
|
|
95
|
+
Stream.decodeText,
|
|
96
|
+
Stream.mkString,
|
|
97
|
+
Effect.map((text) => text.trim()),
|
|
98
|
+
),
|
|
99
|
+
stdout: child.stdout.pipe(
|
|
100
|
+
Stream.decodeText,
|
|
101
|
+
Stream.mkString,
|
|
102
|
+
Effect.map((text) => text.trim()),
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
{ concurrency: 'unbounded' },
|
|
78
106
|
),
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
),
|
|
108
|
+
Effect.scoped,
|
|
109
|
+
);
|
|
81
110
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
111
|
+
if (result.exitCode !== 0) {
|
|
112
|
+
return yield* Effect.fail(
|
|
113
|
+
new Error(`bao kv get ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
90
116
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
|
|
117
|
+
const parsed = JSON.parse(result.stdout) as { data?: { data?: Record<string, unknown> } };
|
|
118
|
+
const data = parsed.data?.data;
|
|
119
|
+
for (const field of fields) {
|
|
120
|
+
const raw = data?.[field];
|
|
121
|
+
if (typeof raw === 'string' && raw.trim() !== '') return raw;
|
|
122
|
+
}
|
|
123
|
+
return yield* Effect.fail(
|
|
124
|
+
new Error(
|
|
125
|
+
`${mount}/${key} returned no ${fields.join('/')} field. Never store the value itself in ` +
|
|
126
|
+
'an Alchemy prop or attribute — fix the KV entry, not the caller.',
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
101
130
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Write `raw` to a session-temp, 0600, exclusively-created file whose lifetime is the CALLER's
|
|
133
|
+
* `Effect.scoped`, not this call's. See the ⛔ C1 FIX note at the top of this file.
|
|
134
|
+
*
|
|
135
|
+
* ⛔ 0600, CREATED EXCLUSIVELY, AND `Bun.write` CANNOT DO EITHER. This file can hold a cluster
|
|
136
|
+
* admin client certificate or a machine config's bootstrap token. `Bun.write` takes no mode, so
|
|
137
|
+
* it lands at the process umask — world-readable on this estate — for as long as the resource
|
|
138
|
+
* runs. The name is unguessable, and "unguessable" is not a permission.
|
|
139
|
+
* ⚠️ `wx` IS THE OTHER HALF. `O_EXCL` means this cannot be made to write through a path an
|
|
140
|
+
* attacker pre-created (the classic /tmp symlink race), and with a UUIDv7 name a collision is a
|
|
141
|
+
* genuine error rather than something to paper over.
|
|
142
|
+
* ⚠️ A HARD KILL (SIGKILL, power loss) skips the finalizer and leaves the file behind. 0600 is
|
|
143
|
+
* what makes that survivable rather than a disclosure.
|
|
144
|
+
*/
|
|
145
|
+
export const mintKvTempFile = (
|
|
146
|
+
raw: string,
|
|
147
|
+
label: string,
|
|
148
|
+
): Effect.Effect<{ readonly path: string }, Error, Scope.Scope> =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const path = `${Bun.env['TMPDIR'] ?? '/tmp'}/hf-talos-${label}-${Bun.randomUUIDv7()}.yaml`;
|
|
151
|
+
yield* Effect.acquireRelease(
|
|
152
|
+
Effect.try({
|
|
116
153
|
try: () => {
|
|
117
|
-
const fd = openSync(
|
|
154
|
+
const fd = openSync(path, 'wx', 0o600);
|
|
118
155
|
try {
|
|
119
156
|
writeFileSync(fd, raw);
|
|
120
157
|
} finally {
|
|
121
158
|
closeSync(fd);
|
|
122
159
|
}
|
|
123
160
|
},
|
|
124
|
-
catch: (cause) => new Error(`writing temp
|
|
125
|
-
})
|
|
161
|
+
catch: (cause) => new Error(`writing ${label} temp file: ${String(cause)}`),
|
|
162
|
+
}),
|
|
163
|
+
() =>
|
|
164
|
+
Effect.tryPromise({ try: () => Bun.file(path).delete(), catch: () => undefined }).pipe(
|
|
165
|
+
Effect.orElseSucceed(() => undefined),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
return { path };
|
|
169
|
+
});
|
|
126
170
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Mint one talosconfig file for `target`.
|
|
173
|
+
*
|
|
174
|
+
* ⛔ CALLERS MUST WRAP THEIR WHOLE USE OF `credential.talosconfigPath` — not just this call — in
|
|
175
|
+
* `Effect.scoped(...)`. This function's own `Scope.Scope` requirement is what makes that the
|
|
176
|
+
* caller's job instead of a lie this function tells itself; see the header above.
|
|
177
|
+
*/
|
|
178
|
+
export const mintTalosconfig = (
|
|
179
|
+
target: TalosTarget,
|
|
180
|
+
): Effect.Effect<TalosCredential, Error, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope> =>
|
|
181
|
+
readKvValue(target.mount, target.talosconfigKey ?? DEFAULT_TALOSCONFIG_KEY, [
|
|
182
|
+
'talosconfig',
|
|
183
|
+
'config',
|
|
184
|
+
]).pipe(
|
|
185
|
+
Effect.flatMap((raw) => mintKvTempFile(raw, `${target.cluster}-talosconfig`)),
|
|
186
|
+
Effect.map(({ path }): TalosCredential => ({ talosconfigPath: path })),
|
|
187
|
+
);
|
|
133
188
|
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Mint one temp kubeconfig file from the vault copy `Talos.Kubeconfig` writes at bring-up
|
|
191
|
+
* (K-talos-first-boot, 2026-09-26) — the consumer-side counterpart to {@link mintTalosconfig},
|
|
192
|
+
* named as the future seam for `Kubernetes.ClusterAdapter` in
|
|
193
|
+
* docs/plans/2026-09-26-talos-secrets-flow.md ("consumers mint it the way the talosconfig is
|
|
194
|
+
* minted"). Not yet wired to that adapter — this only makes the read+materialize step exist.
|
|
195
|
+
*/
|
|
196
|
+
export const mintKubeconfig = (
|
|
197
|
+
target: TalosTarget,
|
|
198
|
+
key = DEFAULT_KUBECONFIG_KEY,
|
|
199
|
+
): Effect.Effect<
|
|
200
|
+
{ readonly path: string },
|
|
201
|
+
Error,
|
|
202
|
+
ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
|
|
203
|
+
> =>
|
|
204
|
+
readKvValue(target.mount, key, ['kubeconfig', 'config']).pipe(
|
|
205
|
+
Effect.flatMap((raw) => mintKvTempFile(raw, `${target.cluster}-kubeconfig`)),
|
|
136
206
|
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fake `ChildProcessSpawner` for offline Talos tests — no real `bao` or `talosctl` process ever
|
|
3
|
+
* starts. Shared by credentials.test.ts and talos-machine-config.test.ts (2026-09-26, K-A3);
|
|
4
|
+
* this package had NO process-level test fake before this file — every Talos resource file spawns
|
|
5
|
+
* a real binary in production and none of that was previously exercised offline.
|
|
6
|
+
*
|
|
7
|
+
* ★ A FULL FAKE SERVICE, NOT A REAL SPAWN WITH A FIXTURE EXECUTABLE ON `PATH`. The alternative —
|
|
8
|
+
* a tiny fake `bao`/`talosctl` script and a mutated `process.env.PATH` — spawns a REAL OS
|
|
9
|
+
* process and mutates process-global state for the test's duration, which is exactly the kind
|
|
10
|
+
* of cross-test interference the house rules ask tests to avoid. `ChildProcessSpawner.make`
|
|
11
|
+
* takes just a `spawn` function and derives everything else, so a fake `spawn` is the whole cost.
|
|
12
|
+
*/
|
|
13
|
+
import * as Effect from 'effect/Effect';
|
|
14
|
+
import * as Sink from 'effect/Sink';
|
|
15
|
+
import * as Stream from 'effect/Stream';
|
|
16
|
+
import type { Command } from 'effect/unstable/process/ChildProcess';
|
|
17
|
+
import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
|
|
18
|
+
|
|
19
|
+
export type FakeResult = {
|
|
20
|
+
readonly stdout?: string;
|
|
21
|
+
readonly stderr?: string;
|
|
22
|
+
readonly exitCode?: number;
|
|
23
|
+
};
|
|
24
|
+
export type FakeCall = {
|
|
25
|
+
readonly command: string;
|
|
26
|
+
readonly args: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Decoded stdin text, present only when the real caller piped a `Stream` in (K-talos-first-boot,
|
|
29
|
+
* `credentials-write.ts`'s `writeKvValue`) — the whole reason this fake exists is to prove a
|
|
30
|
+
* value went in via stdin and never via argv, so a test needs to see it.
|
|
31
|
+
*/
|
|
32
|
+
readonly stdin?: string;
|
|
33
|
+
};
|
|
34
|
+
export type FakeHandler = (call: FakeCall) => FakeResult;
|
|
35
|
+
|
|
36
|
+
const encode = (text: string) => new TextEncoder().encode(text);
|
|
37
|
+
|
|
38
|
+
const fakeHandle = (result: FakeResult): ChildProcessSpawner.ChildProcessHandle =>
|
|
39
|
+
ChildProcessSpawner.makeHandle({
|
|
40
|
+
all: Stream.fromIterable([encode((result.stdout ?? '') + (result.stderr ?? ''))]),
|
|
41
|
+
exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.exitCode ?? 0)),
|
|
42
|
+
getInputFd: () => Sink.drain,
|
|
43
|
+
getOutputFd: () => Stream.empty,
|
|
44
|
+
isRunning: Effect.succeed(false),
|
|
45
|
+
kill: () => Effect.void,
|
|
46
|
+
pid: ChildProcessSpawner.ProcessId(1),
|
|
47
|
+
stderr: Stream.fromIterable([encode(result.stderr ?? '')]),
|
|
48
|
+
stdin: Sink.drain,
|
|
49
|
+
stdout: Stream.fromIterable([encode(result.stdout ?? '')]),
|
|
50
|
+
unref: Effect.succeed(Effect.void),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Decode a `StandardCommand`'s `options.stdin` into text, or `undefined` when there was none to
|
|
55
|
+
* decode. Only `"pipe" | "inherit" | "ignore" | "overlapped"` (no bytes) and a raw `Stream` (the
|
|
56
|
+
* only two shapes anything in this package ever passes — `credentials-write.ts`'s `writeKvValue`
|
|
57
|
+
* uses `Stream.fromIterable`, matching `alchemy`'s own `Cloudflare/Workers/ViteChild.ts` usage) are
|
|
58
|
+
* handled; a `StdinConfig` wrapper is unwrapped to its own `.stream` field first.
|
|
59
|
+
*/
|
|
60
|
+
const stdinText = (stdin: unknown) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
if (stdin === undefined || typeof stdin === 'string') return undefined;
|
|
63
|
+
const stream =
|
|
64
|
+
typeof stdin === 'object' && stdin !== null && 'stream' in stdin
|
|
65
|
+
? (stdin as { stream: unknown }).stream
|
|
66
|
+
: stdin;
|
|
67
|
+
if (stream === undefined || typeof stream === 'string') return undefined;
|
|
68
|
+
const chunks = yield* Stream.runCollect(stream as Stream.Stream<Uint8Array>);
|
|
69
|
+
return chunks.map((chunk) => new TextDecoder().decode(chunk)).join('');
|
|
70
|
+
}).pipe(Effect.orDie);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a fake `ChildProcessSpawner.Service` from `handler`, which sees every command's
|
|
74
|
+
* executable + argv (+ decoded stdin, when piped) and returns canned stdout/stderr/exitCode.
|
|
75
|
+
* `credentials.ts` and `talosctl.ts` only ever build `StandardCommand`s (never a piped command),
|
|
76
|
+
* so that is the only shape this fake accepts — a `PipedCommand` is a test-authoring mistake, not
|
|
77
|
+
* something to fake.
|
|
78
|
+
*/
|
|
79
|
+
export const fakeSpawner = (handler: FakeHandler, calls: FakeCall[] = []) =>
|
|
80
|
+
ChildProcessSpawner.make((command: Command) =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
if (command._tag !== 'StandardCommand') {
|
|
83
|
+
return yield* Effect.die(new Error('fake-process: only StandardCommand is supported'));
|
|
84
|
+
}
|
|
85
|
+
const stdin = yield* stdinText(command.options.stdin);
|
|
86
|
+
const call: FakeCall = {
|
|
87
|
+
args: command.args,
|
|
88
|
+
command: command.command,
|
|
89
|
+
...(stdin === undefined ? {} : { stdin }),
|
|
90
|
+
};
|
|
91
|
+
calls.push(call);
|
|
92
|
+
return fakeHandle(handler(call));
|
|
93
|
+
}),
|
|
94
|
+
);
|
package/src/talos/index.ts
CHANGED
|
@@ -5,10 +5,45 @@
|
|
|
5
5
|
* These are the symbols a real stack consumes; the rest of the files are internals a
|
|
6
6
|
* provider needs but a consumer should not depend on. An `export *` here would publish
|
|
7
7
|
* every helper as API and make the next refactor a breaking change.
|
|
8
|
+
* ★ `Talos.MachineConfig`'S RESOURCE CLASS JOINED ITS PROVIDER 2026-09-26 (K-A3) — until now this
|
|
9
|
+
* barrel exported only `Talos*Provider`, which builds the provider but gives a consuming stack
|
|
10
|
+
* nothing to `new` or call to actually DECLARE a row (the same gap proxmox/index.ts's own header
|
|
11
|
+
* calls out for `ProxmoxVm`). MachineConfig is safe to declare today: it fails closed on a digest
|
|
12
|
+
* mismatch before touching a node, and its cold-start read never silently adopts (PR 307 fix-first
|
|
13
|
+
* #1, machine-config-read.ts's own header).
|
|
14
|
+
* ★ BOOTSTRAP, CLUSTERHEALTH AND KUBECONFIG JOIN THEM (K-talos-first-boot, 2026-09-26) — PR 307's
|
|
15
|
+
* red team (fix-first #3) held these back because `Talos.Bootstrap` could re-run `talosctl
|
|
16
|
+
* bootstrap` against an already-bootstrapped cluster and `Talos.Kubeconfig` wrote a cluster-admin
|
|
17
|
+
* kubeconfig to un-vaulted host disk. Both are fixed now: bootstrap's "once means once" invariant
|
|
18
|
+
* (talos-bootstrap.ts's own header) and kubeconfig landing in OpenBao, written once at bring-up
|
|
19
|
+
* (kubeconfig.ts's own header) — plus `Talos.ClusterHealth`'s error handling no longer conflates a
|
|
20
|
+
* vault/transport failure with "cluster not healthy yet" (talos-cluster-health.ts's own header).
|
|
8
21
|
* ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
|
|
9
22
|
* deliberate, visible act rather than an accident of barrelling.
|
|
10
23
|
*/
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
export type { TalosCredential, TalosTarget } from './credentials.ts';
|
|
25
|
+
export {
|
|
26
|
+
type KubeconfigAttributes,
|
|
27
|
+
type KubeconfigProps,
|
|
28
|
+
TalosKubeconfig,
|
|
29
|
+
TalosKubeconfigProvider,
|
|
30
|
+
} from './kubeconfig.ts';
|
|
31
|
+
export type { ApplyMode, WithTarget } from './resource.ts';
|
|
32
|
+
export {
|
|
33
|
+
type BootstrapAttributes,
|
|
34
|
+
type BootstrapProps,
|
|
35
|
+
TalosBootstrap,
|
|
36
|
+
TalosBootstrapProvider,
|
|
37
|
+
} from './talos-bootstrap.ts';
|
|
38
|
+
export {
|
|
39
|
+
type ClusterHealthAttributes,
|
|
40
|
+
type ClusterHealthProps,
|
|
41
|
+
TalosClusterHealth,
|
|
42
|
+
TalosClusterHealthProvider,
|
|
43
|
+
} from './talos-cluster-health.ts';
|
|
44
|
+
export {
|
|
45
|
+
type MachineConfigAttributes,
|
|
46
|
+
type MachineConfigProps,
|
|
47
|
+
TalosMachineConfig,
|
|
48
|
+
TalosMachineConfigProvider,
|
|
49
|
+
} from './talos-machine-config.ts';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Talos.Kubeconfig`'s attribute builder and its `Kubernetes.Connection` — split out of
|
|
3
|
+
* kubeconfig.ts (LAND, 2026-09-26, C1/I2 fixes) once fixing those findings pushed the file over the
|
|
4
|
+
* 250-line cap, the same reason machine-config-read.ts/machine-config-poll.ts/talos-errors.ts were
|
|
5
|
+
* split out of talos-machine-config.ts before it. Only `import type` comes back from kubeconfig.ts,
|
|
6
|
+
* so there is no runtime cycle between the two files.
|
|
7
|
+
*/
|
|
8
|
+
import type { Connection } from 'alchemy/Kubernetes/Connection';
|
|
9
|
+
import type { KubeconfigAttributes, KubeconfigProps } from './kubeconfig.ts';
|
|
10
|
+
import { kubeconfigMetadata, sha256 } from './values.ts';
|
|
11
|
+
|
|
12
|
+
const generation = (meta: {
|
|
13
|
+
endpoint: string;
|
|
14
|
+
caFingerprint: string;
|
|
15
|
+
clientFingerprint: string;
|
|
16
|
+
context: string;
|
|
17
|
+
}) => sha256(`${meta.context}\n${meta.endpoint}\n${meta.caFingerprint}\n${meta.clientFingerprint}`);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ⛔ I2 FIX (LAND red team, 2026-09-26) — NEVER LEAVE `path` UNDEFINED. An undefined `path` here
|
|
21
|
+
* made the stock `Kubernetes.KubeConfigAdapter` fall back to `$KUBECONFIG`/`~/.kube/config`
|
|
22
|
+
* (alchemy `Kubernetes/internal/kubeconfig.ts`'s `resolveKubeConfigPath`, wired in
|
|
23
|
+
* `BuiltinAdapters.ts`) — exactly the un-vaulted, possibly-stale-context exposure this whole
|
|
24
|
+
* family exists to remove. A consumer wiring `Kubernetes.*` to `kube.connection` (this
|
|
25
|
+
* attribute's own doc invites exactly that) would silently connect through whatever
|
|
26
|
+
* `admin@<cluster>` context happens to be on the operator's machine. `Kubernetes.ClusterAdapter`
|
|
27
|
+
* is not yet wired to `mintKubeconfig` (kubeconfig.ts's own header) — no real path exists for
|
|
28
|
+
* this connection to use — so it must fail LOUDLY instead of silently succeeding against a
|
|
29
|
+
* stranger's cluster. A path that can never resolve does that: `resolveKubeContext` fails at
|
|
30
|
+
* `fs.readFileString` naming this exact sentinel, instead of reading whatever real file happens
|
|
31
|
+
* to be on `$KUBECONFIG`.
|
|
32
|
+
*/
|
|
33
|
+
const UNWIRED_KUBECONFIG_PATH =
|
|
34
|
+
'/talos-first-boot-unwired/wire-Kubernetes.ClusterAdapter-to-mintKubeconfig';
|
|
35
|
+
|
|
36
|
+
const toConnection = (props: KubeconfigProps): Connection => ({
|
|
37
|
+
auth: { context: props.context, kind: 'kubeconfig', path: UNWIRED_KUBECONFIG_PATH },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Parse `raw` kubeconfig YAML into public, persistable attributes, or `undefined` if it fails to. */
|
|
41
|
+
export const buildAttrs = (
|
|
42
|
+
raw: string,
|
|
43
|
+
props: KubeconfigProps,
|
|
44
|
+
): KubeconfigAttributes | undefined => {
|
|
45
|
+
const meta = kubeconfigMetadata(raw, props.context);
|
|
46
|
+
if (meta === undefined) return undefined;
|
|
47
|
+
return {
|
|
48
|
+
certificateAuthorityFingerprint: meta.caFingerprint,
|
|
49
|
+
clientCertificateFingerprint: meta.clientFingerprint,
|
|
50
|
+
connection: toConnection(props),
|
|
51
|
+
context: props.context,
|
|
52
|
+
credentialGeneration: generation({ ...meta, context: props.context }),
|
|
53
|
+
endpoint: meta.endpoint,
|
|
54
|
+
};
|
|
55
|
+
};
|