@homeflare/alchemy 0.47.0 → 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.
- package/README.md +6 -32
- package/dist/talos/bao-env.d.ts +7 -0
- package/dist/talos/bao-env.d.ts.map +1 -0
- package/dist/talos/cluster-adapter-errors.d.ts +134 -0
- package/dist/talos/cluster-adapter-errors.d.ts.map +1 -0
- package/dist/talos/cluster-adapter.d.ts +115 -0
- package/dist/talos/cluster-adapter.d.ts.map +1 -0
- package/dist/talos/cluster-identity.d.ts +28 -0
- package/dist/talos/cluster-identity.d.ts.map +1 -0
- package/dist/talos/cluster-transport.d.ts +64 -0
- package/dist/talos/cluster-transport.d.ts.map +1 -0
- package/dist/talos/credentials-write.d.ts.map +1 -1
- package/dist/talos/credentials.d.ts +10 -19
- package/dist/talos/credentials.d.ts.map +1 -1
- package/dist/talos/fake-apiserver.d.ts +9 -0
- package/dist/talos/fake-apiserver.d.ts.map +1 -0
- package/dist/talos/index.d.ts +7 -0
- package/dist/talos/index.d.ts.map +1 -1
- package/dist/talos/index.js +669 -167
- package/dist/talos/index.js.map +22 -10
- package/dist/talos/kubeconfig-attrs.d.ts +14 -0
- package/dist/talos/kubeconfig-attrs.d.ts.map +1 -1
- package/dist/talos/kubeconfig-doc.d.ts +23 -0
- package/dist/talos/kubeconfig-doc.d.ts.map +1 -0
- package/dist/talos/kubeconfig.d.ts +0 -12
- package/dist/talos/kubeconfig.d.ts.map +1 -1
- package/dist/talos/literal-connection.d.ts +16 -0
- package/dist/talos/literal-connection.d.ts.map +1 -0
- package/dist/talos/machine-config-read.d.ts +1 -1
- package/dist/talos/machine-config-read.d.ts.map +1 -1
- package/dist/talos/mint-temp-file.d.ts +20 -0
- package/dist/talos/mint-temp-file.d.ts.map +1 -0
- package/dist/talos/node-temp.harness.d.ts +2 -0
- package/dist/talos/node-temp.harness.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap-etcd.d.ts +1 -1
- package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -1
- package/dist/talos/talos-cluster-health.d.ts +15 -15
- package/dist/talos/talos-cluster-health.d.ts.map +1 -1
- package/dist/talos/talos-cluster-identity.d.ts +65 -0
- package/dist/talos/talos-cluster-identity.d.ts.map +1 -0
- package/dist/talos/talosctl-binary.d.ts +43 -0
- package/dist/talos/talosctl-binary.d.ts.map +1 -0
- package/dist/talos/talosctl.d.ts +12 -15
- package/dist/talos/talosctl.d.ts.map +1 -1
- package/dist/talos/trust-boundary.d.ts +8 -0
- package/dist/talos/trust-boundary.d.ts.map +1 -0
- package/dist/talos/values.d.ts.map +1 -1
- package/dist/talos/yaml-parse.d.ts +3 -0
- package/dist/talos/yaml-parse.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +2 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/peers.md +32 -5
- package/docs/talos-openbao-adapter.md +44 -0
- package/docs/upstream-conformance-bun.md +61 -24
- package/package.json +116 -2
- package/src/talos/bao-env.ts +16 -0
- package/src/talos/bao-vetting.test.ts +97 -0
- package/src/talos/cluster-adapter-deadline.test.ts +110 -0
- package/src/talos/cluster-adapter-errors.ts +192 -0
- package/src/talos/cluster-adapter-move.test.ts +129 -0
- package/src/talos/cluster-adapter-reconcile.test.ts +165 -0
- package/src/talos/cluster-adapter-refusals.test.ts +102 -0
- package/src/talos/cluster-adapter-rename.test.ts +87 -0
- package/src/talos/cluster-adapter-secrecy.test.ts +243 -0
- package/src/talos/cluster-adapter.fixtures.ts +125 -0
- package/src/talos/cluster-adapter.test.ts +240 -0
- package/src/talos/cluster-adapter.ts +249 -0
- package/src/talos/cluster-identity.ts +63 -0
- package/src/talos/cluster-transport.ts +110 -0
- package/src/talos/credentials-write.ts +27 -5
- package/src/talos/credentials.ts +51 -45
- package/src/talos/fake-apiserver.ts +55 -0
- package/src/talos/fake-process.ts +47 -1
- package/src/talos/index.ts +32 -0
- package/src/talos/kubeconfig-adapter.test.ts +37 -0
- package/src/talos/kubeconfig-attrs.ts +12 -28
- package/src/talos/kubeconfig-doc.ts +74 -0
- package/src/talos/kubeconfig.fixtures.ts +52 -0
- package/src/talos/kubeconfig.test.ts +6 -54
- package/src/talos/kubeconfig.ts +18 -34
- package/src/talos/literal-connection.ts +59 -0
- package/src/talos/mint-temp-file.ts +56 -0
- package/src/talos/node-connect.harness.ts +78 -0
- package/src/talos/node-connect.test.ts +27 -0
- package/src/talos/node-temp.harness.ts +40 -0
- package/src/talos/node-temp.test.ts +26 -0
- package/src/talos/talos-cluster-health-declare.test.ts +54 -0
- package/src/talos/talos-cluster-health-literal.test.ts +69 -0
- package/src/talos/talos-cluster-health.test.ts +104 -18
- package/src/talos/talos-cluster-health.ts +46 -14
- package/src/talos/talos-cluster-identity.test.ts +96 -0
- package/src/talos/talos-cluster-identity.ts +130 -0
- package/src/talos/talosctl-binary.ts +150 -0
- package/src/talos/talosctl-vetting.test.ts +212 -0
- package/src/talos/talosctl.test.ts +110 -0
- package/src/talos/talosctl.ts +101 -15
- package/src/talos/trust-boundary.test.ts +17 -0
- package/src/talos/trust-boundary.ts +45 -0
- package/src/talos/values.ts +5 -12
- package/src/talos/yaml-parse.test.ts +41 -0
- package/src/talos/yaml-parse.ts +28 -0
- package/src/verify/fake-engine.ts +10 -0
- package/dist/talos/fake-process.d.ts +0 -56
- package/dist/talos/fake-process.d.ts.map +0 -1
- package/dist/talos/machine-config-fixtures.d.ts +0 -60
- package/dist/talos/machine-config-fixtures.d.ts.map +0 -1
- package/src/postgres/fixtures/pg-REL_18_6-acl.h.txt +0 -147
- package/src/postgres/fixtures/pg-REL_18_6-aclexplode-func.txt +0 -31
- package/src/postgres/fixtures/pg-REL_18_6-alter-default-privileges-synopsis.txt +0 -82
- package/src/postgres/fixtures/pg-REL_18_6-alter_role.sgml.txt +0 -44
- package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +0 -26
- package/src/postgres/fixtures/pg-REL_18_6-create_role.sgml.txt +0 -26
- package/src/postgres/fixtures/pg-REL_18_6-grant-synopsis.txt +0 -95
- package/src/postgres/fixtures/pg-REL_18_6-pg_attribute.h.txt +0 -24
- package/src/postgres/fixtures/pg-REL_18_6-pg_auth_members.h.txt +0 -62
- package/src/postgres/fixtures/pg-REL_18_6-pg_authid.h.txt +0 -70
- package/src/postgres/fixtures/pg-REL_18_6-pg_class.h.txt +0 -33
- package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +0 -67
- package/src/postgres/fixtures/pg-REL_18_6-pg_default_acl.h.txt +0 -81
- package/src/postgres/fixtures/pg-REL_18_6-pg_namespace.h.txt +0 -28
- package/src/postgres/fixtures/pg-REL_18_6-revoke-synopsis.txt +0 -124
- package/src/release/fixtures/openbao/checksums.txt +0 -83
- package/src/release/fixtures/openbao/checksums.txt.gpgsig +0 -0
- package/src/release/fixtures/openbao/darwin-assets.txt +0 -12
- package/src/release/fixtures/openbao/openbao-gpg-pub-20240618.asc +0 -109
- package/src/release/fixtures/victoria/darwin-arm64-assets.txt +0 -22
- package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +0 -2
- package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +0 -2
- package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +0 -2
- package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +0 -8
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures of the `talos-openbao` adapter — split out of cluster-adapter.ts to keep each
|
|
3
|
+
* class's own header out of that file's 250-line budget. No message names document bytes.
|
|
4
|
+
*/
|
|
5
|
+
import * as Data from 'effect/Data';
|
|
6
|
+
|
|
7
|
+
/** The connection's uid matches no adapter entry. Uids are public. */
|
|
8
|
+
export class TalosOpenBaoUnknownCluster extends Data.TaggedError('TalosOpenBaoUnknownCluster')<{
|
|
9
|
+
readonly uid: string;
|
|
10
|
+
readonly known: readonly string[];
|
|
11
|
+
}> {
|
|
12
|
+
override get message(): string {
|
|
13
|
+
return (
|
|
14
|
+
`talos-openbao adapter has no entry pinned to cluster uid ${this.uid} ` +
|
|
15
|
+
`(configured uids: ${this.known.join(', ') || 'none'}). Refused rather than reading ` +
|
|
16
|
+
"another cluster's kubeconfig. If the cluster was retired, keep its entry with " +
|
|
17
|
+
'`retired: true` and its uid until its rows are destroyed.'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Two adapter entries pin the same uid, so which vault key to read is ambiguous. */
|
|
23
|
+
export class TalosOpenBaoAmbiguousUid extends Data.TaggedError('TalosOpenBaoAmbiguousUid')<{
|
|
24
|
+
readonly uid: string;
|
|
25
|
+
readonly aliases: readonly string[];
|
|
26
|
+
}> {
|
|
27
|
+
override get message(): string {
|
|
28
|
+
return (
|
|
29
|
+
`talos-openbao adapter entries ${this.aliases.join(', ')} are all pinned to uid ` +
|
|
30
|
+
`${this.uid}. Refused instead of picking one: a uid names exactly one entry.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The saved auth block still carries the alias an earlier build of this branch persisted. */
|
|
36
|
+
export class TalosOpenBaoLegacyAuth extends Data.TaggedError('TalosOpenBaoLegacyAuth')<{
|
|
37
|
+
readonly _?: never;
|
|
38
|
+
}> {
|
|
39
|
+
override get message(): string {
|
|
40
|
+
return (
|
|
41
|
+
'talos-openbao connection carries a legacy `cluster` alias in its auth block. Refused ' +
|
|
42
|
+
'before any vault read or request, with no silent migration (old and new auth differ, so ' +
|
|
43
|
+
'upstream would PATCH then DELETE the same object). Remedy: edit the saved state ONCE so ' +
|
|
44
|
+
"each such row's `connection.auth` is `{ kind: 'talos-openbao', uid }` (drop `cluster`), " +
|
|
45
|
+
'then redeploy.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `Talos.ClusterIdentity` read a different uid than the one it published. Never an update. */
|
|
51
|
+
export class TalosClusterMoved extends Data.TaggedError('TalosClusterMoved')<{
|
|
52
|
+
readonly cluster: string;
|
|
53
|
+
readonly saved: string;
|
|
54
|
+
readonly live: string;
|
|
55
|
+
}> {
|
|
56
|
+
override get message(): string {
|
|
57
|
+
return (
|
|
58
|
+
`Talos.ClusterIdentity '${this.cluster}' published uid ${this.saved} but the vault key now ` +
|
|
59
|
+
`answers as ${this.live}. Refused, not updated: an update cannot become a replace for the ` +
|
|
60
|
+
'workloads downstream, so the new cluster would be force-applied and the old one orphaned. ' +
|
|
61
|
+
'A move is explicit: declare a NEW Talos.ClusterIdentity for the new cluster and leave this ' +
|
|
62
|
+
'one pointing at the old cluster until its workloads are destroyed.'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `bao kv get` reported the key unwritten. The message names `mount/key` and no document bytes. */
|
|
68
|
+
export class TalosVaultKeyMissing extends Data.TaggedError('TalosVaultKeyMissing')<{
|
|
69
|
+
readonly mount: string;
|
|
70
|
+
readonly key: string;
|
|
71
|
+
}> {
|
|
72
|
+
override get message(): string {
|
|
73
|
+
return (
|
|
74
|
+
`${this.mount}/${this.key}: OpenBao has no value at this key. Talos.Kubeconfig writes it ` +
|
|
75
|
+
'once at bring-up. Connect refused instead of reading a kubeconfig from disk.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Vault bytes exist but are not a kubeconfig for the pinned context. The document is not echoed. */
|
|
81
|
+
export class TalosKubeconfigUnreadable extends Data.TaggedError('TalosKubeconfigUnreadable')<{
|
|
82
|
+
readonly mount: string;
|
|
83
|
+
readonly key: string;
|
|
84
|
+
readonly context: string;
|
|
85
|
+
}> {
|
|
86
|
+
override get message(): string {
|
|
87
|
+
return (
|
|
88
|
+
`${this.mount}/${this.key}: OpenBao kubeconfig does not parse for context ${this.context}. ` +
|
|
89
|
+
'The document stayed in the vault. Connect wrote nothing to disk and nothing to state.'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The layer was asked to connect a different auth kind. */
|
|
95
|
+
export class TalosOpenBaoAuthKind extends Data.TaggedError('TalosOpenBaoAuthKind')<{
|
|
96
|
+
readonly kind: string;
|
|
97
|
+
}> {
|
|
98
|
+
override get message(): string {
|
|
99
|
+
return (
|
|
100
|
+
`talos-openbao adapter received auth kind '${this.kind}'. This adapter only connects ` +
|
|
101
|
+
'`talos-openbao`.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The saved auth block carries no `uid`, so nothing says WHICH physical cluster it meant. */
|
|
107
|
+
export class TalosClusterIdentityMissing extends Data.TaggedError('TalosClusterIdentityMissing')<{
|
|
108
|
+
readonly _?: never;
|
|
109
|
+
}> {
|
|
110
|
+
override get message(): string {
|
|
111
|
+
return (
|
|
112
|
+
'talos-openbao connection carries no uid, so it cannot say which physical cluster it ' +
|
|
113
|
+
'targets. Refused before any vault read or request: pin the uid ' +
|
|
114
|
+
'printed by `Talos.ClusterIdentity` as a literal in `TalosOpenBaoAdapter` and build ' +
|
|
115
|
+
'workloads from `talosOpenBaoConnection(<that literal>)`.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The cluster that answered is not the one the saved auth block named. Uids are public. */
|
|
121
|
+
export class TalosClusterIdentityMismatch extends Data.TaggedError('TalosClusterIdentityMismatch')<{
|
|
122
|
+
readonly cluster: string;
|
|
123
|
+
readonly expected: string;
|
|
124
|
+
readonly got: string;
|
|
125
|
+
}> {
|
|
126
|
+
override get message(): string {
|
|
127
|
+
return (
|
|
128
|
+
`talos-openbao cluster '${this.cluster}' answered as kube-system uid ${this.got}, but the ` +
|
|
129
|
+
`saved connection expects ${this.expected}. Refused before any apply or delete: the name ` +
|
|
130
|
+
'now resolves to a different physical cluster. If the original cluster is gone for good, ' +
|
|
131
|
+
'remove its rows from state deliberately.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** `GET /api/v1/namespaces/kube-system` answered without a usable `metadata.uid`. */
|
|
137
|
+
export class TalosClusterIdentityUnreadable extends Data.TaggedError(
|
|
138
|
+
'TalosClusterIdentityUnreadable',
|
|
139
|
+
)<{
|
|
140
|
+
readonly cluster: string;
|
|
141
|
+
}> {
|
|
142
|
+
override get message(): string {
|
|
143
|
+
return (
|
|
144
|
+
`talos-openbao cluster '${this.cluster}': kube-system has no readable metadata.uid. ` +
|
|
145
|
+
'Refused rather than assuming the cluster is the expected one.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The kube-system GET got no answer in time: a silent apiserver must not hang connect or plan. */
|
|
151
|
+
export class TalosClusterIdentityTimeout extends Data.TaggedError('TalosClusterIdentityTimeout')<{
|
|
152
|
+
readonly cluster: string;
|
|
153
|
+
readonly seconds: number;
|
|
154
|
+
}> {
|
|
155
|
+
override get message(): string {
|
|
156
|
+
return (
|
|
157
|
+
`talos-openbao cluster '${this.cluster}': no answer to the kube-system uid read within ` +
|
|
158
|
+
`${this.seconds}s. Refused (failed closed) rather than waiting on a silent apiserver.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The whole connect path (vault read, parse, kube-system uid read) did not finish in time. One
|
|
165
|
+
* deadline covers `bao` and the apiserver together: a hung `bao` or a silent apiserver must not
|
|
166
|
+
* hang connect or a plan. The `bao` child is killed when the deadline interrupts the read.
|
|
167
|
+
*/
|
|
168
|
+
export class TalosOpenBaoConnectTimeout extends Data.TaggedError('TalosOpenBaoConnectTimeout')<{
|
|
169
|
+
readonly cluster: string;
|
|
170
|
+
readonly seconds: number;
|
|
171
|
+
}> {
|
|
172
|
+
override get message(): string {
|
|
173
|
+
return (
|
|
174
|
+
`talos-openbao cluster '${this.cluster}': vault read and identity check did not finish ` +
|
|
175
|
+
`within ${this.seconds}s. Refused (failed closed); the bao child was killed.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A connection was built from a uid that is not a plain string known at plan time. */
|
|
181
|
+
export class TalosUidNotLiteral extends Data.TaggedError('TalosUidNotLiteral')<{
|
|
182
|
+
readonly _?: never;
|
|
183
|
+
}> {
|
|
184
|
+
override get message(): string {
|
|
185
|
+
return (
|
|
186
|
+
'talos-openbao connection uid must be a literal string taken from the adapter config, not ' +
|
|
187
|
+
'an Output (for example a Talos.ClusterIdentity attribute). An unresolved uid hides a ' +
|
|
188
|
+
"cluster change from the plan: upstream would plan an UPDATE and reconcile the old cluster's " +
|
|
189
|
+
'objects on the new one. Pin the uid in `TalosOpenBaoAdapter` and take it from there.'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
} finally {
|
|
101
|
+
api.restore();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test(
|
|
106
|
+
'through connect, a silent apiserver fails with the identity timeout, not the outer deadline',
|
|
107
|
+
async () => {
|
|
108
|
+
// ⛔ The inner uid deadline (5 s) is shorter than the outer connect deadline (10 s), so a silent
|
|
109
|
+
// apiserver must be reported as TalosClusterIdentityTimeout, never swallowed into
|
|
110
|
+
// TalosOpenBaoConnectTimeout (red team, PR 355 — on a084e18 the inner deadline never fired).
|
|
111
|
+
const api = fakeApiServer(UIDS, ['c1.cluster.invalid']);
|
|
112
|
+
try {
|
|
113
|
+
const message = await failure(() =>
|
|
114
|
+
Effect.runPromise(
|
|
115
|
+
Effect.provideService(
|
|
116
|
+
connectTalosOpenBao(config, talosOpenBaoConnection('uid-c1')),
|
|
117
|
+
ChildProcessSpawner.ChildProcessSpawner,
|
|
118
|
+
fakeSpawner(vault),
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
expect(message).toContain('TalosClusterIdentityTimeout');
|
|
123
|
+
expect(message).not.toContain('TalosOpenBaoConnectTimeout');
|
|
124
|
+
} finally {
|
|
125
|
+
api.restore();
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{ timeout: 15000 },
|
|
129
|
+
);
|
|
@@ -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
|
+
});
|