@homeflare/alchemy 0.47.1 → 0.48.1

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 (130) 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 +116 -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 +683 -170
  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 +29 -3
  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 +45 -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 +193 -0
  58. package/src/talos/cluster-adapter-move.test.ts +131 -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 +250 -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.test.ts +44 -0
  81. package/src/talos/mint-temp-file.ts +68 -0
  82. package/src/talos/node-connect.harness.ts +78 -0
  83. package/src/talos/node-connect.test.ts +27 -0
  84. package/src/talos/node-temp.harness.ts +40 -0
  85. package/src/talos/node-temp.test.ts +26 -0
  86. package/src/talos/talos-cluster-health-declare.test.ts +54 -0
  87. package/src/talos/talos-cluster-health-literal.test.ts +69 -0
  88. package/src/talos/talos-cluster-health.test.ts +104 -18
  89. package/src/talos/talos-cluster-health.ts +46 -14
  90. package/src/talos/talos-cluster-identity.test.ts +96 -0
  91. package/src/talos/talos-cluster-identity.ts +130 -0
  92. package/src/talos/talosctl-binary.ts +150 -0
  93. package/src/talos/talosctl-probe-kill.test.ts +56 -0
  94. package/src/talos/talosctl-vetting.test.ts +212 -0
  95. package/src/talos/talosctl.test.ts +163 -0
  96. package/src/talos/talosctl.ts +133 -15
  97. package/src/talos/trust-boundary.test.ts +17 -0
  98. package/src/talos/trust-boundary.ts +45 -0
  99. package/src/talos/values.ts +5 -12
  100. package/src/talos/yaml-parse.test.ts +41 -0
  101. package/src/talos/yaml-parse.ts +28 -0
  102. package/src/verify/fake-engine.ts +10 -0
  103. package/dist/talos/fake-process.d.ts +0 -56
  104. package/dist/talos/fake-process.d.ts.map +0 -1
  105. package/dist/talos/machine-config-fixtures.d.ts +0 -60
  106. package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
  107. package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
  108. package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
  109. package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
  110. package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
  111. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
  112. package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
  113. package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
  114. package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
  115. package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
  116. package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
  117. package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
  118. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
  119. package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
  120. package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
  121. package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
  122. package/src/release/fixtures/openbao/checksums.txt +0 -83
  123. package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
  124. package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
  125. package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
  126. package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
  127. package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
  128. package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
  129. package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
  130. package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +0 -8
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Round-2 findings, each THROUGH the real engine or providers with the fake `bao` and fake
3
+ * apiserver: (1) a workload moved between two PINNED literal uids plans a replace and never
4
+ * reconciles the old cluster's objects on the new one, and an Output uid is refused at declare
5
+ * time; (2) `Drift.repair` cannot launder a moved `Talos.ClusterIdentity`; (3) a silent apiserver
6
+ * fails closed with a typed timeout instead of hanging connect.
7
+ */
8
+ import { expect, test } from 'bun:test';
9
+ import { Manifest } from 'alchemy/Kubernetes/Manifest';
10
+ import * as Output from 'alchemy/Output';
11
+ import * as Duration from 'effect/Duration';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Layer from 'effect/Layer';
14
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
15
+ import { engineOver } from '../verify/fake-engine.ts';
16
+ import { connectTalosOpenBao, talosOpenBaoConnection } from './cluster-adapter.ts';
17
+ import { UIDS, config, failure, providerLayer, vault } from './cluster-adapter.fixtures.ts';
18
+ import { readClusterUid } from './cluster-identity.ts';
19
+ import { openTransport } from './cluster-transport.ts';
20
+ import { fakeApiServer } from './fake-apiserver.ts';
21
+ import { fakeSpawner } from './fake-process.ts';
22
+ import { TalosClusterIdentity, TalosClusterIdentityProvider } from './talos-cluster-identity.ts';
23
+
24
+ const namespace = (uid: string) =>
25
+ Manifest('ns', {
26
+ cluster: talosOpenBaoConnection(uid),
27
+ manifest: { apiVersion: 'v1', kind: 'Namespace', metadata: { name: 'ns-shared' } },
28
+ });
29
+ const declare = (uid: string) =>
30
+ Effect.gen(function* () {
31
+ yield* namespace(uid);
32
+ });
33
+ const spawner = Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, fakeSpawner(vault));
34
+
35
+ test('a workload moved between two pinned uids is a replace; the old objects are never deleted on the new cluster', async () => {
36
+ const api = fakeApiServer(UIDS);
37
+ try {
38
+ const engine = engineOver(providerLayer(config));
39
+ expect((await engine.deploy(declare('uid-c1')))['ns']).toBe('create');
40
+ api.seen.length = 0;
41
+ expect((await engine.deploy(declare('uid-c2')))['ns']).toBe('replace');
42
+ const onC1 = api.seen.filter((call) => call.includes('c1.cluster.invalid'));
43
+ const deletesOnC2 = api.seen.filter(
44
+ (call) => call.startsWith('DELETE ') && call.includes('c2.cluster.invalid'),
45
+ );
46
+ // The new cluster is created on; the replaced generation is cleaned up on the OLD one.
47
+ expect(api.seen.some((call) => call.startsWith('PATCH c2.cluster.invalid'))).toBe(true);
48
+ expect(onC1.some((call) => call.startsWith('DELETE '))).toBe(true);
49
+ expect(deletesOnC2).toEqual([]);
50
+ } finally {
51
+ api.restore();
52
+ }
53
+ });
54
+
55
+ test('an Output uid is refused when the connection is declared', () => {
56
+ const output = Output.literal('uid-c1') as unknown as string;
57
+ expect(() => talosOpenBaoConnection(output)).toThrow('literal string');
58
+ });
59
+
60
+ test('Drift.repair on a moved Talos.ClusterIdentity fails with TalosClusterMoved and keeps the saved uid', async () => {
61
+ const stack = Effect.gen(function* () {
62
+ yield* TalosClusterIdentity('c1', {
63
+ context: 'admin@c1',
64
+ target: { cluster: 'c1', mount: 'talos-c1' },
65
+ });
66
+ });
67
+ const engine = engineOver(Layer.mergeAll(TalosClusterIdentityProvider(), spawner));
68
+ const first = fakeApiServer({ 'c1.cluster.invalid': 'uid-c1' });
69
+ try {
70
+ await engine.deploy(stack);
71
+ } finally {
72
+ first.restore();
73
+ }
74
+ const moved = fakeApiServer({ 'c1.cluster.invalid': 'uid-c2' });
75
+ try {
76
+ const repaired = await engine.repair(stack).then(
77
+ (rows) => JSON.stringify(rows),
78
+ (error: unknown) => String(error),
79
+ );
80
+ expect(repaired).not.toContain('"repaired"');
81
+ expect(engine.stored()).toContain('uid-c1');
82
+ expect(engine.stored()).not.toContain('uid-c2');
83
+ } finally {
84
+ moved.restore();
85
+ }
86
+ });
87
+
88
+ test('a silent apiserver fails closed with TalosClusterIdentityTimeout', async () => {
89
+ const api = fakeApiServer(UIDS, ['c1.cluster.invalid']);
90
+ try {
91
+ const read = Effect.gen(function* () {
92
+ const transport = yield* openTransport(config.c1);
93
+ return yield* readClusterUid('c1', transport, Duration.millis(50));
94
+ });
95
+ const spawn = ChildProcessSpawner.ChildProcessSpawner;
96
+ const message = await failure(() =>
97
+ Effect.runPromise(Effect.provideService(read, spawn, fakeSpawner(vault))),
98
+ );
99
+ expect(message).toContain('TalosClusterIdentityTimeout');
100
+ // The text names the identity read's own deadline inside the 10 s connect deadline.
101
+ expect(message).toContain("the identity read's own deadline, inside the 10 s connect deadline");
102
+ } finally {
103
+ api.restore();
104
+ }
105
+ });
106
+
107
+ test(
108
+ 'through connect, a silent apiserver fails with the identity timeout, not the outer deadline',
109
+ async () => {
110
+ // ⛔ The inner uid deadline (5 s) is shorter than the outer connect deadline (10 s), so a silent
111
+ // apiserver must be reported as TalosClusterIdentityTimeout, never swallowed into
112
+ // TalosOpenBaoConnectTimeout (red team, PR 355 — on a084e18 the inner deadline never fired).
113
+ const api = fakeApiServer(UIDS, ['c1.cluster.invalid']);
114
+ try {
115
+ const message = await failure(() =>
116
+ Effect.runPromise(
117
+ Effect.provideService(
118
+ connectTalosOpenBao(config, talosOpenBaoConnection('uid-c1')),
119
+ ChildProcessSpawner.ChildProcessSpawner,
120
+ fakeSpawner(vault),
121
+ ),
122
+ ),
123
+ );
124
+ expect(message).toContain('TalosClusterIdentityTimeout');
125
+ expect(message).not.toContain('TalosOpenBaoConnectTimeout');
126
+ } finally {
127
+ api.restore();
128
+ }
129
+ },
130
+ { timeout: 15000 },
131
+ );
@@ -0,0 +1,165 @@
1
+ /**
2
+ * `Kubernetes.Manifest` and `Kubernetes.HelmChart` THROUGH the registered `talos-openbao` adapter,
3
+ * with a fake `bao` and a fake apiserver (`fake-apiserver.ts` — no socket, no vault, no cluster).
4
+ * Proves the per-cluster config (each connection reaches its own endpoint, an unconfigured uid
5
+ * is refused, a retired cluster's delete is a no-op) and the IDENTITY RULE (cluster-adapter.ts):
6
+ * a saved row whose uid-pinned entry was repointed at another physical cluster is refused with NO
7
+ * write sent to either (one read-only GET proves the mismatch), while a matching uid still works and a missing uid fails closed.
8
+ * The alias-rename and engine-level cases live in cluster-adapter-rename.test.ts.
9
+ */
10
+ import { expect, test } from 'bun:test';
11
+ import type { Manifest } from 'alchemy/Kubernetes/Manifest';
12
+ import type * as Provider from 'alchemy/Provider';
13
+ import { talosOpenBaoConnection } from './cluster-adapter.ts';
14
+ import {
15
+ UIDS,
16
+ config,
17
+ failure,
18
+ helmApi,
19
+ helmRow,
20
+ manifestApi,
21
+ manifestRow,
22
+ session,
23
+ } from './cluster-adapter.fixtures.ts';
24
+ import { fakeApiServer } from './fake-apiserver.ts';
25
+
26
+ test('two clusters in one stack each reach their own endpoint, uid-checked first', async () => {
27
+ const api = fakeApiServer(UIDS);
28
+ try {
29
+ const reconcile = (cluster: string, uid: string) => (s: Provider.ProviderService<Manifest>) =>
30
+ s.reconcile({
31
+ news: {
32
+ cluster: talosOpenBaoConnection(uid),
33
+ manifest: { apiVersion: 'v1', kind: 'Namespace', metadata: { name: `ns-${cluster}` } },
34
+ },
35
+ output: undefined,
36
+ session,
37
+ } as never);
38
+ const out1 = (await manifestApi(reconcile('c1', 'uid-c1'))) as { connection: unknown };
39
+ await manifestApi(reconcile('c2', 'uid-c2'));
40
+ expect(api.seen).toEqual([
41
+ 'GET c1.cluster.invalid/api/v1/namespaces/kube-system',
42
+ 'PATCH c1.cluster.invalid/api/v1/namespaces/ns-c1',
43
+ 'GET c2.cluster.invalid/api/v1/namespaces/kube-system',
44
+ 'PATCH c2.cluster.invalid/api/v1/namespaces/ns-c2',
45
+ ]);
46
+ expect(JSON.stringify(out1.connection)).toBe(
47
+ '{"auth":{"kind":"talos-openbao","uid":"uid-c1"}}',
48
+ );
49
+ } finally {
50
+ api.restore();
51
+ }
52
+ });
53
+
54
+ test('a connection naming an unconfigured uid is refused before any request', async () => {
55
+ const api = fakeApiServer(UIDS);
56
+ try {
57
+ const message = await failure(() =>
58
+ manifestApi((s) => s.delete({ output: manifestRow({ uid: 'uid-c9' }) } as never)),
59
+ );
60
+ expect(message).toContain('no entry pinned to cluster uid uid-c9');
61
+ expect(api.seen).toEqual([]);
62
+ } finally {
63
+ api.restore();
64
+ }
65
+ });
66
+
67
+ test('delete on a retired cluster is a no-op, on a live one it reaches that cluster only', async () => {
68
+ const api = fakeApiServer(UIDS);
69
+ try {
70
+ await manifestApi((s) => s.delete({ output: manifestRow({ uid: 'uid-gone' }) } as never));
71
+ expect(api.seen).toEqual([]);
72
+ await manifestApi((s) =>
73
+ s.delete({ output: manifestRow({ uid: 'uid-c2' }, 'ns-c2') } as never),
74
+ );
75
+ expect(api.seen).toEqual([
76
+ 'GET c2.cluster.invalid/api/v1/namespaces/kube-system',
77
+ 'DELETE c2.cluster.invalid/api/v1/namespaces/ns-c2',
78
+ ]);
79
+ } finally {
80
+ api.restore();
81
+ }
82
+ });
83
+
84
+ // ★ THE CODEX SCENARIO: create with c1's config, then repoint the entry pinned to uid-c1 at c2's
85
+ // vault key and delete the saved output. Before the identity rule that sent DELETE to c2.
86
+ const repointed = { ...config, c1: { ...config.c2, uid: 'uid-c1' } };
87
+
88
+ test('a saved c1 row is refused when its entry now points at c2: no write reaches either', async () => {
89
+ const api = fakeApiServer(UIDS);
90
+ try {
91
+ const row = manifestRow({ uid: 'uid-c1' });
92
+ const message = await failure(() =>
93
+ manifestApi((s) => s.delete({ output: row } as never), repointed),
94
+ );
95
+ expect(message).toContain('TalosClusterIdentityMismatch');
96
+ expect(message).toContain('uid-c2');
97
+ expect(api.seen).toEqual(['GET c2.cluster.invalid/api/v1/namespaces/kube-system']);
98
+ } finally {
99
+ api.restore();
100
+ }
101
+ });
102
+
103
+ test('the same repoint refuses a Manifest read and a HelmChart cleanup: no write reaches either', async () => {
104
+ const api = fakeApiServer(UIDS);
105
+ try {
106
+ const read = await failure(() =>
107
+ manifestApi(
108
+ (s) => s.read?.({ output: manifestRow({ uid: 'uid-c1' }) } as never) as never,
109
+ repointed,
110
+ ),
111
+ );
112
+ expect(read).toContain('TalosClusterIdentityMismatch');
113
+ const cleanup = await failure(() =>
114
+ helmApi((s) => s.delete({ output: helmRow({ uid: 'uid-c1' }) } as never), repointed),
115
+ );
116
+ expect(cleanup).toContain('TalosClusterIdentityMismatch');
117
+ expect(api.seen.filter((call) => !call.startsWith('GET '))).toEqual([]);
118
+ expect(api.seen.every((call) => call.startsWith('GET c2.'))).toBe(true);
119
+ } finally {
120
+ api.restore();
121
+ }
122
+ });
123
+
124
+ test('a matching uid still deletes on the right cluster (Manifest and HelmChart)', async () => {
125
+ const api = fakeApiServer(UIDS);
126
+ try {
127
+ await manifestApi((s) => s.delete({ output: manifestRow({ uid: 'uid-c1' }) } as never));
128
+ await helmApi((s) => s.delete({ output: helmRow({ uid: 'uid-c1' }) } as never));
129
+ expect(api.seen.filter((call) => call.startsWith('DELETE '))).toEqual([
130
+ 'DELETE c1.cluster.invalid/api/v1/namespaces/ns-c1',
131
+ 'DELETE c1.cluster.invalid/api/v1/namespaces/ns-c1',
132
+ ]);
133
+ } finally {
134
+ api.restore();
135
+ }
136
+ });
137
+
138
+ test('a saved auth block with no uid fails closed with no request', async () => {
139
+ const api = fakeApiServer(UIDS);
140
+ try {
141
+ const message = await failure(() =>
142
+ manifestApi((s) => s.delete({ output: manifestRow({}) } as never)),
143
+ );
144
+ expect(message).toContain('TalosClusterIdentityMissing');
145
+ expect(api.seen).toEqual([]);
146
+ } finally {
147
+ api.restore();
148
+ }
149
+ });
150
+
151
+ test('a legacy row with auth.cluster is refused with no request and a remedy', async () => {
152
+ const api = fakeApiServer(UIDS);
153
+ try {
154
+ const message = await failure(() =>
155
+ manifestApi((s) =>
156
+ s.delete({ output: manifestRow({ cluster: 'c1', uid: 'uid-c1' }) } as never),
157
+ ),
158
+ );
159
+ expect(message).toContain('TalosOpenBaoLegacyAuth');
160
+ expect(message).toContain('edit the saved state');
161
+ expect(api.seen).toEqual([]);
162
+ } finally {
163
+ api.restore();
164
+ }
165
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `talos-openbao` connect refusals that must happen BEFORE any vault read: a legacy `auth.cluster`
3
+ * row, no uid, a uid no entry is pinned to, and two entries pinned to one uid. Fake `bao` only; the
4
+ * `calls` list proves the vault was never spawned.
5
+ */
6
+ import type { Connection } from 'alchemy/Kubernetes/Connection';
7
+ import { expect, test } from 'bun:test';
8
+ import * as Effect from 'effect/Effect';
9
+ import * as ChildProcessSpawner from 'effect/process/ChildProcessSpawner';
10
+ import { connectTalosOpenBao, talosOpenBaoConnection } from './cluster-adapter.ts';
11
+ import { type FakeCall, fakeSpawner } from './fake-process.ts';
12
+
13
+ const config = {
14
+ c1: { context: 'admin@hf-c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
15
+ };
16
+ const connection = talosOpenBaoConnection('uid-c1');
17
+
18
+ const run = <A, E>(
19
+ effect: Effect.Effect<A, E, ChildProcessSpawner.ChildProcessSpawner>,
20
+ calls: FakeCall[] = [],
21
+ ) =>
22
+ Effect.runPromise(
23
+ Effect.provideService(
24
+ effect,
25
+ ChildProcessSpawner.ChildProcessSpawner,
26
+ fakeSpawner(() => ({ stdout: '{}' }), calls),
27
+ ),
28
+ );
29
+
30
+ const refusedWith = (
31
+ effect: Effect.Effect<unknown, unknown, ChildProcessSpawner.ChildProcessSpawner>,
32
+ calls: FakeCall[],
33
+ ) =>
34
+ run(
35
+ effect.pipe(
36
+ Effect.catch((error) =>
37
+ Effect.succeed(error instanceof Error && '_tag' in error ? String(error._tag) : 'untagged'),
38
+ ),
39
+ ),
40
+ calls,
41
+ );
42
+
43
+ test('a legacy auth.cluster row is refused with no vault read and a remedy', async () => {
44
+ const calls: FakeCall[] = [];
45
+ const legacy = { auth: { kind: 'talos-openbao', cluster: 'c1', uid: 'uid-c1' } } as Connection;
46
+ expect(await refusedWith(connectTalosOpenBao(config, legacy), calls)).toBe(
47
+ 'TalosOpenBaoLegacyAuth',
48
+ );
49
+ expect(calls).toEqual([]);
50
+ const message = await run(
51
+ connectTalosOpenBao(config, legacy).pipe(
52
+ Effect.catchTag('TalosOpenBaoLegacyAuth', (error) => Effect.succeed(error.message)),
53
+ ),
54
+ );
55
+ expect(message).toContain('edit the saved state');
56
+ });
57
+
58
+ test('the missing-uid remedy says to pin a literal, not wire ClusterIdentity (fails on 19502c6)', async () => {
59
+ const noUid = { auth: { kind: 'talos-openbao' } } as Connection;
60
+ const message = await run(
61
+ connectTalosOpenBao(config, noUid).pipe(
62
+ Effect.catchTag('TalosClusterIdentityMissing', (error) => Effect.succeed(error.message)),
63
+ ),
64
+ );
65
+ expect(message).toContain('pin the uid');
66
+ expect(message).not.toContain('its `connection`');
67
+ });
68
+
69
+ test('a connection with no auth block is refused as a typed failure, not a raw TypeError', async () => {
70
+ const calls: FakeCall[] = [];
71
+ const noAuth = { endpoint: 'https://example.invalid' } as Connection;
72
+ expect(await refusedWith(connectTalosOpenBao(config, noAuth), calls)).toBe(
73
+ 'TalosClusterIdentityMissing',
74
+ );
75
+ expect(calls).toEqual([]);
76
+ });
77
+
78
+ test('a connection that is itself undefined or null is refused as a typed failure (fails on a7668f7: the whole-connection destructure threw)', async () => {
79
+ const calls: FakeCall[] = [];
80
+ for (const absent of [undefined, null]) {
81
+ expect(
82
+ await refusedWith(connectTalosOpenBao(config, absent as unknown as Connection), calls),
83
+ ).toBe('TalosClusterIdentityMissing');
84
+ }
85
+ expect(calls).toEqual([]);
86
+ });
87
+
88
+ test('no uid, an unknown uid and an ambiguous pin are refused before any vault read', async () => {
89
+ const calls: FakeCall[] = [];
90
+ const noUid = { auth: { kind: 'talos-openbao' } } as Connection;
91
+ expect(await refusedWith(connectTalosOpenBao(config, noUid), calls)).toBe(
92
+ 'TalosClusterIdentityMissing',
93
+ );
94
+ expect(
95
+ await refusedWith(connectTalosOpenBao(config, talosOpenBaoConnection('uid-x')), calls),
96
+ ).toBe('TalosOpenBaoUnknownCluster');
97
+ const twice = { ...config, c1b: { ...config.c1 } };
98
+ expect(await refusedWith(connectTalosOpenBao(twice, connection), calls)).toBe(
99
+ 'TalosOpenBaoAmbiguousUid',
100
+ );
101
+ expect(calls).toEqual([]);
102
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Renaming adapter configuration must be a no-op for upstream workloads, THROUGH the real
3
+ * `ManifestProvider` and `HelmChartProvider` with the fake apiserver. Upstream hashes only the auth
4
+ * block (`internal/workload.ts:91-96` `connectionIdentity`) and replaces on a difference, and a
5
+ * replace PATCHes then DELETEs the same-named object. The auth block is `{ kind, uid }`, so an
6
+ * alias, vault key, mount or context rename must change nothing it hashes.
7
+ *
8
+ * ⚠️ The engine-level plan (Plan.ts) is not driven here: it needs a state store, a stack and the
9
+ * full alchemy runtime. What decides replace-versus-update is the provider `diff` result, which
10
+ * is asserted directly; the engine only maps `undefined` to update and `replace` to a recreate.
11
+ */
12
+ import { expect, test } from 'bun:test';
13
+ import type { Manifest } from 'alchemy/Kubernetes/Manifest';
14
+ import type * as Provider from 'alchemy/Provider';
15
+ import { talosOpenBaoConnection } from './cluster-adapter.ts';
16
+ import { UIDS, manifestApi, session } from './cluster-adapter.fixtures.ts';
17
+ import { fakeApiServer } from './fake-apiserver.ts';
18
+
19
+ const props = (uid: string) => ({
20
+ cluster: talosOpenBaoConnection(uid),
21
+ manifest: { apiVersion: 'v1', kind: 'Namespace', metadata: { name: 'ns-c1' } },
22
+ });
23
+ const only = (calls: readonly string[], method: string) =>
24
+ calls.filter((call) => call.startsWith(`${method} `));
25
+
26
+ /** Same physical cluster (uid-c1) under a new alias: context, key and mount stay as they were. */
27
+ const renamed = {
28
+ moved: { context: 'admin@c1', key: 'kubeconfig', mount: 'talos-c1', uid: 'uid-c1' },
29
+ };
30
+
31
+ test('an alias rename plans no replace and sends no DELETE; update and delete reach c1 only', async () => {
32
+ const api = fakeApiServer(UIDS);
33
+ try {
34
+ const out = (await manifestApi((s: Provider.ProviderService<Manifest>) =>
35
+ s.reconcile({ news: props('uid-c1'), olds: undefined, output: undefined, session } as never),
36
+ )) as { connection: unknown };
37
+ api.seen.length = 0;
38
+
39
+ const diff = await manifestApi(
40
+ (s) =>
41
+ s.diff?.({
42
+ news: props('uid-c1'),
43
+ olds: props('uid-c1'),
44
+ output: out,
45
+ } as never) as never,
46
+ renamed,
47
+ );
48
+ expect(diff).toBeUndefined();
49
+ expect(api.seen).toEqual([]);
50
+
51
+ await manifestApi(
52
+ (s) =>
53
+ s.reconcile({
54
+ news: props('uid-c1'),
55
+ olds: props('uid-c1'),
56
+ output: out,
57
+ session,
58
+ } as never),
59
+ renamed,
60
+ );
61
+ await manifestApi((s) => s.delete({ output: out } as never), renamed);
62
+ expect(only(api.seen, 'DELETE')).toEqual(['DELETE c1.cluster.invalid/api/v1/namespaces/ns-c1']);
63
+ expect(api.seen.filter((call) => !call.includes('c1.cluster.invalid'))).toEqual([]);
64
+ expect(only(api.seen, 'PATCH')).toHaveLength(1);
65
+ expect(JSON.stringify(out.connection)).toBe('{"auth":{"kind":"talos-openbao","uid":"uid-c1"}}');
66
+ } finally {
67
+ api.restore();
68
+ }
69
+ });
70
+
71
+ test('a real move (a different uid) is a replace, never an in-place update', async () => {
72
+ const api = fakeApiServer(UIDS);
73
+ try {
74
+ const diff = await manifestApi(
75
+ (s) =>
76
+ s.diff?.({
77
+ news: props('uid-c2'),
78
+ olds: props('uid-c1'),
79
+ output: undefined,
80
+ } as never) as never,
81
+ );
82
+ expect(diff).toEqual({ action: 'replace' });
83
+ expect(api.seen).toEqual([]);
84
+ } finally {
85
+ api.restore();
86
+ }
87
+ });