@homeflare/alchemy 0.39.0 → 0.40.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/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 +1609 -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/proxmox/index.js +15 -6
- package/dist/proxmox/index.js.map +3 -3
- package/dist/proxmox/qemu-volume.d.ts +12 -2
- package/dist/proxmox/qemu-volume.d.ts.map +1 -1
- 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/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 +6 -2
- 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/proxmox/qemu-volume.test.ts +153 -4
- package/src/proxmox/qemu-volume.ts +71 -14
- 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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `credentials-write.ts` — offline, fake `bao`, no real process ever spawns. Fixtures only.
|
|
3
|
+
*/
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
|
|
9
|
+
import { reservedTempPath, writeKvValue } from './credentials-write.ts';
|
|
10
|
+
import { type FakeCall, fakeSpawner } from './fake-process.ts';
|
|
11
|
+
|
|
12
|
+
const run = <A, E>(
|
|
13
|
+
effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
|
|
14
|
+
handler: (c: FakeCall) => { stdout?: string; stderr?: string; exitCode?: number },
|
|
15
|
+
calls: FakeCall[] = [],
|
|
16
|
+
) =>
|
|
17
|
+
Effect.runPromise(
|
|
18
|
+
Effect.provideService(
|
|
19
|
+
effect,
|
|
20
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
21
|
+
fakeSpawner(handler, calls),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
describe('writeKvValue', () => {
|
|
26
|
+
it('sends the value on stdin, never in argv', async () => {
|
|
27
|
+
const calls: FakeCall[] = [];
|
|
28
|
+
await run(
|
|
29
|
+
writeKvValue('talos-c1', 'kubeconfig', 'kubeconfig', 'super-secret-kubeconfig-body'),
|
|
30
|
+
() => ({}),
|
|
31
|
+
calls,
|
|
32
|
+
);
|
|
33
|
+
assert.equal(calls.length, 1);
|
|
34
|
+
// ⛔ C2 (LAND red team): `=-`, never `=@-` — `@` means "read a file at this literal path",
|
|
35
|
+
// which OpenBao's own CLI rejects (or, worse, silently reads a file named `-` if one exists).
|
|
36
|
+
assert.deepEqual(calls[0]?.args, ['kv', 'put', 'talos-c1/kubeconfig', 'kubeconfig=-']);
|
|
37
|
+
assert.equal(calls[0]?.stdin, 'super-secret-kubeconfig-body');
|
|
38
|
+
// ⛔ THE POINT OF THE TEST: the value appears nowhere in argv.
|
|
39
|
+
assert.ok(!calls[0]?.args.some((arg) => arg.includes('super-secret-kubeconfig-body')));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fails on a non-zero exit and never echoes stdin in the error', async () => {
|
|
43
|
+
await assert.rejects(
|
|
44
|
+
run(writeKvValue('talos-c1', 'kubeconfig', 'kubeconfig', 'secret-body'), () => ({
|
|
45
|
+
exitCode: 2,
|
|
46
|
+
stderr: 'permission denied',
|
|
47
|
+
})),
|
|
48
|
+
(error: unknown) =>
|
|
49
|
+
error instanceof Error &&
|
|
50
|
+
error.message.includes('permission denied') &&
|
|
51
|
+
!error.message.includes('secret-body'),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('reservedTempPath', () => {
|
|
57
|
+
it('does not pre-create the file, and cleans up if something else wrote there', async () => {
|
|
58
|
+
let path = '';
|
|
59
|
+
await Effect.runPromise(
|
|
60
|
+
Effect.scoped(
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const reserved = yield* reservedTempPath('c1-node-kubeconfig-out');
|
|
63
|
+
path = reserved.path;
|
|
64
|
+
assert.equal(existsSync(path), false, 'nothing has written here yet');
|
|
65
|
+
yield* Effect.promise(() =>
|
|
66
|
+
Bun.write(path, 'written by the caller, like talosctl would'),
|
|
67
|
+
);
|
|
68
|
+
assert.ok(existsSync(path));
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
assert.equal(existsSync(path), false, 'must be gone once the caller scope closed');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('two calls never collide on the same path', async () => {
|
|
76
|
+
const [a, b] = await Effect.runPromise(
|
|
77
|
+
Effect.scoped(Effect.all([reservedTempPath('same-label'), reservedTempPath('same-label')])),
|
|
78
|
+
);
|
|
79
|
+
assert.notEqual(a.path, b.path);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write half of Talos's OpenBao KV access — split out of credentials.ts (2026-09-26,
|
|
3
|
+
* K-talos-first-boot) once it needed its own header, the same reason machine-config-read.ts,
|
|
4
|
+
* machine-config-poll.ts and talos-errors.ts were split out of talos-machine-config.ts before it.
|
|
5
|
+
* `credentials.ts` stays the read side (mint a talosconfig, read a KV value); this file is the one
|
|
6
|
+
* place anything in this package ever writes TO OpenBao.
|
|
7
|
+
*
|
|
8
|
+
* ★ FIRST CALLER: `Talos.Kubeconfig` (kubeconfig.ts) writing the admin kubeconfig it generates at
|
|
9
|
+
* cluster bring-up into the vault instead of an un-vaulted host `runtimePath` — see that file's
|
|
10
|
+
* own header. `Ceph.AuthEntity` (K-A4, a separate design/PR) is expected to need the same shape
|
|
11
|
+
* for its own vault-written entity keys; this is not yet shared with it on purpose — two callers
|
|
12
|
+
* is the point at which sharing stops being speculative.
|
|
13
|
+
*/
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import type * as Scope from 'effect/Scope';
|
|
16
|
+
import * as Stream from 'effect/Stream';
|
|
17
|
+
import * as ChildProcess from 'effect/unstable/process/ChildProcess';
|
|
18
|
+
import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A fresh, unguessable temp path for SOME OTHER PROCESS (talosctl) to create — unlike
|
|
22
|
+
* `credentials.ts`'s `mintKvTempFile`, this does NOT pre-create the file with `wx`, because the
|
|
23
|
+
* caller here is capturing a command's OWN output rather than writing known content itself. The
|
|
24
|
+
* random UUIDv7 name is the only pre-fabrication guard (an attacker cannot pre-plant a symlink at a
|
|
25
|
+
* path it cannot predict) — first used by `kubeconfig.ts` to capture `talosctl kubeconfig`'s
|
|
26
|
+
* generated file before reading it back and writing it into the vault.
|
|
27
|
+
*/
|
|
28
|
+
export const reservedTempPath = (
|
|
29
|
+
label: string,
|
|
30
|
+
): Effect.Effect<{ readonly path: string }, never, Scope.Scope> =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const path = `${Bun.env['TMPDIR'] ?? '/tmp'}/hf-talos-${label}-${Bun.randomUUIDv7()}.yaml`;
|
|
33
|
+
yield* Effect.addFinalizer(() =>
|
|
34
|
+
Effect.tryPromise({ try: () => Bun.file(path).delete(), catch: () => undefined }).pipe(
|
|
35
|
+
Effect.orElseSucceed(() => undefined),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
return { path };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write one field of an OpenBao KV-v2 value from `value` — never argv, the mirror image of
|
|
43
|
+
* `credentials.ts`'s `readKvValue`.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ C2 FIX (LAND red team, 2026-09-26) — `field=@-` IS WRONG AND WAS NEVER MEASURED. `@value` means
|
|
46
|
+
* "read a file at this path"; a literal path of `-` is not the stdin convention for `@` — it is
|
|
47
|
+
* just a file named `-`. MEASURED against the estate's OpenBao v2.6.2 binary (isolated in-memory
|
|
48
|
+
* dev server, `env -i`, scratch HOME, no estate credentials involved; server removed after):
|
|
49
|
+
* `field=@-` exits 1 with `invalid key/value pair "field=@-": error reading file: open -: no
|
|
50
|
+
* such file or directory`, and if a file literally named `-` exists in the working directory it
|
|
51
|
+
* silently stores THAT file's bytes instead. `field=-` (no `@`) is Vault/OpenBao's own stdin
|
|
52
|
+
* convention and was confirmed against the same binary to read the piped bytes correctly. Every
|
|
53
|
+
* `Talos.Kubeconfig` create failed here — AFTER `talosctl kubeconfig` had already issued a fresh
|
|
54
|
+
* admin client certificate, which cannot be revoked.
|
|
55
|
+
* ⛔ stderr ONLY ON FAILURE — same rule as `readKvValue`: never echo what was being written, on
|
|
56
|
+
* success or failure.
|
|
57
|
+
*/
|
|
58
|
+
export const writeKvValue = (
|
|
59
|
+
mount: string,
|
|
60
|
+
key: string,
|
|
61
|
+
field: string,
|
|
62
|
+
value: string,
|
|
63
|
+
): Effect.Effect<void, Error, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
66
|
+
const result = yield* ChildProcess.make('bao', ['kv', 'put', `${mount}/${key}`, `${field}=-`], {
|
|
67
|
+
detached: false,
|
|
68
|
+
extendEnv: true,
|
|
69
|
+
stderr: 'pipe',
|
|
70
|
+
stdin: Stream.fromIterable([new TextEncoder().encode(value)]),
|
|
71
|
+
stdout: 'pipe',
|
|
72
|
+
}).pipe(
|
|
73
|
+
spawner.spawn,
|
|
74
|
+
Effect.flatMap((child) =>
|
|
75
|
+
Effect.all(
|
|
76
|
+
{
|
|
77
|
+
exitCode: child.exitCode,
|
|
78
|
+
stderr: child.stderr.pipe(
|
|
79
|
+
Stream.decodeText,
|
|
80
|
+
Stream.mkString,
|
|
81
|
+
Effect.map((text) => text.trim()),
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
{ concurrency: 'unbounded' },
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
Effect.scoped,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (result.exitCode !== 0) {
|
|
91
|
+
return yield* Effect.fail(
|
|
92
|
+
new Error(`bao kv put ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The C1 lifetime fix — pins that `mintTalosconfig`'s temp file survives until the CALLER's own
|
|
3
|
+
* `Effect.scoped` closes, not until `mintTalosconfig` itself returns (the shipped defect,
|
|
4
|
+
* credentials.ts's own header). Fully offline: fake-process.ts fakes `bao`, nothing real spawns.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ FIXTURES ONLY. No real OpenBao, no real talosconfig material — every "talosconfig" body
|
|
7
|
+
* below is a plain marker string, not YAML that could be mistaken for the real shape.
|
|
8
|
+
*/
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { existsSync, statSync } from 'node:fs';
|
|
11
|
+
import { describe, it } from 'node:test';
|
|
12
|
+
import * as Effect from 'effect/Effect';
|
|
13
|
+
import * as ChildProcessSpawner from 'effect/unstable/process/ChildProcessSpawner';
|
|
14
|
+
import { mintKubeconfig, mintKvTempFile, mintTalosconfig, readKvValue } from './credentials.ts';
|
|
15
|
+
import { type FakeCall, fakeSpawner } from './fake-process.ts';
|
|
16
|
+
|
|
17
|
+
const bao =
|
|
18
|
+
(data: Record<string, unknown>, exitCode = 0) =>
|
|
19
|
+
(call: FakeCall) => {
|
|
20
|
+
assert.equal(call.command, 'bao');
|
|
21
|
+
return exitCode === 0
|
|
22
|
+
? { stdout: JSON.stringify({ data: { data } }) }
|
|
23
|
+
: { exitCode, stderr: 'permission denied' };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const run = <A, E>(
|
|
27
|
+
effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
|
|
28
|
+
handler: (c: FakeCall) => ReturnType<ReturnType<typeof bao>>,
|
|
29
|
+
calls: FakeCall[] = [],
|
|
30
|
+
) =>
|
|
31
|
+
Effect.runPromise(
|
|
32
|
+
Effect.provideService(
|
|
33
|
+
effect,
|
|
34
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
35
|
+
fakeSpawner(handler, calls),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
describe('mintTalosconfig — C1 lifetime fix', () => {
|
|
40
|
+
it('reads talosconfig/<mount> — the FIXED default key, not the doubled data/data path', async () => {
|
|
41
|
+
const calls: FakeCall[] = [];
|
|
42
|
+
let capturedPath = '';
|
|
43
|
+
await Effect.runPromise(
|
|
44
|
+
Effect.scoped(
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const credential = yield* mintTalosconfig({ cluster: 'c1', mount: 'talos-c1' });
|
|
47
|
+
capturedPath = credential.talosconfigPath;
|
|
48
|
+
assert.ok(existsSync(capturedPath), 'file must exist while the caller scope is open');
|
|
49
|
+
}),
|
|
50
|
+
).pipe(
|
|
51
|
+
Effect.provideService(
|
|
52
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
53
|
+
fakeSpawner(bao({ talosconfig: 'fake-talosconfig-body' }), calls),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
assert.equal(calls.length, 1);
|
|
58
|
+
assert.deepEqual(calls[0]?.args, ['kv', 'get', '-format=json', 'talos-c1/talosconfig']);
|
|
59
|
+
assert.equal(existsSync(capturedPath), false, 'must be gone once the caller scope closed');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is gone even when the caller scope is closed by a later failure (finalizers always run)', async () => {
|
|
63
|
+
let capturedPath = '';
|
|
64
|
+
const failing = Effect.scoped(
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
const credential = yield* mintTalosconfig({ cluster: 'c1', mount: 'talos-c1' });
|
|
67
|
+
capturedPath = credential.talosconfigPath;
|
|
68
|
+
return yield* Effect.fail(new Error('downstream talosctl call failed'));
|
|
69
|
+
}),
|
|
70
|
+
).pipe(
|
|
71
|
+
Effect.provideService(
|
|
72
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
73
|
+
fakeSpawner(bao({ talosconfig: 'fake-body' })),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
await assert.rejects(Effect.runPromise(failing));
|
|
77
|
+
assert.equal(existsSync(capturedPath), false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('honors a custom talosconfigKey without adding a data/ prefix', async () => {
|
|
81
|
+
const calls: FakeCall[] = [];
|
|
82
|
+
await run(
|
|
83
|
+
Effect.scoped(
|
|
84
|
+
mintTalosconfig({ cluster: 'c1', mount: 'talos-c1', talosconfigKey: 'context/admin' }),
|
|
85
|
+
),
|
|
86
|
+
bao({ config: 'fallback-field-body' }),
|
|
87
|
+
calls,
|
|
88
|
+
);
|
|
89
|
+
assert.deepEqual(calls[0]?.args, ['kv', 'get', '-format=json', 'talos-c1/context/admin']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('fails without spawning talosctl or leaving a file when bao denies the read', async () => {
|
|
93
|
+
await assert.rejects(
|
|
94
|
+
run(Effect.scoped(mintTalosconfig({ cluster: 'c1', mount: 'talos-c1' })), bao({}, 1)),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('fails when the KV entry has neither a talosconfig nor a config field', async () => {
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
run(
|
|
101
|
+
Effect.scoped(mintTalosconfig({ cluster: 'c1', mount: 'talos-c1' })),
|
|
102
|
+
bao({ unrelated: 'x' }),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('mintKvTempFile', () => {
|
|
109
|
+
it('writes exactly the given content at mode 0600', async () => {
|
|
110
|
+
let path = '';
|
|
111
|
+
await Effect.runPromise(
|
|
112
|
+
Effect.scoped(
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
const file = yield* mintKvTempFile('exact body\n', 'c1-node-config');
|
|
115
|
+
path = file.path;
|
|
116
|
+
const text = yield* Effect.promise(() => Bun.file(path).text());
|
|
117
|
+
assert.equal(text, 'exact body\n');
|
|
118
|
+
assert.equal(statSync(path).mode & 0o777, 0o600);
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
assert.equal(existsSync(path), false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('mintKubeconfig — the consumer-side seam, same lifetime shape as mintTalosconfig', () => {
|
|
127
|
+
it('reads the default kubeconfig key and the file survives only within the caller scope', async () => {
|
|
128
|
+
const calls: FakeCall[] = [];
|
|
129
|
+
let capturedPath = '';
|
|
130
|
+
await Effect.runPromise(
|
|
131
|
+
Effect.scoped(
|
|
132
|
+
Effect.gen(function* () {
|
|
133
|
+
const file = yield* mintKubeconfig({ cluster: 'c1', mount: 'talos-c1' });
|
|
134
|
+
capturedPath = file.path;
|
|
135
|
+
assert.ok(existsSync(capturedPath));
|
|
136
|
+
}),
|
|
137
|
+
).pipe(
|
|
138
|
+
Effect.provideService(
|
|
139
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
140
|
+
fakeSpawner(bao({ kubeconfig: 'fake-kubeconfig-body' }), calls),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
assert.deepEqual(calls[0]?.args, ['kv', 'get', '-format=json', 'talos-c1/kubeconfig']);
|
|
145
|
+
assert.equal(existsSync(capturedPath), false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('readKvValue', () => {
|
|
150
|
+
it('tries fields in order and returns the first non-empty match', async () => {
|
|
151
|
+
const value = await run(
|
|
152
|
+
readKvValue('talos-c1', 'nodes/10001', ['config', 'legacyConfig']),
|
|
153
|
+
bao({ legacyConfig: 'second-field-body' }),
|
|
154
|
+
);
|
|
155
|
+
assert.equal(value, 'second-field-body');
|
|
156
|
+
});
|
|
157
|
+
});
|
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
|
);
|