@homeflare/alchemy 0.47.1 → 0.48.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.
Files changed (128) hide show
  1. package/dist/talos/bao-env.d.ts +7 -0
  2. package/dist/talos/bao-env.d.ts.map +1 -0
  3. package/dist/talos/cluster-adapter-errors.d.ts +134 -0
  4. package/dist/talos/cluster-adapter-errors.d.ts.map +1 -0
  5. package/dist/talos/cluster-adapter.d.ts +115 -0
  6. package/dist/talos/cluster-adapter.d.ts.map +1 -0
  7. package/dist/talos/cluster-identity.d.ts +28 -0
  8. package/dist/talos/cluster-identity.d.ts.map +1 -0
  9. package/dist/talos/cluster-transport.d.ts +64 -0
  10. package/dist/talos/cluster-transport.d.ts.map +1 -0
  11. package/dist/talos/credentials-write.d.ts.map +1 -1
  12. package/dist/talos/credentials.d.ts +10 -19
  13. package/dist/talos/credentials.d.ts.map +1 -1
  14. package/dist/talos/fake-apiserver.d.ts +9 -0
  15. package/dist/talos/fake-apiserver.d.ts.map +1 -0
  16. package/dist/talos/index.d.ts +7 -0
  17. package/dist/talos/index.d.ts.map +1 -1
  18. package/dist/talos/index.js +669 -167
  19. package/dist/talos/index.js.map +22 -10
  20. package/dist/talos/kubeconfig-attrs.d.ts +14 -0
  21. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -1
  22. package/dist/talos/kubeconfig-doc.d.ts +23 -0
  23. package/dist/talos/kubeconfig-doc.d.ts.map +1 -0
  24. package/dist/talos/kubeconfig.d.ts +0 -12
  25. package/dist/talos/kubeconfig.d.ts.map +1 -1
  26. package/dist/talos/literal-connection.d.ts +16 -0
  27. package/dist/talos/literal-connection.d.ts.map +1 -0
  28. package/dist/talos/machine-config-read.d.ts +1 -1
  29. package/dist/talos/machine-config-read.d.ts.map +1 -1
  30. package/dist/talos/mint-temp-file.d.ts +20 -0
  31. package/dist/talos/mint-temp-file.d.ts.map +1 -0
  32. package/dist/talos/node-temp.harness.d.ts +2 -0
  33. package/dist/talos/node-temp.harness.d.ts.map +1 -0
  34. package/dist/talos/talos-bootstrap-etcd.d.ts +1 -1
  35. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -1
  36. package/dist/talos/talos-cluster-health.d.ts +15 -15
  37. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  38. package/dist/talos/talos-cluster-identity.d.ts +65 -0
  39. package/dist/talos/talos-cluster-identity.d.ts.map +1 -0
  40. package/dist/talos/talosctl-binary.d.ts +43 -0
  41. package/dist/talos/talosctl-binary.d.ts.map +1 -0
  42. package/dist/talos/talosctl.d.ts +12 -15
  43. package/dist/talos/talosctl.d.ts.map +1 -1
  44. package/dist/talos/trust-boundary.d.ts +8 -0
  45. package/dist/talos/trust-boundary.d.ts.map +1 -0
  46. package/dist/talos/values.d.ts.map +1 -1
  47. package/dist/talos/yaml-parse.d.ts +3 -0
  48. package/dist/talos/yaml-parse.d.ts.map +1 -0
  49. package/dist/verify/fake-engine.d.ts +2 -0
  50. package/dist/verify/fake-engine.d.ts.map +1 -1
  51. package/docs/talos-openbao-adapter.md +44 -0
  52. package/docs/upstream-conformance-bun.md +61 -24
  53. package/package.json +6 -2
  54. package/src/talos/bao-env.ts +16 -0
  55. package/src/talos/bao-vetting.test.ts +97 -0
  56. package/src/talos/cluster-adapter-deadline.test.ts +110 -0
  57. package/src/talos/cluster-adapter-errors.ts +192 -0
  58. package/src/talos/cluster-adapter-move.test.ts +129 -0
  59. package/src/talos/cluster-adapter-reconcile.test.ts +165 -0
  60. package/src/talos/cluster-adapter-refusals.test.ts +102 -0
  61. package/src/talos/cluster-adapter-rename.test.ts +87 -0
  62. package/src/talos/cluster-adapter-secrecy.test.ts +243 -0
  63. package/src/talos/cluster-adapter.fixtures.ts +125 -0
  64. package/src/talos/cluster-adapter.test.ts +240 -0
  65. package/src/talos/cluster-adapter.ts +249 -0
  66. package/src/talos/cluster-identity.ts +63 -0
  67. package/src/talos/cluster-transport.ts +110 -0
  68. package/src/talos/credentials-write.ts +27 -5
  69. package/src/talos/credentials.ts +51 -45
  70. package/src/talos/fake-apiserver.ts +55 -0
  71. package/src/talos/fake-process.ts +47 -1
  72. package/src/talos/index.ts +32 -0
  73. package/src/talos/kubeconfig-adapter.test.ts +37 -0
  74. package/src/talos/kubeconfig-attrs.ts +12 -28
  75. package/src/talos/kubeconfig-doc.ts +74 -0
  76. package/src/talos/kubeconfig.fixtures.ts +52 -0
  77. package/src/talos/kubeconfig.test.ts +6 -54
  78. package/src/talos/kubeconfig.ts +18 -34
  79. package/src/talos/literal-connection.ts +59 -0
  80. package/src/talos/mint-temp-file.ts +56 -0
  81. package/src/talos/node-connect.harness.ts +78 -0
  82. package/src/talos/node-connect.test.ts +27 -0
  83. package/src/talos/node-temp.harness.ts +40 -0
  84. package/src/talos/node-temp.test.ts +26 -0
  85. package/src/talos/talos-cluster-health-declare.test.ts +54 -0
  86. package/src/talos/talos-cluster-health-literal.test.ts +69 -0
  87. package/src/talos/talos-cluster-health.test.ts +104 -18
  88. package/src/talos/talos-cluster-health.ts +46 -14
  89. package/src/talos/talos-cluster-identity.test.ts +96 -0
  90. package/src/talos/talos-cluster-identity.ts +130 -0
  91. package/src/talos/talosctl-binary.ts +150 -0
  92. package/src/talos/talosctl-vetting.test.ts +212 -0
  93. package/src/talos/talosctl.test.ts +110 -0
  94. package/src/talos/talosctl.ts +101 -15
  95. package/src/talos/trust-boundary.test.ts +17 -0
  96. package/src/talos/trust-boundary.ts +45 -0
  97. package/src/talos/values.ts +5 -12
  98. package/src/talos/yaml-parse.test.ts +41 -0
  99. package/src/talos/yaml-parse.ts +28 -0
  100. package/src/verify/fake-engine.ts +10 -0
  101. package/dist/talos/fake-process.d.ts +0 -56
  102. package/dist/talos/fake-process.d.ts.map +0 -1
  103. package/dist/talos/machine-config-fixtures.d.ts +0 -60
  104. package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
  105. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
  106. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
  107. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
  108. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
  109. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
  110. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
  111. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
  112. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
  113. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
  114. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
  115. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
  116. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
  117. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
  118. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
  119. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
  120. package/src/release/fixtures/openbao/checksums.txt +0 -83
  121. package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
  122. package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
  123. package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
  124. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
  125. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
  126. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
  127. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
  128. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +0 -8
@@ -0,0 +1,243 @@
1
+ /**
2
+ * `talos-openbao` secrecy and identity, through the real adapter and provider paths. Fake `bao`
3
+ * only. Asserts the persisted Connection is `{ kind, uid }` (so a vault-key rename cannot make
4
+ * upstream answer `replace` and delete same-named objects), and that no PEM marker, base64 key
5
+ * body or kubeconfig byte reaches attributes, error fields, disk or logs.
6
+ */
7
+ import fs, { existsSync } from 'node:fs';
8
+ import { expect, mock, spyOn, test } from 'bun:test';
9
+ import { ClusterAdapter } from 'alchemy/Kubernetes/ClusterAdapter';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Layer from 'effect/Layer';
12
+ import * as Logger from 'effect/Logger';
13
+ import * as References from 'effect/References';
14
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
15
+ import {
16
+ TalosOpenBaoAdapter,
17
+ TalosVaultKeyMissing,
18
+ connectTalosOpenBao,
19
+ talosOpenBaoConnection,
20
+ } from './cluster-adapter.ts';
21
+ import { fakeApiServer } from './fake-apiserver.ts';
22
+ import { readKvValue } from './credentials.ts';
23
+ import { type FakeCall, type FakeHandler, fakeSpawner } from './fake-process.ts';
24
+ import { buildAttrs } from './kubeconfig-attrs.ts';
25
+ import { tmpdir } from 'node:os';
26
+
27
+ const b64 = (text: string) => Buffer.from(text).toString('base64');
28
+ const begin = (label: string) => `-----${'BEGIN'} ${label}-----`;
29
+ const certBody = b64('not-a-cert');
30
+ const keyBody = b64('not-a-key');
31
+ const pemCert = `${begin('CERTIFICATE')}\n${certBody}\n`;
32
+ const pemKey = `${begin('PRIVATE KEY')}\n${keyBody}\n`;
33
+ const yaml = `apiVersion: v1
34
+ kind: Config
35
+ clusters:
36
+ - name: hf-c1
37
+ cluster: { server: 'https://192.0.2.50:6443', certificate-authority-data: ${b64(pemCert)} }
38
+ contexts:
39
+ - name: admin@hf-c1
40
+ context: { cluster: hf-c1, user: admin@hf-c1 }
41
+ users:
42
+ - name: admin@hf-c1
43
+ user: { client-certificate-data: ${b64(pemCert)}, client-key-data: ${b64(pemKey)} }
44
+ `;
45
+ const config = {
46
+ c1: { context: 'admin@hf-c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
47
+ };
48
+ const vault: FakeHandler = () => ({
49
+ stdout: JSON.stringify({ data: { data: { kubeconfig: yaml } } }),
50
+ });
51
+ const spawner = (handler: FakeHandler, calls: FakeCall[] = []) =>
52
+ Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, fakeSpawner(handler, calls));
53
+ const provide = <A, E>(
54
+ effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
55
+ handler: FakeHandler,
56
+ calls: FakeCall[] = [],
57
+ ) => Effect.runPromise(Effect.provide(effect, spawner(handler, calls)));
58
+ // ⚠️ EVERY ENCODING THE FIXTURE STORES, not just the PEM text: the kubeconfig carries
59
+ // `client-key-data: b64(pemKey)` and the PEM body is itself base64, so the stored field is b64 of
60
+ // a base64-wrapped body. Needles: the PEM markers and bodies, b64 of each PEM and of each body,
61
+ // and the raw YAML field values and whole document (an echoed document or field cannot slip by).
62
+ const fieldValues = [b64(pemCert), b64(pemKey)];
63
+ const needles = [
64
+ begin('CERTIFICATE'),
65
+ begin('PRIVATE KEY'),
66
+ certBody,
67
+ keyBody,
68
+ b64(certBody),
69
+ b64(keyBody),
70
+ b64(begin('PRIVATE KEY')),
71
+ ...fieldValues,
72
+ yaml,
73
+ ];
74
+ const leaks = (text: string) => needles.filter((needle) => text.includes(needle));
75
+
76
+ test('the persisted Connection is { kind, uid } and carries no vault path', () => {
77
+ // ★ buildAttrs persists no connection at all (kubeconfig-attrs.ts): only the identity resource
78
+ // does, and its connection is a function of the cluster name and uid, never mount/key/context.
79
+ const attrs = buildAttrs(yaml, {
80
+ context: config.c1.context,
81
+ node: '198.51.100.10',
82
+ target: { cluster: 'c1', mount: config.c1.mount },
83
+ });
84
+ expect(Object.keys(attrs ?? {})).not.toContain('connection');
85
+ const a = JSON.stringify(talosOpenBaoConnection('uid-c1'));
86
+ expect(a).toBe('{"auth":{"kind":"talos-openbao","uid":"uid-c1"}}');
87
+ expect(a).not.toContain('kubeconfig');
88
+ expect(a).not.toContain('talos-c1');
89
+ });
90
+
91
+ test('connect through the layer leaves no PEM in tagged errors, temp files or logs', async () => {
92
+ // ★ Spies, not a before/after temp-dir listing: a write that is cleaned up again, or lands
93
+ // outside tmpdir, still fails here. Anything carrying kubeconfig material is a leak.
94
+ const written: string[] = [];
95
+ const note = (data: unknown) => void written.push(String(data));
96
+ // ⚠️ credentials.ts and kubeconfig.ts use NAMED imports from node:fs, which a spy on the fs
97
+ // default export never reaches (the earlier version of this test could not fail). So the module
98
+ // itself is mocked, like fake-apiserver.ts does for node:https. mock.module is process-wide in
99
+ // bun: only the three write entry points are replaced, and the real module goes back in finally.
100
+ const realFs = { ...fs };
101
+ const fakeFs = {
102
+ ...realFs,
103
+ openSync: () => {
104
+ throw new Error('openSync is not expected on the connect path');
105
+ },
106
+ writeFileSync: (_path: unknown, data: unknown) => note(data),
107
+ };
108
+ mock.module('node:fs', () => ({ ...fakeFs, default: fakeFs }));
109
+ const spies = [
110
+ spyOn(Bun, 'write').mockImplementation(((_d: unknown, data: unknown) => {
111
+ note(data);
112
+ return Promise.resolve(0);
113
+ }) as never),
114
+ ];
115
+ // ★ Effect.logDebug of raw bytes only shows when the level is "All" and a Logger captures it.
116
+ const logged: string[] = [];
117
+ const capture = Logger.layer([
118
+ Logger.make(({ message }) => void logged.push(JSON.stringify(message))),
119
+ ]);
120
+ const saved = { ...console };
121
+ for (const level of ['log', 'debug', 'info', 'warn', 'error'] as const) {
122
+ console[level] = (...args: unknown[]) => void logged.push(args.map(String).join(' '));
123
+ }
124
+ const api = fakeApiServer({ '192.0.2.50': 'uid-c1' });
125
+ try {
126
+ const run = (handler: FakeHandler) =>
127
+ Effect.runPromise(
128
+ Effect.gen(function* () {
129
+ const adapter = yield* ClusterAdapter('talos-openbao');
130
+ return yield* adapter.connect(talosOpenBaoConnection('uid-c1'));
131
+ }).pipe(
132
+ Effect.provide(TalosOpenBaoAdapter(config).pipe(Layer.provide(spawner(handler)))),
133
+ Effect.provide(capture),
134
+ Effect.provideService(References.MinimumLogLevel, 'All'),
135
+ ),
136
+ );
137
+ // ★ The success path also gets the whole document on stderr: a regression that logs or
138
+ // surfaces bao's stderr on success shows up in `logged` below. The transport legitimately
139
+ // holds the client key (inside `clientCert`, by contract), so the raw document is barred from
140
+ // the whole transport and the PEM markers from everything but `clientCert`.
141
+ const transport = await run(() => ({ ...vault({} as FakeCall), stderr: yaml }));
142
+ const { clientCert, ...rest } = transport;
143
+ // ⚠️ JSON.stringify ESCAPES newlines, so the needle must be the escaped form: a raw-text needle
144
+ // can never match a multi-line document and the assertion passes vacuously (measured
145
+ // 2026-10-06: a transport leaking the whole document in `clientCert` passed).
146
+ const escaped = (text: string) => JSON.stringify(text).slice(1, -1);
147
+ expect(JSON.stringify(transport)).not.toContain(escaped(yaml));
148
+ expect(JSON.stringify(transport)).not.toContain(escaped(yaml.split('\n')[0] ?? ''));
149
+ expect(JSON.stringify(transport)).not.toContain('apiVersion');
150
+ expect(JSON.stringify(rest)).not.toContain(begin('PRIVATE KEY'));
151
+ expect(JSON.stringify(rest)).not.toContain(begin('CERTIFICATE'));
152
+ expect(clientCert?.key).toContain(begin('PRIVATE KEY'));
153
+ const attempts = [
154
+ // ★ stdout carries the whole document on the failing exit: only stderr may surface.
155
+ run(() => ({
156
+ exitCode: 2,
157
+ stderr: 'No value found at talos-c1/data/kubeconfig',
158
+ stdout: yaml,
159
+ })),
160
+ run(() => ({ stdout: `${yaml} not json` })),
161
+ run(() => ({ stdout: JSON.stringify({ data: { data: { kubeconfig: pemKey } } }) })),
162
+ ];
163
+ const failures = await Promise.all(attempts.map((attempt) => attempt.catch((e: unknown) => e)));
164
+ expect(failures[0]).toBeInstanceOf(TalosVaultKeyMissing);
165
+ for (const failure of failures) {
166
+ const text = JSON.stringify(failure, Object.getOwnPropertyNames(failure as object));
167
+ expect(leaks(`${text}${String(failure)}`)).toEqual([]);
168
+ }
169
+ } finally {
170
+ Object.assign(console, saved);
171
+ api.restore();
172
+ for (const spy of spies) spy.mockRestore();
173
+ mock.module('node:fs', () => ({ ...realFs, default: realFs }));
174
+ }
175
+ expect(leaks(logged.join('\n'))).toEqual([]);
176
+ expect(written).toEqual([]);
177
+ });
178
+
179
+ test('Effect.logDebug of raw bytes is caught by the capturing logger (the probe can fail)', async () => {
180
+ const logged: string[] = [];
181
+ const capture = Logger.make(({ message }) => void logged.push(JSON.stringify(message)));
182
+ await Effect.runPromise(
183
+ Effect.logDebug(pemKey).pipe(
184
+ Effect.provide(Logger.layer([capture])),
185
+ Effect.provideService(References.MinimumLogLevel, 'All'),
186
+ ),
187
+ );
188
+ expect(leaks(logged.join('\n'))).not.toEqual([]);
189
+ });
190
+
191
+ test('bao is refused without BAO_ADDR and BAO_TOKEN, and gets a minimal env', async () => {
192
+ const saved = { addr: process.env['BAO_ADDR'], token: process.env['BAO_TOKEN'] };
193
+ process.env['HOME_SECRET_PROBE'] = 'x';
194
+ try {
195
+ delete process.env['BAO_TOKEN'];
196
+ const calls: FakeCall[] = [];
197
+ await expect(
198
+ provide(readKvValue('talos-c1', 'kubeconfig', ['kubeconfig']), vault, calls),
199
+ ).rejects.toThrow(/BAO_ADDR and BAO_TOKEN/);
200
+ expect(calls).toEqual([]);
201
+ process.env['BAO_TOKEN'] = saved.token ?? 'fake-test-token';
202
+ const cmds: Record<string, unknown>[] = [];
203
+ const layer = ChildProcessSpawner.make((cmd) => {
204
+ if (cmd._tag === 'StandardCommand') cmds.push({ ...cmd.options });
205
+ return fakeSpawner(vault).spawn(cmd);
206
+ });
207
+ await Effect.runPromise(
208
+ Effect.provideService(
209
+ readKvValue('talos-c1', 'kubeconfig', ['kubeconfig']),
210
+ ChildProcessSpawner.ChildProcessSpawner,
211
+ layer,
212
+ ),
213
+ );
214
+ const env = cmds[0]?.['env'] as Record<string, string>;
215
+ expect(cmds[0]?.['extendEnv']).toBe(false);
216
+ expect(Object.keys(env)).not.toContain('HOME_SECRET_PROBE');
217
+ } finally {
218
+ delete process.env['HOME_SECRET_PROBE'];
219
+ if (saved.token === undefined) delete process.env['BAO_TOKEN'];
220
+ else process.env['BAO_TOKEN'] = saved.token;
221
+ }
222
+ });
223
+
224
+ test('non-JSON bao output never quotes its bytes', async () => {
225
+ const error = await provide(readKvValue('talos-c1', 'kubeconfig', ['kubeconfig']), () => ({
226
+ stdout: `${pemKey}{`,
227
+ })).catch((e: unknown) => e);
228
+ const message = String((error as Error).message);
229
+ expect(message).toContain('not JSON');
230
+ expect(leaks(message)).toEqual([]);
231
+ expect(message).not.toContain('Unexpected');
232
+ });
233
+
234
+ test('connect fails with the typed adapter error when the kind mismatches', async () => {
235
+ const outcome = await provide(
236
+ connectTalosOpenBao(config, { auth: { kind: 'token', token: 'not-a-token' } }).pipe(
237
+ Effect.catchTag('TalosOpenBaoAuthKind', () => Effect.succeed('mismatch')),
238
+ ),
239
+ vault,
240
+ );
241
+ expect(outcome).toBe('mismatch');
242
+ expect(existsSync(`${tmpdir()}/talos-openbao`)).toBe(false);
243
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared fixtures for the `talos-openbao` adapter tests that go THROUGH the real upstream
3
+ * `ManifestProvider` / `HelmChartProvider`: a fake `bao` vault, per-cluster kubeconfigs, the
4
+ * uid-pinned adapter config and the provider runners. The fake apiserver is `fake-apiserver.ts`.
5
+ */
6
+ import { HelmChart, HelmChartProvider } from 'alchemy/Kubernetes/HelmChart';
7
+ import { Manifest, ManifestProvider } from 'alchemy/Kubernetes/Manifest';
8
+ import * as Provider from 'alchemy/Provider';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
12
+ import { TalosOpenBaoAdapter } from './cluster-adapter.ts';
13
+ import { type FakeCall, type FakeHandler, fakeSpawner } from './fake-process.ts';
14
+
15
+ const b64 = (text: string) => Buffer.from(text).toString('base64');
16
+ const pem = (label: string) => `-----${'BEGIN'} ${label}-----\n${b64(label)}\n`;
17
+ const kubeconfig = (name: string, server: string) => `apiVersion: v1
18
+ kind: Config
19
+ clusters:
20
+ - name: ${name}
21
+ cluster: { server: '${server}', certificate-authority-data: ${b64(pem('CERTIFICATE'))} }
22
+ contexts:
23
+ - name: admin@${name}
24
+ context: { cluster: ${name}, user: admin@${name} }
25
+ users:
26
+ - name: admin@${name}
27
+ user: { client-certificate-data: ${b64(pem('CERTIFICATE'))}, client-key-data: ${b64(pem('PRIVATE KEY'))} }
28
+ `;
29
+
30
+ /** Fake `bao kv get`: `talos-<name>` answers a kubeconfig for `<name>.cluster.invalid`. */
31
+ export const vault: FakeHandler = (call: FakeCall) => {
32
+ const path = call.args.at(-1) ?? '';
33
+ const [mount] = path.split('/');
34
+ if (mount === 'talos-gone') {
35
+ return { exitCode: 2, stderr: 'No value found at talos-gone/data/kubeconfig' };
36
+ }
37
+ const name = mount?.replace('talos-', '') ?? '';
38
+ return {
39
+ stdout: JSON.stringify({
40
+ data: { data: { kubeconfig: kubeconfig(name, `https://${name}.cluster.invalid:6443`) } },
41
+ }),
42
+ };
43
+ };
44
+
45
+ type Entry = {
46
+ readonly context: string;
47
+ readonly key: string;
48
+ readonly mount: string;
49
+ readonly uid: string;
50
+ readonly retired?: boolean;
51
+ };
52
+ export type Config = Readonly<Record<string, Entry>>;
53
+
54
+ export const config = {
55
+ c1: { context: 'admin@c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
56
+ c2: { context: 'admin@c2', key: 'kubeconfig', mount: 'talos-c2', uid: 'uid-c2' },
57
+ gone: {
58
+ context: 'admin@gone',
59
+ key: 'kubeconfig',
60
+ mount: 'talos-gone',
61
+ retired: true,
62
+ uid: 'uid-gone',
63
+ },
64
+ } satisfies Config;
65
+ /** Each physical cluster's kube-system uid, by apiserver host. */
66
+ export const UIDS = { 'c1.cluster.invalid': 'uid-c1', 'c2.cluster.invalid': 'uid-c2' };
67
+
68
+ const layer = (adapterConfig: Config) =>
69
+ Layer.mergeAll(
70
+ ManifestProvider(),
71
+ HelmChartProvider(),
72
+ TalosOpenBaoAdapter(adapterConfig).pipe(
73
+ Layer.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, fakeSpawner(vault))),
74
+ ),
75
+ );
76
+
77
+ /** The layer an engine-level test provides: upstream providers plus the adapter. */
78
+ export const providerLayer = layer;
79
+
80
+ const withProvider = (
81
+ resource: { Type: string },
82
+ body: (service: never) => Effect.Effect<unknown, unknown, never>,
83
+ adapterConfig: Config,
84
+ ) =>
85
+ Effect.runPromise(
86
+ Effect.gen(function* () {
87
+ const service = yield* Provider.Provider<never>(resource.Type as never);
88
+ return yield* body(service as never);
89
+ }).pipe(Effect.provide(layer(adapterConfig))) as unknown as Effect.Effect<unknown, unknown>,
90
+ );
91
+
92
+ type Body<R extends Manifest | HelmChart> = (
93
+ s: Provider.ProviderService<R>,
94
+ ) => Effect.Effect<unknown, unknown, never>;
95
+ export const manifestApi = (body: Body<Manifest>, adapterConfig: Config = config) =>
96
+ withProvider(Manifest, body as never, adapterConfig) as Promise<unknown>;
97
+ export const helmApi = (body: Body<HelmChart>, adapterConfig: Config = config) =>
98
+ withProvider(HelmChart, body as never, adapterConfig) as Promise<unknown>;
99
+
100
+ export const session = { note: () => Effect.void } as never;
101
+
102
+ export const manifestRow = (auth: { cluster?: string; uid?: string }, name = 'ns-c1') => ({
103
+ apiVersion: 'v1',
104
+ connection: { auth: { kind: 'talos-openbao', ...auth } },
105
+ kind: 'Namespace',
106
+ name,
107
+ namespace: undefined,
108
+ ref: { apiVersion: 'v1', kind: 'Namespace', name },
109
+ uid: 'u1',
110
+ });
111
+ export const helmRow = (auth: { uid?: string }) => ({
112
+ chart: 'oci://example.invalid/chart',
113
+ code: { hash: 'h' },
114
+ connection: { auth: { kind: 'talos-openbao', ...auth } },
115
+ namespace: 'default',
116
+ objects: [{ apiVersion: 'v1', kind: 'Namespace', name: 'ns-c1' }],
117
+ releaseName: 'r',
118
+ version: '1',
119
+ });
120
+
121
+ export const failure = (run: () => Promise<unknown>) =>
122
+ run().then(
123
+ () => 'resolved',
124
+ (error: unknown) => String(error),
125
+ );
@@ -0,0 +1,240 @@
1
+ /**
2
+ * `talos-openbao` connect, offline. Fake `bao` only — no vault, no cluster, no kubeconfig on disk.
3
+ */
4
+ import type { ClusterLike, Connection } from 'alchemy/Kubernetes/Connection';
5
+ import { ClusterAdapter, ClusterNotFoundError } from 'alchemy/Kubernetes/ClusterAdapter';
6
+ import { type HelmChart } from 'alchemy/Kubernetes/HelmChart';
7
+ import { type Manifest } from 'alchemy/Kubernetes/Manifest';
8
+ import { expect, test } from 'bun:test';
9
+ import * as Effect from 'effect/Effect';
10
+ import * as Layer from 'effect/Layer';
11
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
12
+ import {
13
+ TalosOpenBaoAdapter,
14
+ connectTalosOpenBao,
15
+ talosOpenBaoConnection,
16
+ } from './cluster-adapter.ts';
17
+ import { fakeApiServer } from './fake-apiserver.ts';
18
+ import { type FakeCall, type FakeHandler, fakeSpawner } from './fake-process.ts';
19
+ import { buildAttrs } from './kubeconfig-attrs.ts';
20
+ import type { ClusterHealthAttributes } from './talos-cluster-health.ts';
21
+
22
+ type HealthIsClusterLike = ClusterHealthAttributes extends ClusterLike ? true : never;
23
+ const healthIsClusterLike: HealthIsClusterLike = true;
24
+
25
+ const beginLine = (label: string) => `-----${'BEGIN'} ${label}-----`;
26
+ const armor = (label: string, body: string) =>
27
+ `${beginLine(label)}\n${body}\n-----${'END'} ${label}-----\n`;
28
+ const b64 = (text: string) => Buffer.from(text).toString('base64');
29
+ // Fake bodies computed at runtime (base64 of fixed words) so no literal looks like a credential to gitleaks.
30
+ const pemCert = armor('CERTIFICATE', b64('not-a-cert'));
31
+ const pemKey = armor('PRIVATE KEY', b64('not-a-key'));
32
+
33
+ /** The raw document contains the PEM markers (comments) and their base64 payloads. */
34
+ const kubeconfigYaml = `# ${beginLine('CERTIFICATE')}
35
+ # ${beginLine('PRIVATE KEY')}
36
+ apiVersion: v1
37
+ kind: Config
38
+ clusters:
39
+ - name: hf-c1
40
+ cluster:
41
+ server: https://192.0.2.50:6443
42
+ certificate-authority-data: ${b64(pemCert)}
43
+ contexts:
44
+ - name: admin@hf-c1
45
+ context: { cluster: hf-c1, user: admin@hf-c1 }
46
+ users:
47
+ - name: admin@hf-c1
48
+ user:
49
+ client-certificate-data: ${b64(pemCert)}
50
+ client-key-data: ${b64(pemKey)}
51
+ current-context: admin@hf-c1
52
+ `;
53
+
54
+ const config = {
55
+ c1: { context: 'admin@hf-c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
56
+ };
57
+ const connection = talosOpenBaoConnection('uid-c1');
58
+
59
+ const baoGet =
60
+ (stdout: string): FakeHandler =>
61
+ (call) => {
62
+ if (call.command !== 'bao' || call.args[1] !== 'get') {
63
+ throw new Error(`unexpected call ${call.command} ${call.args.join(' ')}`);
64
+ }
65
+ return { stdout };
66
+ };
67
+
68
+ const provideBao = (handler: FakeHandler, calls: FakeCall[]) =>
69
+ Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, fakeSpawner(handler, calls));
70
+
71
+ /** Through the registered adapter, so a missing layer cannot pass this test. */
72
+ const runAdapter = (target: Connection, handler: FakeHandler, calls: FakeCall[] = []) =>
73
+ Effect.runPromise(
74
+ Effect.gen(function* () {
75
+ const adapter = yield* ClusterAdapter('talos-openbao');
76
+ return yield* adapter.connect(target);
77
+ }).pipe(
78
+ Effect.provide(TalosOpenBaoAdapter(config).pipe(Layer.provide(provideBao(handler, calls)))),
79
+ ),
80
+ );
81
+
82
+ /** Direct connect, so `catchTag` sees the typed error channel the layer's signature widens. */
83
+ const runDirect = <A, E>(
84
+ effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
85
+ handler: FakeHandler,
86
+ calls: FakeCall[] = [],
87
+ ) =>
88
+ Effect.runPromise(
89
+ Effect.provideService(
90
+ effect,
91
+ ChildProcessSpawner.ChildProcessSpawner,
92
+ fakeSpawner(handler, calls),
93
+ ),
94
+ );
95
+
96
+ const persisted = () => {
97
+ const attrs = buildAttrs(kubeconfigYaml, {
98
+ context: 'admin@hf-c1',
99
+ node: '198.51.100.10',
100
+ target: { cluster: 'c1', mount: 'talos-c1' },
101
+ });
102
+ if (attrs === undefined) throw new Error('fixture kubeconfig did not parse');
103
+ const helm = {
104
+ chart: 'oci://quay.io/cilium/charts/cilium',
105
+ code: { hash: 'abc' },
106
+ connection,
107
+ namespace: 'kube-system',
108
+ objects: [],
109
+ releaseName: 'cilium',
110
+ version: '1.20.2',
111
+ } satisfies HelmChart['Attributes'];
112
+ const manifest = {
113
+ apiVersion: 'v1',
114
+ connection,
115
+ kind: 'Namespace',
116
+ name: 'kube-system',
117
+ namespace: 'kube-system',
118
+ ref: { apiVersion: 'v1', kind: 'Namespace', name: 'kube-system' },
119
+ uid: undefined,
120
+ } satisfies Manifest['Attributes'];
121
+ return JSON.stringify({ helm, manifest, kubeconfig: attrs });
122
+ };
123
+
124
+ test('ClusterHealth attributes are ClusterLike', () => {
125
+ expect(healthIsClusterLike).toBe(true);
126
+ });
127
+
128
+ test('HelmChart and Manifest attributes carry no PEM and no sentinel path', () => {
129
+ expect(kubeconfigYaml).toContain('BEGIN CERTIFICATE');
130
+ expect(kubeconfigYaml).toContain('PRIVATE KEY');
131
+ const encoded = persisted();
132
+ expect(encoded).not.toContain('BEGIN CERTIFICATE');
133
+ expect(encoded).not.toContain('PRIVATE KEY');
134
+ expect(encoded).not.toContain(b64(pemCert));
135
+ expect(encoded).not.toContain(b64(pemKey));
136
+ expect(encoded).not.toContain('talos-first-boot-unwired');
137
+ expect(encoded).not.toContain('client-cert');
138
+ expect(encoded).toContain('talos-openbao');
139
+ });
140
+
141
+ test('connect reads the vault in memory and returns PEM only on the transport', async () => {
142
+ const calls: FakeCall[] = [];
143
+ const api = fakeApiServer({ '192.0.2.50': 'uid-c1' });
144
+ let transport: Awaited<ReturnType<typeof runAdapter>>;
145
+ try {
146
+ transport = await runAdapter(
147
+ connection,
148
+ baoGet(JSON.stringify({ data: { data: { kubeconfig: kubeconfigYaml } } })),
149
+ calls,
150
+ );
151
+ } finally {
152
+ api.restore();
153
+ }
154
+ expect(api.seen).toEqual(['GET 192.0.2.50/api/v1/namespaces/kube-system']);
155
+ expect(calls.map((call) => call.command)).toEqual(['bao']);
156
+ expect(calls[0]?.args[1]).toBe('get');
157
+ expect(calls[0]?.stdin).toBeUndefined();
158
+ expect(transport.endpoint).toBe('https://192.0.2.50:6443');
159
+ expect(transport.clientCert?.certificate).toContain('BEGIN CERTIFICATE');
160
+ expect(transport.clientCert?.key).toContain('PRIVATE KEY');
161
+ expect(await Effect.runPromise(transport.headers)).toEqual({});
162
+ expect(JSON.stringify(connection)).not.toContain('BEGIN CERTIFICATE');
163
+ });
164
+
165
+ const absent = () => ({ exitCode: 2, stderr: 'No value found at talos-c1/data/kubeconfig' });
166
+
167
+ test('a missing key on a live cluster is the loud TalosVaultKeyMissing, not ClusterNotFound', async () => {
168
+ const message = await runDirect(
169
+ connectTalosOpenBao(config, connection).pipe(
170
+ Effect.catchTag('TalosVaultKeyMissing', (error) => Effect.succeed(error.message)),
171
+ ),
172
+ absent,
173
+ );
174
+ expect(message).toContain('talos-c1/kubeconfig');
175
+ expect(message).not.toContain('BEGIN CERTIFICATE');
176
+ let caught: unknown;
177
+ try {
178
+ await runAdapter(connection, absent);
179
+ } catch (error) {
180
+ caught = error;
181
+ }
182
+ expect(caught).not.toBeInstanceOf(ClusterNotFoundError);
183
+ expect(String(caught)).toContain('talos-c1/kubeconfig');
184
+ });
185
+
186
+ test('a retired pin is ClusterNotFoundError with no vault read', async () => {
187
+ const retired = { c1: { ...config.c1, retired: true } };
188
+ const calls: FakeCall[] = [];
189
+ const message = await runDirect(
190
+ connectTalosOpenBao(retired, connection).pipe(
191
+ Effect.catchTag('Kubernetes.ClusterNotFoundError', (error) => Effect.succeed(error.message)),
192
+ ),
193
+ absent,
194
+ calls,
195
+ );
196
+ expect(message).toContain('retired');
197
+ expect(calls).toEqual([]);
198
+ });
199
+
200
+ test('a vault denial is not reported as a missing key', async () => {
201
+ let caught: unknown;
202
+ try {
203
+ await runDirect(connectTalosOpenBao(config, connection), () => ({
204
+ exitCode: 1,
205
+ stderr: 'permission denied',
206
+ }));
207
+ } catch (error) {
208
+ caught = error;
209
+ }
210
+ expect(caught).toBeInstanceOf(Error);
211
+ expect(caught).not.toBeInstanceOf(ClusterNotFoundError);
212
+ expect(String(caught)).toContain('permission denied');
213
+ expect(String(caught)).toContain('talos-c1/kubeconfig');
214
+ });
215
+
216
+ test('an unreadable document fails without echoing PEM', async () => {
217
+ const secret = `${beginLine('PRIVATE KEY')}\nnot-a-document\n`;
218
+ const message = await runDirect(
219
+ connectTalosOpenBao(config, connection).pipe(
220
+ Effect.catchTag('TalosKubeconfigUnreadable', (error) => Effect.succeed(error.message)),
221
+ ),
222
+ baoGet(JSON.stringify({ data: { data: { kubeconfig: secret } } })),
223
+ );
224
+ expect(message).toContain('talos-c1/kubeconfig');
225
+ expect(message).toContain('admin@hf-c1');
226
+ expect(message).not.toContain('PRIVATE KEY');
227
+ });
228
+
229
+ test('a different auth kind never calls bao', async () => {
230
+ const calls: FakeCall[] = [];
231
+ const message = await runDirect(
232
+ connectTalosOpenBao(config, { auth: { kind: 'token', token: 'not-a-token' } }).pipe(
233
+ Effect.catchTag('TalosOpenBaoAuthKind', (error) => Effect.succeed(error.message)),
234
+ ),
235
+ baoGet('{}'),
236
+ calls,
237
+ );
238
+ expect(message).toContain('token');
239
+ expect(calls).toEqual([]);
240
+ });