@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.
- 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/talos-openbao-adapter.md +44 -0
- package/docs/upstream-conformance-bun.md +61 -24
- package/package.json +6 -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
package/dist/talos/index.js
CHANGED
|
@@ -1,40 +1,254 @@
|
|
|
1
|
-
// src/talos/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import * as
|
|
1
|
+
// src/talos/cluster-adapter.ts
|
|
2
|
+
import {
|
|
3
|
+
ClusterAdapter,
|
|
4
|
+
ClusterNotFoundError
|
|
5
|
+
} from "alchemy/Kubernetes/ClusterAdapter";
|
|
6
|
+
import * as Effect6 from "effect/Effect";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
7
8
|
|
|
8
|
-
// src/talos/
|
|
9
|
-
import
|
|
9
|
+
// src/talos/cluster-adapter-errors.ts
|
|
10
|
+
import * as Data from "effect/Data";
|
|
11
|
+
|
|
12
|
+
class TalosOpenBaoUnknownCluster extends Data.TaggedError("TalosOpenBaoUnknownCluster") {
|
|
13
|
+
get message() {
|
|
14
|
+
return `talos-openbao adapter has no entry pinned to cluster uid ${this.uid} ` + `(configured uids: ${this.known.join(", ") || "none"}). Refused rather than reading ` + "another cluster's kubeconfig. If the cluster was retired, keep its entry with " + "`retired: true` and its uid until its rows are destroyed.";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class TalosOpenBaoAmbiguousUid extends Data.TaggedError("TalosOpenBaoAmbiguousUid") {
|
|
19
|
+
get message() {
|
|
20
|
+
return `talos-openbao adapter entries ${this.aliases.join(", ")} are all pinned to uid ` + `${this.uid}. Refused instead of picking one: a uid names exactly one entry.`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class TalosOpenBaoLegacyAuth extends Data.TaggedError("TalosOpenBaoLegacyAuth") {
|
|
25
|
+
get message() {
|
|
26
|
+
return "talos-openbao connection carries a legacy `cluster` alias in its auth block. Refused " + "before any vault read or request, with no silent migration (old and new auth differ, so " + "upstream would PATCH then DELETE the same object). Remedy: edit the saved state ONCE so " + "each such row's `connection.auth` is `{ kind: 'talos-openbao', uid }` (drop `cluster`), " + "then redeploy.";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TalosClusterMoved extends Data.TaggedError("TalosClusterMoved") {
|
|
31
|
+
get message() {
|
|
32
|
+
return `Talos.ClusterIdentity '${this.cluster}' published uid ${this.saved} but the vault key now ` + `answers as ${this.live}. Refused, not updated: an update cannot become a replace for the ` + "workloads downstream, so the new cluster would be force-applied and the old one orphaned. " + "A move is explicit: declare a NEW Talos.ClusterIdentity for the new cluster and leave this " + "one pointing at the old cluster until its workloads are destroyed.";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TalosVaultKeyMissing extends Data.TaggedError("TalosVaultKeyMissing") {
|
|
37
|
+
get message() {
|
|
38
|
+
return `${this.mount}/${this.key}: OpenBao has no value at this key. Talos.Kubeconfig writes it ` + "once at bring-up. Connect refused instead of reading a kubeconfig from disk.";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class TalosKubeconfigUnreadable extends Data.TaggedError("TalosKubeconfigUnreadable") {
|
|
43
|
+
get message() {
|
|
44
|
+
return `${this.mount}/${this.key}: OpenBao kubeconfig does not parse for context ${this.context}. ` + "The document stayed in the vault. Connect wrote nothing to disk and nothing to state.";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class TalosOpenBaoAuthKind extends Data.TaggedError("TalosOpenBaoAuthKind") {
|
|
49
|
+
get message() {
|
|
50
|
+
return `talos-openbao adapter received auth kind '${this.kind}'. This adapter only connects ` + "`talos-openbao`.";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class TalosClusterIdentityMissing extends Data.TaggedError("TalosClusterIdentityMissing") {
|
|
55
|
+
get message() {
|
|
56
|
+
return "talos-openbao connection carries no uid, so it cannot say which physical cluster it " + "targets. Refused before any vault read or request: pin the uid " + "printed by `Talos.ClusterIdentity` as a literal in `TalosOpenBaoAdapter` and build " + "workloads from `talosOpenBaoConnection(<that literal>)`.";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class TalosClusterIdentityMismatch extends Data.TaggedError("TalosClusterIdentityMismatch") {
|
|
61
|
+
get message() {
|
|
62
|
+
return `talos-openbao cluster '${this.cluster}' answered as kube-system uid ${this.got}, but the ` + `saved connection expects ${this.expected}. Refused before any apply or delete: the name ` + "now resolves to a different physical cluster. If the original cluster is gone for good, " + "remove its rows from state deliberately.";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class TalosClusterIdentityUnreadable extends Data.TaggedError("TalosClusterIdentityUnreadable") {
|
|
67
|
+
get message() {
|
|
68
|
+
return `talos-openbao cluster '${this.cluster}': kube-system has no readable metadata.uid. ` + "Refused rather than assuming the cluster is the expected one.";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class TalosClusterIdentityTimeout extends Data.TaggedError("TalosClusterIdentityTimeout") {
|
|
73
|
+
get message() {
|
|
74
|
+
return `talos-openbao cluster '${this.cluster}': no answer to the kube-system uid read within ` + `${this.seconds}s. Refused (failed closed) rather than waiting on a silent apiserver.`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class TalosOpenBaoConnectTimeout extends Data.TaggedError("TalosOpenBaoConnectTimeout") {
|
|
79
|
+
get message() {
|
|
80
|
+
return `talos-openbao cluster '${this.cluster}': vault read and identity check did not finish ` + `within ${this.seconds}s. Refused (failed closed); the bao child was killed.`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class TalosUidNotLiteral extends Data.TaggedError("TalosUidNotLiteral") {
|
|
85
|
+
get message() {
|
|
86
|
+
return "talos-openbao connection uid must be a literal string taken from the adapter config, not " + "an Output (for example a Talos.ClusterIdentity attribute). An unresolved uid hides a " + "cluster change from the plan: upstream would plan an UPDATE and reconcile the old cluster's " + "objects on the new one. Pin the uid in `TalosOpenBaoAdapter` and take it from there.";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/talos/cluster-identity.ts
|
|
91
|
+
import { readObject } from "alchemy/Kubernetes/internal/client";
|
|
92
|
+
import * as Duration from "effect/Duration";
|
|
10
93
|
import * as Effect from "effect/Effect";
|
|
94
|
+
var KUBE_SYSTEM = { apiVersion: "v1", kind: "Namespace", name: "kube-system" };
|
|
95
|
+
var UID_READ_TIMEOUT = Duration.seconds(5);
|
|
96
|
+
var readClusterUid = (cluster, transport, timeout = UID_READ_TIMEOUT) => readObject({ object: KUBE_SYSTEM, transport }).pipe(Effect.timeoutOrElse({
|
|
97
|
+
duration: timeout,
|
|
98
|
+
orElse: () => Effect.fail(new TalosClusterIdentityTimeout({ cluster, seconds: Duration.toSeconds(timeout) }))
|
|
99
|
+
}), Effect.flatMap((body) => {
|
|
100
|
+
const uid = body?.metadata?.uid;
|
|
101
|
+
return typeof uid === "string" && uid !== "" ? Effect.succeed(uid) : Effect.fail(new TalosClusterIdentityUnreadable({ cluster }));
|
|
102
|
+
}));
|
|
103
|
+
var assertClusterUid = (cluster, expected, transport) => readClusterUid(cluster, transport).pipe(Effect.flatMap((got) => got === expected ? Effect.void : Effect.fail(new TalosClusterIdentityMismatch({ cluster, expected, got }))));
|
|
104
|
+
|
|
105
|
+
// src/talos/cluster-transport.ts
|
|
106
|
+
import * as Duration3 from "effect/Duration";
|
|
107
|
+
import * as Effect5 from "effect/Effect";
|
|
108
|
+
|
|
109
|
+
// src/talos/credentials.ts
|
|
110
|
+
import * as Duration2 from "effect/Duration";
|
|
111
|
+
import * as Effect4 from "effect/Effect";
|
|
11
112
|
import * as Stream from "effect/Stream";
|
|
12
113
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
13
114
|
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
var
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
115
|
+
|
|
116
|
+
// src/talos/bao-env.ts
|
|
117
|
+
var baoEnv = () => {
|
|
118
|
+
const addr = process.env["BAO_ADDR"];
|
|
119
|
+
const token = process.env["BAO_TOKEN"];
|
|
120
|
+
if (addr === undefined || addr === "" || token === undefined || token === "")
|
|
121
|
+
return;
|
|
122
|
+
const env = { BAO_ADDR: addr, BAO_TOKEN: token };
|
|
123
|
+
for (const name of ["PATH", "BAO_NAMESPACE", "BAO_CACERT"]) {
|
|
124
|
+
const value = process.env[name];
|
|
125
|
+
if (value !== undefined && value !== "")
|
|
126
|
+
env[name] = value;
|
|
25
127
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
128
|
+
return env;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/talos/talosctl-binary.ts
|
|
132
|
+
import { accessSync, constants, lstatSync, realpathSync as realpathSync2 } from "node:fs";
|
|
133
|
+
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
134
|
+
import * as Effect2 from "effect/Effect";
|
|
135
|
+
|
|
136
|
+
// src/talos/trust-boundary.ts
|
|
137
|
+
import { realpathSync } from "node:fs";
|
|
138
|
+
var boundaries = new Set;
|
|
139
|
+
var isTrustedBoundary = (dir) => {
|
|
140
|
+
try {
|
|
141
|
+
return boundaries.has(realpathSync(dir));
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var seamSpecifier = "./trust-boundary" + ".seam.ts";
|
|
147
|
+
import(seamSpecifier).then((seam) => {
|
|
148
|
+
boundaries = seam.boundaries;
|
|
149
|
+
}, () => {});
|
|
150
|
+
|
|
151
|
+
// src/talos/talosctl-binary.ts
|
|
152
|
+
var DEFAULT_TALOSCTL_BINARY = "talosctl";
|
|
153
|
+
var TALOSCTL_BINARY_ENV = "HF_TALOSCTL";
|
|
154
|
+
var DEFAULT_BAO_BINARY = "bao";
|
|
155
|
+
var BAO_BINARY_ENV = "HF_BAO";
|
|
156
|
+
|
|
157
|
+
class TalosBinaryRefused extends Error {
|
|
158
|
+
command;
|
|
159
|
+
binary;
|
|
160
|
+
constructor(command, detail, binary = DEFAULT_TALOSCTL_BINARY) {
|
|
161
|
+
super(`${binary} ${command} refused: ${detail}`);
|
|
162
|
+
this.name = "TalosBinaryRefused";
|
|
163
|
+
this.command = command;
|
|
164
|
+
this.binary = binary;
|
|
32
165
|
}
|
|
33
|
-
|
|
166
|
+
}
|
|
167
|
+
var resolveDefault = (binary) => Effect2.try({
|
|
168
|
+
try: () => {
|
|
169
|
+
for (const entry of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
170
|
+
if (!isAbsolute(entry))
|
|
171
|
+
continue;
|
|
172
|
+
try {
|
|
173
|
+
const candidate = join(entry, binary);
|
|
174
|
+
accessSync(candidate, constants.X_OK);
|
|
175
|
+
return realpathSync2(candidate);
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
throw new Error("not found");
|
|
179
|
+
},
|
|
180
|
+
catch: () => new TalosBinaryRefused("binary check", `${binary} was not found on PATH`, binary)
|
|
34
181
|
});
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
182
|
+
var untrustedAncestor = (file, uid) => {
|
|
183
|
+
let dir = realpathSync2(dirname(file));
|
|
184
|
+
for (;; ) {
|
|
185
|
+
const stat = lstatSync(dir);
|
|
186
|
+
const isBoundary = isTrustedBoundary(dir);
|
|
187
|
+
if (uid !== undefined && stat.uid !== uid && stat.uid !== 0) {
|
|
188
|
+
return `lives under a directory owned by another user (${dir})`;
|
|
189
|
+
}
|
|
190
|
+
const sticky = (stat.mode & 512) !== 0;
|
|
191
|
+
if ((stat.mode & 18) !== 0 && !sticky) {
|
|
192
|
+
return `lives in a group- or world-writable directory (${dir})`;
|
|
193
|
+
}
|
|
194
|
+
const up = dirname(dir);
|
|
195
|
+
if (up === dir || isBoundary)
|
|
196
|
+
return;
|
|
197
|
+
dir = up;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var checkBinaryPath = (binary, source) => {
|
|
201
|
+
const refuse = (why) => Effect2.fail(new TalosBinaryRefused("binary check", `${source} ${why}`, binary));
|
|
202
|
+
if (!isAbsolute(binary))
|
|
203
|
+
return refuse("must be an absolute path");
|
|
204
|
+
return Effect2.try({
|
|
205
|
+
try: () => {
|
|
206
|
+
const canonical = join(realpathSync2(dirname(binary)), basename(binary));
|
|
207
|
+
const file = lstatSync(canonical);
|
|
208
|
+
const uid = process.getuid?.();
|
|
209
|
+
return { canonical, file, uid, ancestor: untrustedAncestor(canonical, uid) };
|
|
210
|
+
},
|
|
211
|
+
catch: () => new TalosBinaryRefused("binary check", `${source} is not a readable file`, binary)
|
|
212
|
+
}).pipe(Effect2.flatMap(({ canonical, file, uid, ancestor }) => {
|
|
213
|
+
if (file.isSymbolicLink())
|
|
214
|
+
return refuse("must not be a symlink");
|
|
215
|
+
if (!file.isFile())
|
|
216
|
+
return refuse("is not a regular file");
|
|
217
|
+
if (uid === undefined)
|
|
218
|
+
return refuse("cannot be vetted: no process uid to check ownership");
|
|
219
|
+
if (ancestor !== undefined)
|
|
220
|
+
return refuse(ancestor);
|
|
221
|
+
if (uid !== undefined && file.uid !== uid && file.uid !== 0) {
|
|
222
|
+
return refuse("must be owned by the current user or root");
|
|
223
|
+
}
|
|
224
|
+
if ((file.mode & 18) !== 0)
|
|
225
|
+
return refuse("is group- or world-writable");
|
|
226
|
+
try {
|
|
227
|
+
accessSync(canonical, constants.X_OK);
|
|
228
|
+
} catch {
|
|
229
|
+
return refuse("is not executable");
|
|
230
|
+
}
|
|
231
|
+
return Effect2.succeed(canonical);
|
|
232
|
+
}));
|
|
233
|
+
};
|
|
234
|
+
var resolveBao = () => {
|
|
235
|
+
const override = process.env[BAO_BINARY_ENV]?.trim();
|
|
236
|
+
const requested = override !== undefined && override !== "" ? override : DEFAULT_BAO_BINARY;
|
|
237
|
+
const source = requested === DEFAULT_BAO_BINARY ? "bao on PATH" : BAO_BINARY_ENV;
|
|
238
|
+
const resolved = requested === DEFAULT_BAO_BINARY ? resolveDefault(requested) : Effect2.succeed(requested);
|
|
239
|
+
return Effect2.flatMap(resolved, (path) => checkBinaryPath(path, source));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/talos/mint-temp-file.ts
|
|
243
|
+
import { closeSync, openSync, writeFileSync } from "node:fs";
|
|
244
|
+
import { unlink } from "node:fs/promises";
|
|
245
|
+
import { randomUUID } from "node:crypto";
|
|
246
|
+
import { tmpdir } from "node:os";
|
|
247
|
+
import { join as join2 } from "node:path";
|
|
248
|
+
import * as Effect3 from "effect/Effect";
|
|
249
|
+
var mintKvTempFile = (raw, label) => Effect3.gen(function* () {
|
|
250
|
+
const path = join2(tmpdir(), `hf-talos-${label}-${randomUUID()}.yaml`);
|
|
251
|
+
yield* Effect3.acquireRelease(Effect3.try({
|
|
38
252
|
try: () => {
|
|
39
253
|
const fd = openSync(path, "wx", 384);
|
|
40
254
|
try {
|
|
@@ -44,46 +258,226 @@ var mintKvTempFile = (raw, label) => Effect.gen(function* () {
|
|
|
44
258
|
}
|
|
45
259
|
},
|
|
46
260
|
catch: (cause) => new Error(`writing ${label} temp file: ${String(cause)}`)
|
|
47
|
-
}), () =>
|
|
261
|
+
}), () => Effect3.tryPromise({ try: () => unlink(path), catch: () => {
|
|
48
262
|
return;
|
|
49
|
-
} }).pipe(
|
|
263
|
+
} }).pipe(Effect3.orElseSucceed(() => {
|
|
50
264
|
return;
|
|
51
265
|
})));
|
|
52
266
|
return { path };
|
|
53
267
|
});
|
|
268
|
+
|
|
269
|
+
// src/talos/credentials.ts
|
|
270
|
+
var DEFAULT_TALOSCONFIG_KEY = "talosconfig";
|
|
271
|
+
var DEFAULT_KUBECONFIG_KEY = "kubeconfig";
|
|
272
|
+
var readKvValue = (mount, key, fields) => Effect4.gen(function* () {
|
|
273
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
274
|
+
const bao = yield* resolveBao();
|
|
275
|
+
const env = baoEnv();
|
|
276
|
+
if (env === undefined) {
|
|
277
|
+
return yield* Effect4.fail(new Error(`bao kv get ${mount}/${key} refused: BAO_ADDR and BAO_TOKEN must both be set. ` + "Without them bao would fall back to a cached login in the operator's home."));
|
|
278
|
+
}
|
|
279
|
+
const result = yield* ChildProcess.make(bao, ["kv", "get", "-format=json", `${mount}/${key}`], {
|
|
280
|
+
env,
|
|
281
|
+
extendEnv: false,
|
|
282
|
+
forceKillAfter: Duration2.seconds(1),
|
|
283
|
+
stderr: "pipe",
|
|
284
|
+
stdin: "ignore",
|
|
285
|
+
stdout: "pipe"
|
|
286
|
+
}).pipe(spawner.spawn, Effect4.flatMap((child) => Effect4.all({
|
|
287
|
+
exitCode: child.exitCode,
|
|
288
|
+
stderr: child.stderr.pipe(Stream.decodeText, Stream.mkString, Effect4.map((text) => text.trim())),
|
|
289
|
+
stdout: child.stdout.pipe(Stream.decodeText, Stream.mkString, Effect4.map((text) => text.trim()))
|
|
290
|
+
}, { concurrency: "unbounded" })), Effect4.scoped);
|
|
291
|
+
if (result.exitCode !== 0) {
|
|
292
|
+
return yield* Effect4.fail(new Error(`bao kv get ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
|
|
293
|
+
}
|
|
294
|
+
let parsed;
|
|
295
|
+
try {
|
|
296
|
+
parsed = JSON.parse(result.stdout);
|
|
297
|
+
} catch {
|
|
298
|
+
return yield* Effect4.fail(new Error(`bao kv get ${mount}/${key} returned output that is not JSON (not echoed).`));
|
|
299
|
+
}
|
|
300
|
+
const data = parsed.data?.data;
|
|
301
|
+
for (const field of fields) {
|
|
302
|
+
const raw = data?.[field];
|
|
303
|
+
if (typeof raw === "string" && raw.trim() !== "")
|
|
304
|
+
return raw;
|
|
305
|
+
}
|
|
306
|
+
return yield* Effect4.fail(new Error(`${mount}/${key} returned no ${fields.join("/")} field. Never store the value itself in ` + "an Alchemy prop or attribute — fix the KV entry, not the caller."));
|
|
307
|
+
});
|
|
308
|
+
var isVaultKeyAbsent = (error) => error instanceof Error && /no value found at/i.test(error.message);
|
|
54
309
|
var mintTalosconfig = (target) => readKvValue(target.mount, target.talosconfigKey ?? DEFAULT_TALOSCONFIG_KEY, [
|
|
55
310
|
"talosconfig",
|
|
56
311
|
"config"
|
|
57
|
-
]).pipe(
|
|
312
|
+
]).pipe(Effect4.flatMap((raw) => mintKvTempFile(raw, `${target.cluster}-talosconfig`)), Effect4.map(({ path }) => ({ talosconfigPath: path })));
|
|
313
|
+
|
|
314
|
+
// src/talos/yaml-parse.ts
|
|
315
|
+
import { YAMLParseError, parse } from "yaml";
|
|
316
|
+
var parseYaml = (text) => {
|
|
317
|
+
try {
|
|
318
|
+
return parse(text, { logLevel: "error", prettyErrors: false });
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error instanceof YAMLParseError)
|
|
321
|
+
return;
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/talos/kubeconfig-doc.ts
|
|
327
|
+
var pem = (data) => {
|
|
328
|
+
if (data === undefined || data.trim() === "")
|
|
329
|
+
return;
|
|
330
|
+
const decoded = Buffer.from(data, "base64").toString("utf8");
|
|
331
|
+
return decoded.includes("BEGIN ") ? decoded : undefined;
|
|
332
|
+
};
|
|
333
|
+
var kubeconfigTransport = (yamlText, contextName) => {
|
|
334
|
+
const parsed = parseYaml(yamlText);
|
|
335
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
336
|
+
return;
|
|
337
|
+
const doc = parsed;
|
|
338
|
+
const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;
|
|
339
|
+
const clusterName = ctx?.cluster;
|
|
340
|
+
const userName = ctx?.user;
|
|
341
|
+
if (clusterName === undefined || userName === undefined)
|
|
342
|
+
return;
|
|
343
|
+
const cluster = doc.clusters?.find((row) => row.name === clusterName)?.cluster;
|
|
344
|
+
const user = doc.users?.find((row) => row.name === userName)?.user;
|
|
345
|
+
const endpoint = cluster?.server;
|
|
346
|
+
const certificateAuthorityData = cluster?.["certificate-authority-data"];
|
|
347
|
+
const certificate = pem(user?.["client-certificate-data"]);
|
|
348
|
+
const key = pem(user?.["client-key-data"]);
|
|
349
|
+
if (endpoint === undefined || certificateAuthorityData === undefined || pem(certificateAuthorityData) === undefined || certificate === undefined || key === undefined) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
return { certificateAuthorityData, clientCert: { certificate, key }, endpoint };
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/talos/cluster-transport.ts
|
|
356
|
+
var talosVaultLocation = (props) => ({
|
|
357
|
+
context: props.context,
|
|
358
|
+
key: props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY,
|
|
359
|
+
mount: props.target.mount
|
|
360
|
+
});
|
|
361
|
+
var talosOpenBaoCluster = (props, pin) => ({
|
|
362
|
+
...talosVaultLocation(props),
|
|
363
|
+
uid: pin.uid,
|
|
364
|
+
...pin.retired === true ? { retired: true } : {}
|
|
365
|
+
});
|
|
366
|
+
var CONNECT_TIMEOUT = Duration3.seconds(10);
|
|
367
|
+
var withConnectDeadline = (cluster, effect, timeout = CONNECT_TIMEOUT) => effect.pipe(Effect5.timeoutOrElse({
|
|
368
|
+
duration: timeout,
|
|
369
|
+
orElse: () => Effect5.fail(new TalosOpenBaoConnectTimeout({ cluster, seconds: Duration3.toSeconds(timeout) }))
|
|
370
|
+
}));
|
|
371
|
+
var openTransport = (config) => Effect5.gen(function* () {
|
|
372
|
+
const { context, key, mount } = config;
|
|
373
|
+
const raw = yield* readKvValue(mount, key, ["kubeconfig", "config"]).pipe(Effect5.mapError((error) => isVaultKeyAbsent(error) ? new TalosVaultKeyMissing({ key, mount }) : error));
|
|
374
|
+
const material = yield* Effect5.sync(() => kubeconfigTransport(raw, context));
|
|
375
|
+
if (material === undefined) {
|
|
376
|
+
return yield* Effect5.fail(new TalosKubeconfigUnreadable({ context, key, mount }));
|
|
377
|
+
}
|
|
378
|
+
return { ...material, headers: Effect5.succeed({}) };
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// src/talos/cluster-adapter.ts
|
|
382
|
+
var talosOpenBaoConnection = (uid) => {
|
|
383
|
+
if (typeof uid !== "string" || uid === "")
|
|
384
|
+
throw new TalosUidNotLiteral({});
|
|
385
|
+
return { auth: { kind: "talos-openbao", uid } };
|
|
386
|
+
};
|
|
387
|
+
var connectTalosOpenBao = (configs, connection, timeout = CONNECT_TIMEOUT) => Effect6.gen(function* () {
|
|
388
|
+
if (connection === undefined || connection === null) {
|
|
389
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
390
|
+
}
|
|
391
|
+
const { auth } = connection;
|
|
392
|
+
if (auth === undefined || auth === null) {
|
|
393
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
394
|
+
}
|
|
395
|
+
if (auth.kind !== "talos-openbao") {
|
|
396
|
+
return yield* Effect6.fail(new TalosOpenBaoAuthKind({ kind: auth.kind }));
|
|
397
|
+
}
|
|
398
|
+
if (Object.hasOwn(auth, "cluster")) {
|
|
399
|
+
return yield* Effect6.fail(new TalosOpenBaoLegacyAuth({}));
|
|
400
|
+
}
|
|
401
|
+
const { uid } = auth;
|
|
402
|
+
if (typeof uid !== "string" || uid === "") {
|
|
403
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
404
|
+
}
|
|
405
|
+
const matches = Object.entries(configs).filter(([, entry]) => entry.uid === uid);
|
|
406
|
+
const [match] = matches;
|
|
407
|
+
if (match === undefined) {
|
|
408
|
+
return yield* Effect6.fail(new TalosOpenBaoUnknownCluster({
|
|
409
|
+
known: Object.values(configs).map((entry) => entry.uid),
|
|
410
|
+
uid
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
if (matches.length > 1) {
|
|
414
|
+
return yield* Effect6.fail(new TalosOpenBaoAmbiguousUid({ aliases: matches.map(([alias2]) => alias2), uid }));
|
|
415
|
+
}
|
|
416
|
+
const [alias, config] = match;
|
|
417
|
+
if (config.retired === true) {
|
|
418
|
+
return yield* Effect6.fail(new ClusterNotFoundError({ message: `talos-openbao cluster '${alias}' is retired` }));
|
|
419
|
+
}
|
|
420
|
+
return yield* withConnectDeadline(alias, Effect6.gen(function* () {
|
|
421
|
+
const transport = yield* openTransport(config);
|
|
422
|
+
yield* assertClusterUid(alias, uid, transport);
|
|
423
|
+
return transport;
|
|
424
|
+
}), timeout);
|
|
425
|
+
});
|
|
426
|
+
var TalosOpenBaoAdapter = (config) => Layer.effect(ClusterAdapter("talos-openbao"), Effect6.gen(function* () {
|
|
427
|
+
const context2 = yield* Effect6.context();
|
|
428
|
+
const service = {
|
|
429
|
+
kind: "Kubernetes.ClusterAdapter",
|
|
430
|
+
connect: (connection) => connectTalosOpenBao(config, connection).pipe(Effect6.provideContext(context2))
|
|
431
|
+
};
|
|
432
|
+
return service;
|
|
433
|
+
}));
|
|
434
|
+
// src/talos/kubeconfig.ts
|
|
435
|
+
import { readFile } from "node:fs/promises";
|
|
436
|
+
import { chmodSync } from "node:fs";
|
|
437
|
+
import { Resource } from "alchemy";
|
|
438
|
+
import { isResolved } from "alchemy/Diff";
|
|
439
|
+
import * as Provider from "alchemy/Provider";
|
|
440
|
+
import * as Effect9 from "effect/Effect";
|
|
58
441
|
|
|
59
442
|
// src/talos/credentials-write.ts
|
|
60
|
-
import * as
|
|
443
|
+
import * as Effect7 from "effect/Effect";
|
|
444
|
+
import * as Duration4 from "effect/Duration";
|
|
61
445
|
import * as Stream2 from "effect/Stream";
|
|
446
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
447
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
448
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
449
|
+
import { join as join3 } from "node:path";
|
|
62
450
|
import * as ChildProcess2 from "effect/process/ChildProcess";
|
|
63
451
|
import * as ChildProcessSpawner3 from "effect/process/ChildProcessSpawner";
|
|
64
|
-
var reservedTempPath = (label) =>
|
|
65
|
-
const path =
|
|
66
|
-
yield*
|
|
452
|
+
var reservedTempPath = (label) => Effect7.gen(function* () {
|
|
453
|
+
const path = join3(tmpdir2(), `hf-talos-${label}-${randomUUID2()}.yaml`);
|
|
454
|
+
yield* Effect7.addFinalizer(() => Effect7.tryPromise({ try: () => unlink2(path), catch: () => {
|
|
67
455
|
return;
|
|
68
|
-
} }).pipe(
|
|
456
|
+
} }).pipe(Effect7.orElseSucceed(() => {
|
|
69
457
|
return;
|
|
70
458
|
})));
|
|
71
459
|
return { path };
|
|
72
460
|
});
|
|
73
|
-
var writeKvValue = (mount, key, field, value) =>
|
|
461
|
+
var writeKvValue = (mount, key, field, value) => Effect7.gen(function* () {
|
|
74
462
|
const spawner = yield* ChildProcessSpawner3.ChildProcessSpawner;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
463
|
+
const bao = yield* resolveBao();
|
|
464
|
+
const env = baoEnv();
|
|
465
|
+
if (env === undefined) {
|
|
466
|
+
return yield* Effect7.fail(new Error(`bao kv put ${mount}/${key} refused: BAO_ADDR and BAO_TOKEN must both be set. ` + "Without them bao would fall back to a cached login in the operator's home."));
|
|
467
|
+
}
|
|
468
|
+
const result = yield* ChildProcess2.make(bao, ["kv", "put", `${mount}/${key}`, `${field}=-`], {
|
|
469
|
+
env,
|
|
470
|
+
extendEnv: false,
|
|
471
|
+
forceKillAfter: Duration4.seconds(1),
|
|
78
472
|
stderr: "pipe",
|
|
79
473
|
stdin: Stream2.fromIterable([new TextEncoder().encode(value)]),
|
|
80
474
|
stdout: "pipe"
|
|
81
|
-
}).pipe(spawner.spawn,
|
|
475
|
+
}).pipe(spawner.spawn, Effect7.flatMap((child) => Effect7.all({
|
|
82
476
|
exitCode: child.exitCode,
|
|
83
|
-
stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString,
|
|
84
|
-
}, { concurrency: "unbounded" })),
|
|
477
|
+
stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString, Effect7.map((text) => text.trim()))
|
|
478
|
+
}, { concurrency: "unbounded" })), Effect7.scoped);
|
|
85
479
|
if (result.exitCode !== 0) {
|
|
86
|
-
return yield*
|
|
480
|
+
return yield* Effect7.fail(new Error(`bao kv put ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
|
|
87
481
|
}
|
|
88
482
|
});
|
|
89
483
|
|
|
@@ -93,12 +487,7 @@ var canonicalText = (text) => text.trimEnd();
|
|
|
93
487
|
var sha256 = (text) => createHash("sha256").update(canonicalText(text)).digest("hex");
|
|
94
488
|
var configDigest = (text) => sha256(text);
|
|
95
489
|
var extractMachineConfigSpec = (wrapperYaml) => {
|
|
96
|
-
let doc;
|
|
97
|
-
try {
|
|
98
|
-
doc = Bun.YAML.parse(wrapperYaml);
|
|
99
|
-
} catch {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
490
|
+
let doc = parseYaml(wrapperYaml);
|
|
102
491
|
if (Array.isArray(doc)) {
|
|
103
492
|
if (doc.length !== 1)
|
|
104
493
|
return;
|
|
@@ -110,12 +499,10 @@ var extractMachineConfigSpec = (wrapperYaml) => {
|
|
|
110
499
|
return typeof spec === "string" ? spec : undefined;
|
|
111
500
|
};
|
|
112
501
|
var kubeconfigMetadata = (yamlText, contextName) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
doc = Bun.YAML.parse(yamlText);
|
|
116
|
-
} catch {
|
|
502
|
+
const parsed = parseYaml(yamlText);
|
|
503
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
117
504
|
return;
|
|
118
|
-
|
|
505
|
+
const doc = parsed;
|
|
119
506
|
const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;
|
|
120
507
|
const clusterName = ctx?.cluster;
|
|
121
508
|
const userName = ctx?.user;
|
|
@@ -140,10 +527,6 @@ var generation = (meta) => sha256(`${meta.context}
|
|
|
140
527
|
${meta.endpoint}
|
|
141
528
|
${meta.caFingerprint}
|
|
142
529
|
${meta.clientFingerprint}`);
|
|
143
|
-
var UNWIRED_KUBECONFIG_PATH = "/talos-first-boot-unwired/wire-Kubernetes.ClusterAdapter-to-mintKubeconfig";
|
|
144
|
-
var toConnection = (props) => ({
|
|
145
|
-
auth: { context: props.context, kind: "kubeconfig", path: UNWIRED_KUBECONFIG_PATH }
|
|
146
|
-
});
|
|
147
530
|
var buildAttrs = (raw, props) => {
|
|
148
531
|
const meta = kubeconfigMetadata(raw, props.context);
|
|
149
532
|
if (meta === undefined)
|
|
@@ -151,7 +534,6 @@ var buildAttrs = (raw, props) => {
|
|
|
151
534
|
return {
|
|
152
535
|
certificateAuthorityFingerprint: meta.caFingerprint,
|
|
153
536
|
clientCertificateFingerprint: meta.clientFingerprint,
|
|
154
|
-
connection: toConnection(props),
|
|
155
537
|
context: props.context,
|
|
156
538
|
credentialGeneration: generation({ ...meta, context: props.context }),
|
|
157
539
|
endpoint: meta.endpoint
|
|
@@ -159,23 +541,64 @@ var buildAttrs = (raw, props) => {
|
|
|
159
541
|
};
|
|
160
542
|
|
|
161
543
|
// src/talos/talosctl.ts
|
|
162
|
-
import * as
|
|
544
|
+
import * as Duration5 from "effect/Duration";
|
|
545
|
+
import * as Effect8 from "effect/Effect";
|
|
163
546
|
import * as Stream3 from "effect/Stream";
|
|
164
547
|
import * as ChildProcess3 from "effect/process/ChildProcess";
|
|
165
548
|
import * as ChildProcessSpawner5 from "effect/process/ChildProcessSpawner";
|
|
166
|
-
|
|
167
549
|
class TalosError extends Error {
|
|
168
550
|
command;
|
|
169
551
|
exitCode;
|
|
170
|
-
constructor(command, exitCode, detail) {
|
|
171
|
-
super(
|
|
552
|
+
constructor(command, exitCode, detail, binary = DEFAULT_TALOSCTL_BINARY) {
|
|
553
|
+
super(`${binary} ${command} -> ${String(exitCode)}: ${detail}`);
|
|
172
554
|
this.command = command;
|
|
173
555
|
this.exitCode = exitCode;
|
|
174
556
|
this.name = "TalosError";
|
|
175
557
|
}
|
|
176
558
|
}
|
|
177
|
-
var
|
|
559
|
+
var TALOSCTL_PINNED_VERSION = "v1.14.2";
|
|
560
|
+
var talosctlBinary = (override) => {
|
|
561
|
+
const fromOption = override?.trim();
|
|
562
|
+
if (fromOption !== undefined && fromOption !== "")
|
|
563
|
+
return fromOption;
|
|
564
|
+
const fromEnv = process.env[TALOSCTL_BINARY_ENV]?.trim();
|
|
565
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
566
|
+
return fromEnv;
|
|
567
|
+
return DEFAULT_TALOSCTL_BINARY;
|
|
568
|
+
};
|
|
569
|
+
var talosctlEnv = () => {
|
|
570
|
+
const env = {};
|
|
571
|
+
for (const name of ["PATH", "HOME", "TMPDIR"]) {
|
|
572
|
+
const value = process.env[name];
|
|
573
|
+
if (value !== undefined && value !== "")
|
|
574
|
+
env[name] = value;
|
|
575
|
+
}
|
|
576
|
+
return env;
|
|
577
|
+
};
|
|
578
|
+
var capture = (binary, argv) => Effect8.gen(function* () {
|
|
178
579
|
const spawner = yield* ChildProcessSpawner5.ChildProcessSpawner;
|
|
580
|
+
return yield* ChildProcess3.make(binary, argv, {
|
|
581
|
+
env: talosctlEnv(),
|
|
582
|
+
extendEnv: false,
|
|
583
|
+
forceKillAfter: Duration5.seconds(1),
|
|
584
|
+
stderr: "pipe",
|
|
585
|
+
stdin: "ignore",
|
|
586
|
+
stdout: "pipe"
|
|
587
|
+
}).pipe(spawner.spawn, Effect8.flatMap((child) => Effect8.all({
|
|
588
|
+
exitCode: child.exitCode,
|
|
589
|
+
stderr: child.stderr.pipe(Stream3.decodeText, Stream3.mkString, Effect8.map((text) => text.trim())),
|
|
590
|
+
stdout: child.stdout.pipe(Stream3.decodeText, Stream3.mkString, Effect8.map((text) => text.trim()))
|
|
591
|
+
}, { concurrency: "unbounded" })), Effect8.scoped);
|
|
592
|
+
});
|
|
593
|
+
var reportsPinnedVersion = (stdout) => stdout.split(/[^\w.+-]+/).includes(TALOSCTL_PINNED_VERSION);
|
|
594
|
+
var checkOverrideVersion = (binary, source) => capture(binary, ["version", "--client"]).pipe(Effect8.flatMap((out) => out.exitCode === 0 && reportsPinnedVersion(out.stdout) ? Effect8.void : Effect8.fail(new TalosBinaryRefused("version --client", `${source} is not talosctl ${TALOSCTL_PINNED_VERSION} (exit ${String(out.exitCode)})`, binary))));
|
|
595
|
+
var talosctl = (args, options) => Effect8.gen(function* () {
|
|
596
|
+
const requested = talosctlBinary(options.binary);
|
|
597
|
+
const resolved = requested === DEFAULT_TALOSCTL_BINARY ? yield* resolveDefault(requested) : requested;
|
|
598
|
+
const fromOption = options.binary?.trim();
|
|
599
|
+
const source = requested === DEFAULT_TALOSCTL_BINARY ? "talosctl on PATH" : fromOption !== undefined && fromOption !== "" ? "the `binary` option" : TALOSCTL_BINARY_ENV;
|
|
600
|
+
const binary = yield* checkBinaryPath(resolved, source);
|
|
601
|
+
yield* checkOverrideVersion(binary, source);
|
|
179
602
|
const argv = [
|
|
180
603
|
...args,
|
|
181
604
|
"--talosconfig",
|
|
@@ -184,119 +607,108 @@ var talosctl = (args, options) => Effect3.gen(function* () {
|
|
|
184
607
|
...options.endpoints === undefined ? [] : ["--endpoints", options.endpoints.join(",")],
|
|
185
608
|
...options.insecure === true ? ["--insecure"] : []
|
|
186
609
|
];
|
|
187
|
-
const result = yield*
|
|
188
|
-
detached: false,
|
|
189
|
-
extendEnv: true,
|
|
190
|
-
stderr: "pipe",
|
|
191
|
-
stdin: "ignore",
|
|
192
|
-
stdout: "pipe"
|
|
193
|
-
}).pipe(spawner.spawn, Effect3.flatMap((child) => Effect3.all({
|
|
194
|
-
exitCode: child.exitCode,
|
|
195
|
-
stderr: child.stderr.pipe(Stream3.decodeText, Stream3.mkString, Effect3.map((text) => text.trim())),
|
|
196
|
-
stdout: child.stdout.pipe(Stream3.decodeText, Stream3.mkString, Effect3.map((text) => text.trim()))
|
|
197
|
-
}, { concurrency: "unbounded" })), Effect3.scoped);
|
|
610
|
+
const result = yield* capture(binary, argv);
|
|
198
611
|
const command = args.join(" ");
|
|
199
612
|
if (result.exitCode !== 0) {
|
|
200
|
-
return yield*
|
|
613
|
+
return yield* Effect8.fail(new TalosError(command, result.exitCode, result.stderr.slice(0, 300), binary));
|
|
201
614
|
}
|
|
202
615
|
return result.stdout;
|
|
203
616
|
});
|
|
204
|
-
var talosctlOrAlready = (args, options) => talosctl(args, options).pipe(
|
|
617
|
+
var talosctlOrAlready = (args, options) => talosctl(args, options).pipe(Effect8.catchIf((cause) => cause instanceof TalosError && cause.exitCode !== 0 && /already|exists|bootstrapped/i.test(cause.message), () => Effect8.succeed("")));
|
|
205
618
|
|
|
206
619
|
// src/talos/kubeconfig.ts
|
|
207
620
|
var TalosKubeconfig = Resource("Talos.Kubeconfig");
|
|
208
|
-
var
|
|
209
|
-
var
|
|
210
|
-
var readKubeconfig = (props) => Effect4.gen(function* () {
|
|
621
|
+
var readVaultMeta = (props, key) => readKvValue(props.target.mount, key, ["kubeconfig", "config"]).pipe(Effect9.catchIf(isVaultKeyAbsent, () => Effect9.succeed(undefined)));
|
|
622
|
+
var readKubeconfig = (props) => Effect9.gen(function* () {
|
|
211
623
|
const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
|
|
212
624
|
const raw = yield* readVaultMeta(props, key);
|
|
213
625
|
if (raw === undefined)
|
|
214
626
|
return;
|
|
215
627
|
const meta = buildAttrs(raw, props);
|
|
216
628
|
if (meta === undefined) {
|
|
217
|
-
return yield*
|
|
629
|
+
return yield* Effect9.fail(new Error(`${props.target.mount}/${key}: vault content exists but does not parse as a kubeconfig ` + `for context ${props.context} — not a cold start. Fix the KV entry or the pinned ` + "context rather than trusting an empty read."));
|
|
218
630
|
}
|
|
219
631
|
return meta;
|
|
220
632
|
});
|
|
221
|
-
var diffKubeconfig = (news, output) =>
|
|
633
|
+
var diffKubeconfig = (news, output) => Effect9.gen(function* () {
|
|
222
634
|
if (output === undefined || !isResolved(news))
|
|
223
635
|
return;
|
|
224
636
|
const live = yield* readKubeconfig(news);
|
|
225
|
-
if (live !== undefined && live.credentialGeneration === output.credentialGeneration && live.context === output.context && live.endpoint === output.endpoint) {
|
|
637
|
+
if (live !== undefined && live.credentialGeneration === output.credentialGeneration && live.context === output.context && !("connection" in output) && live.endpoint === output.endpoint) {
|
|
226
638
|
return { action: "noop" };
|
|
227
639
|
}
|
|
228
640
|
return { action: "update" };
|
|
229
641
|
});
|
|
230
|
-
var reconcileKubeconfig = (props, output) =>
|
|
642
|
+
var reconcileKubeconfig = (props, output) => Effect9.scoped(Effect9.gen(function* () {
|
|
231
643
|
const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
|
|
232
644
|
if (output !== undefined && output.credentialGeneration !== "") {
|
|
233
645
|
const raw2 = yield* readKvValue(props.target.mount, key, ["kubeconfig", "config"]);
|
|
234
646
|
const meta2 = buildAttrs(raw2, props);
|
|
235
647
|
if (meta2 === undefined) {
|
|
236
|
-
return yield*
|
|
648
|
+
return yield* Effect9.die(new Error(`${props.target.mount}/${key}: vault content no longer parses as a kubeconfig for ` + `context ${props.context}. Read back rather than trusting the write once.`));
|
|
237
649
|
}
|
|
238
650
|
return meta2;
|
|
239
651
|
}
|
|
240
652
|
const credential = yield* mintTalosconfig(props.target);
|
|
241
653
|
const { path: outPath } = yield* reservedTempPath(`${props.target.cluster}-${props.node}-kubeconfig-out`);
|
|
242
654
|
yield* talosctl(["kubeconfig", outPath, "--merge=false", "--force", "--force-context-name", props.context], { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
243
|
-
const raw = yield*
|
|
655
|
+
const raw = yield* Effect9.tryPromise({
|
|
244
656
|
catch: (cause) => new Error(`${outPath}: reading generated kubeconfig: ${String(cause)}`),
|
|
245
|
-
try: () =>
|
|
657
|
+
try: () => readFile(outPath, "utf8")
|
|
246
658
|
});
|
|
247
659
|
if (raw.trim() === "") {
|
|
248
|
-
return yield*
|
|
660
|
+
return yield* Effect9.die(new Error(`${props.node}: talosctl kubeconfig returned no error but wrote an empty file. Read ` + "back rather than trusting the exit code alone."));
|
|
249
661
|
}
|
|
250
|
-
yield*
|
|
662
|
+
yield* Effect9.try({
|
|
251
663
|
catch: (cause) => new Error(`${outPath}: chmod 0600: ${String(cause)}`),
|
|
252
664
|
try: () => chmodSync(outPath, 384)
|
|
253
665
|
});
|
|
254
666
|
const meta = buildAttrs(raw, props);
|
|
255
667
|
if (meta === undefined) {
|
|
256
|
-
return yield*
|
|
668
|
+
return yield* Effect9.die(new Error(`${props.node}: talosctl kubeconfig wrote a file that does not parse for context ` + `${props.context}.`));
|
|
257
669
|
}
|
|
258
670
|
yield* writeKvValue(props.target.mount, key, "kubeconfig", raw);
|
|
259
671
|
return meta;
|
|
260
672
|
}));
|
|
261
673
|
var handlers = {
|
|
262
|
-
delete: () =>
|
|
674
|
+
delete: () => Effect9.void,
|
|
263
675
|
diff: ({
|
|
264
676
|
news,
|
|
265
677
|
output
|
|
266
678
|
}) => diffKubeconfig(news, output),
|
|
267
|
-
list: () =>
|
|
679
|
+
list: () => Effect9.succeed([]),
|
|
268
680
|
read: ({ olds }) => readKubeconfig(olds),
|
|
269
681
|
reconcile: ({
|
|
270
682
|
news,
|
|
271
683
|
output
|
|
272
684
|
}) => reconcileKubeconfig(news, output)
|
|
273
685
|
};
|
|
274
|
-
var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig,
|
|
686
|
+
var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig, Effect9.succeed(TalosKubeconfig.Provider.of(handlers)));
|
|
275
687
|
// src/talos/talos-bootstrap.ts
|
|
276
688
|
import { Resource as Resource2 } from "alchemy";
|
|
277
689
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
278
690
|
import * as Provider2 from "alchemy/Provider";
|
|
279
|
-
import * as
|
|
691
|
+
import * as Effect11 from "effect/Effect";
|
|
280
692
|
|
|
281
693
|
// src/talos/talos-bootstrap-etcd.ts
|
|
282
|
-
import * as
|
|
694
|
+
import * as Effect10 from "effect/Effect";
|
|
283
695
|
|
|
284
696
|
// src/talos/talos-errors.ts
|
|
285
|
-
import * as
|
|
697
|
+
import * as Data2 from "effect/Data";
|
|
286
698
|
|
|
287
|
-
class TalosConfigDigestMismatch extends
|
|
699
|
+
class TalosConfigDigestMismatch extends Data2.TaggedError("TalosConfigDigestMismatch") {
|
|
288
700
|
get message() {
|
|
289
701
|
return `${this.mount}/${this.key}: live content digest ${this.got} does not match the pinned ` + `configDigest ${this.expected}. Fail closed — nothing was sent to talosctl. Either the KV ` + "value was rewritten without a matching PR, or the pinned digest is stale; check which one " + "moved before re-pinning either side.";
|
|
290
702
|
}
|
|
291
703
|
}
|
|
292
704
|
|
|
293
|
-
class TalosConvergenceTimeout extends
|
|
705
|
+
class TalosConvergenceTimeout extends Data2.TaggedError("TalosConvergenceTimeout") {
|
|
294
706
|
get message() {
|
|
295
707
|
return `${this.node}: apply-config (--mode ${this.mode}) was accepted but the live machineconfig ` + `digest never matched the pin within ${this.attempts} bounded poll attempts. Refusing to ` + "report converged — hand this node to the operator rather than silently pass.";
|
|
296
708
|
}
|
|
297
709
|
}
|
|
298
710
|
|
|
299
|
-
class TalosReBootstrapRefused extends
|
|
711
|
+
class TalosReBootstrapRefused extends Data2.TaggedError("TalosReBootstrapRefused") {
|
|
300
712
|
get message() {
|
|
301
713
|
return `${this.node}: state already recorded a successful bootstrap, and a live check could not ` + `reconfirm it (${this.detail}). Refusing to run 'talosctl bootstrap' again — that risks a ` + "second, isolated etcd cluster on a reset node. This needs an operator, not a reconcile.";
|
|
302
714
|
}
|
|
@@ -306,28 +718,28 @@ class TalosReBootstrapRefused extends Data.TaggedError("TalosReBootstrapRefused"
|
|
|
306
718
|
var isBootstrapped = (props, talosconfigPath) => talosctl(["get", "etcdmembers", "-o", "json"], {
|
|
307
719
|
nodes: [props.node],
|
|
308
720
|
talosconfigPath
|
|
309
|
-
}).pipe(
|
|
310
|
-
var assertNoLivePeers = (props, talosconfigPath) =>
|
|
311
|
-
onFailure: (cause) =>
|
|
721
|
+
}).pipe(Effect10.map((text) => text.includes('"id"') || text.includes('"member"')));
|
|
722
|
+
var assertNoLivePeers = (props, talosconfigPath) => Effect10.all((props.peers ?? []).map((peer) => isBootstrapped({ ...props, node: peer }, talosconfigPath).pipe(Effect10.matchEffect({
|
|
723
|
+
onFailure: (cause) => Effect10.fail(new TalosReBootstrapRefused({
|
|
312
724
|
detail: `peer ${peer}: live read failed before bootstrap: ${String(cause)}`,
|
|
313
725
|
node: props.node
|
|
314
726
|
})),
|
|
315
|
-
onSuccess: (hasMembers) => hasMembers ?
|
|
727
|
+
onSuccess: (hasMembers) => hasMembers ? Effect10.fail(new TalosReBootstrapRefused({
|
|
316
728
|
detail: `peer ${peer} already reports etcd members — a cluster may already exist; ` + "refusing to bootstrap a second one",
|
|
317
729
|
node: props.node
|
|
318
|
-
})) :
|
|
730
|
+
})) : Effect10.void
|
|
319
731
|
}))), { discard: true });
|
|
320
732
|
|
|
321
733
|
// src/talos/talos-bootstrap.ts
|
|
322
734
|
var TalosBootstrap = Resource2("Talos.Bootstrap", {
|
|
323
735
|
defaultRemovalPolicy: "retain"
|
|
324
736
|
});
|
|
325
|
-
var readBootstrap = (props) =>
|
|
737
|
+
var readBootstrap = (props) => Effect11.scoped(Effect11.gen(function* () {
|
|
326
738
|
const credential = yield* mintTalosconfig(props.target);
|
|
327
739
|
const bootstrapped = yield* isBootstrapped(props, credential.talosconfigPath);
|
|
328
740
|
return bootstrapped ? { bootstrapped: true, node: props.node } : undefined;
|
|
329
741
|
}));
|
|
330
|
-
var diffBootstrap = (news, output) =>
|
|
742
|
+
var diffBootstrap = (news, output) => Effect11.gen(function* () {
|
|
331
743
|
if (output === undefined || !isResolved2(news))
|
|
332
744
|
return;
|
|
333
745
|
if (output.bootstrapped)
|
|
@@ -335,15 +747,15 @@ var diffBootstrap = (news, output) => Effect6.gen(function* () {
|
|
|
335
747
|
const live = yield* readBootstrap(news);
|
|
336
748
|
return live?.bootstrapped === true ? { action: "noop" } : { action: "update" };
|
|
337
749
|
});
|
|
338
|
-
var reconcileBootstrap = (props, output) =>
|
|
750
|
+
var reconcileBootstrap = (props, output) => Effect11.scoped(Effect11.gen(function* () {
|
|
339
751
|
const credential = yield* mintTalosconfig(props.target);
|
|
340
752
|
if (output?.bootstrapped === true) {
|
|
341
|
-
const stillBootstrapped = yield* isBootstrapped(props, credential.talosconfigPath).pipe(
|
|
753
|
+
const stillBootstrapped = yield* isBootstrapped(props, credential.talosconfigPath).pipe(Effect11.catchIf(() => true, (cause) => Effect11.fail(new TalosReBootstrapRefused({
|
|
342
754
|
detail: `live read failed: ${String(cause)}`,
|
|
343
755
|
node: props.node
|
|
344
756
|
}))));
|
|
345
757
|
if (!stillBootstrapped) {
|
|
346
|
-
return yield*
|
|
758
|
+
return yield* Effect11.fail(new TalosReBootstrapRefused({
|
|
347
759
|
detail: "etcd members are absent on a live read against already-bootstrapped state",
|
|
348
760
|
node: props.node
|
|
349
761
|
}));
|
|
@@ -360,31 +772,104 @@ var reconcileBootstrap = (props, output) => Effect6.scoped(Effect6.gen(function*
|
|
|
360
772
|
}
|
|
361
773
|
const after = yield* isBootstrapped(props, credential.talosconfigPath);
|
|
362
774
|
if (!after) {
|
|
363
|
-
return yield*
|
|
775
|
+
return yield* Effect11.die(new Error(`${props.node}: bootstrap returned no error but etcd members are still absent. ` + "Read back rather than trusting the exit code alone."));
|
|
364
776
|
}
|
|
365
777
|
return { bootstrapped: true, node: props.node };
|
|
366
778
|
}));
|
|
367
779
|
var handlers2 = {
|
|
368
|
-
delete: () =>
|
|
780
|
+
delete: () => Effect11.void,
|
|
369
781
|
diff: ({
|
|
370
782
|
news,
|
|
371
783
|
output
|
|
372
784
|
}) => diffBootstrap(news, output),
|
|
373
|
-
list: () =>
|
|
785
|
+
list: () => Effect11.succeed([]),
|
|
374
786
|
read: ({ olds }) => readBootstrap(olds),
|
|
375
787
|
reconcile: ({
|
|
376
788
|
news,
|
|
377
789
|
output
|
|
378
790
|
}) => reconcileBootstrap(news, output)
|
|
379
791
|
};
|
|
380
|
-
var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap,
|
|
381
|
-
// src/talos/talos-cluster-
|
|
792
|
+
var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap, Effect11.succeed(TalosBootstrap.Provider.of(handlers2)));
|
|
793
|
+
// src/talos/talos-cluster-identity.ts
|
|
382
794
|
import { Resource as Resource3 } from "alchemy";
|
|
383
795
|
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
384
796
|
import * as Provider3 from "alchemy/Provider";
|
|
385
|
-
import * as
|
|
386
|
-
var
|
|
797
|
+
import * as Effect12 from "effect/Effect";
|
|
798
|
+
var TalosClusterIdentity = Resource3("Talos.ClusterIdentity");
|
|
799
|
+
var readClusterIdentity = (props, output) => Effect12.gen(function* () {
|
|
800
|
+
const cluster = props.target.cluster;
|
|
801
|
+
const uid = yield* withConnectDeadline(cluster, Effect12.gen(function* () {
|
|
802
|
+
const transport = yield* openTransport(talosVaultLocation(props));
|
|
803
|
+
return yield* readClusterUid(cluster, transport);
|
|
804
|
+
}));
|
|
805
|
+
if (output !== undefined && uid !== output.uid) {
|
|
806
|
+
return yield* Effect12.fail(new TalosClusterMoved({ cluster, live: uid, saved: output.uid }));
|
|
807
|
+
}
|
|
808
|
+
return { uid };
|
|
809
|
+
});
|
|
810
|
+
var reconcileClusterIdentity = (props, output) => readClusterIdentity(props, output);
|
|
811
|
+
var diffClusterIdentity = (news, output) => Effect12.gen(function* () {
|
|
812
|
+
if (output === undefined || !isResolved3(news))
|
|
813
|
+
return;
|
|
814
|
+
yield* reconcileClusterIdentity(news, output);
|
|
815
|
+
return { action: "noop" };
|
|
816
|
+
});
|
|
817
|
+
var handlers3 = {
|
|
818
|
+
delete: () => Effect12.void,
|
|
819
|
+
diff: ({
|
|
820
|
+
news,
|
|
821
|
+
output
|
|
822
|
+
}) => diffClusterIdentity(news, output),
|
|
823
|
+
list: () => Effect12.succeed([]),
|
|
824
|
+
read: ({
|
|
825
|
+
olds,
|
|
826
|
+
output
|
|
827
|
+
}) => readClusterIdentity(olds, output),
|
|
828
|
+
reconcile: ({
|
|
829
|
+
news,
|
|
830
|
+
output
|
|
831
|
+
}) => reconcileClusterIdentity(news, output)
|
|
832
|
+
};
|
|
833
|
+
var TalosClusterIdentityProvider = () => Provider3.effect(TalosClusterIdentity, Effect12.succeed(TalosClusterIdentity.Provider.of(handlers3)));
|
|
834
|
+
// src/talos/talos-cluster-health.ts
|
|
835
|
+
import { Resource as Resource4 } from "alchemy";
|
|
836
|
+
import { isResolved as isResolved4 } from "alchemy/Diff";
|
|
837
|
+
import * as Provider4 from "alchemy/Provider";
|
|
838
|
+
import * as Effect14 from "effect/Effect";
|
|
839
|
+
|
|
840
|
+
// src/talos/literal-connection.ts
|
|
841
|
+
import { hasUnresolvedInputs } from "alchemy/Diff";
|
|
842
|
+
import * as Effect13 from "effect/Effect";
|
|
843
|
+
var literalConnectionRefusal = (connection) => {
|
|
844
|
+
const auth = connection?.auth;
|
|
845
|
+
return hasUnresolvedInputs(connection) || auth?.kind !== "talos-openbao" || typeof auth?.uid !== "string" || auth.uid === "" ? new TalosUidNotLiteral({}) : undefined;
|
|
846
|
+
};
|
|
847
|
+
var withLiteralConnection = (resource) => new Proxy(resource, {
|
|
848
|
+
apply: (target, thisArg, args) => {
|
|
849
|
+
const refusal = (given) => literalConnectionRefusal(given?.connection);
|
|
850
|
+
const props = args[1];
|
|
851
|
+
if (Effect13.isEffect(props)) {
|
|
852
|
+
const checked = props.pipe(Effect13.flatMap((resolved) => {
|
|
853
|
+
const refused2 = refusal(resolved);
|
|
854
|
+
return refused2 === undefined ? Effect13.succeed(resolved) : Effect13.fail(refused2);
|
|
855
|
+
}));
|
|
856
|
+
return Reflect.apply(target, thisArg, [args[0], checked, ...args.slice(2)]);
|
|
857
|
+
}
|
|
858
|
+
const refused = refusal(props);
|
|
859
|
+
return refused === undefined ? Reflect.apply(target, thisArg, args) : Effect13.fail(refused);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// src/talos/talos-cluster-health.ts
|
|
864
|
+
var TalosClusterHealth = withLiteralConnection(Resource4("Talos.ClusterHealth"));
|
|
387
865
|
var nodeCsv = (nodes) => nodes === undefined || nodes.length === 0 ? "" : nodes.join(",");
|
|
866
|
+
var attributes = (props, healthy) => ({
|
|
867
|
+
connection: props.connection,
|
|
868
|
+
controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
|
|
869
|
+
healthy,
|
|
870
|
+
workerNodes: nodeCsv(props.workerNodes)
|
|
871
|
+
});
|
|
872
|
+
var sameConnection = (left, right) => right?.auth?.kind === left.auth.kind && right.auth.uid === left.auth.uid && !Object.hasOwn(right.auth, "cluster");
|
|
388
873
|
var healthArgs = (props, waitTimeout) => {
|
|
389
874
|
const args = ["health", "--wait-timeout", waitTimeout];
|
|
390
875
|
const cp = nodeCsv(props.controlPlaneNodes);
|
|
@@ -395,79 +880,75 @@ var healthArgs = (props, waitTimeout) => {
|
|
|
395
880
|
args.push("--worker-nodes", workers);
|
|
396
881
|
return args;
|
|
397
882
|
};
|
|
398
|
-
var check = (props, waitTimeout) =>
|
|
883
|
+
var check = (props, waitTimeout) => Effect14.scoped(Effect14.gen(function* () {
|
|
399
884
|
const credential = yield* mintTalosconfig(props.target);
|
|
400
885
|
const contact = props.controlPlaneNodes[0];
|
|
401
886
|
if (contact === undefined) {
|
|
402
|
-
return yield*
|
|
887
|
+
return yield* Effect14.fail(new Error(`Talos.ClusterHealth ${props.target.cluster}: controlPlaneNodes is empty — need at ` + "least one node to contact."));
|
|
403
888
|
}
|
|
404
889
|
yield* talosctl(healthArgs(props, waitTimeout), {
|
|
405
890
|
endpoints: [contact],
|
|
406
891
|
nodes: [contact],
|
|
407
892
|
talosconfigPath: credential.talosconfigPath
|
|
408
893
|
});
|
|
409
|
-
return
|
|
410
|
-
controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
|
|
411
|
-
healthy: true,
|
|
412
|
-
workerNodes: nodeCsv(props.workerNodes)
|
|
413
|
-
};
|
|
894
|
+
return attributes(props, true);
|
|
414
895
|
}));
|
|
415
896
|
var isTalosError = (cause) => cause instanceof TalosError;
|
|
416
|
-
var readClusterHealth = (props) => check(props, "5s").pipe(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (output === undefined || !isResolved3(news))
|
|
897
|
+
var readClusterHealth = (props) => check(props, "5s").pipe(Effect14.catchIf(isTalosError, () => Effect14.succeed(attributes(props, false))));
|
|
898
|
+
var diffClusterHealth = (news, output) => Effect14.gen(function* () {
|
|
899
|
+
const refused = literalConnectionRefusal(news.connection);
|
|
900
|
+
if (refused !== undefined)
|
|
901
|
+
return yield* Effect14.fail(refused);
|
|
902
|
+
if (output === undefined || !isResolved4(news))
|
|
423
903
|
return;
|
|
424
904
|
const live = yield* readClusterHealth(news);
|
|
425
|
-
if (live.healthy)
|
|
905
|
+
if (live.healthy && sameConnection(live.connection, output.connection)) {
|
|
426
906
|
return { action: "noop" };
|
|
907
|
+
}
|
|
427
908
|
return { action: "update" };
|
|
428
909
|
});
|
|
429
|
-
var reconcileClusterHealth = (props) => check(props, props.waitTimeout ?? "20m0s").pipe(
|
|
430
|
-
var
|
|
431
|
-
delete: () =>
|
|
910
|
+
var reconcileClusterHealth = (props) => check(props, props.waitTimeout ?? "20m0s").pipe(Effect14.catchIf(isTalosError, (cause) => Effect14.fail(new Error(`Talos.ClusterHealth ${props.target.cluster}: cluster not healthy within ` + `${props.waitTimeout ?? "20m0s"} — ${String(cause)}`))));
|
|
911
|
+
var handlers4 = {
|
|
912
|
+
delete: () => Effect14.void,
|
|
432
913
|
diff: ({
|
|
433
914
|
news,
|
|
434
915
|
output
|
|
435
916
|
}) => diffClusterHealth(news, output),
|
|
436
|
-
list: () =>
|
|
917
|
+
list: () => Effect14.succeed([]),
|
|
437
918
|
read: ({ olds }) => readClusterHealth(olds),
|
|
438
919
|
reconcile: ({ news }) => reconcileClusterHealth(news)
|
|
439
920
|
};
|
|
440
|
-
var TalosClusterHealthProvider = () =>
|
|
921
|
+
var TalosClusterHealthProvider = () => Provider4.effect(TalosClusterHealth, Effect14.succeed(TalosClusterHealth.Provider.of(handlers4)));
|
|
441
922
|
// src/talos/talos-machine-config.ts
|
|
442
|
-
import { Resource as
|
|
443
|
-
import * as
|
|
444
|
-
import * as
|
|
923
|
+
import { Resource as Resource5 } from "alchemy";
|
|
924
|
+
import * as Provider5 from "alchemy/Provider";
|
|
925
|
+
import * as Effect17 from "effect/Effect";
|
|
445
926
|
|
|
446
927
|
// src/talos/machine-config-read.ts
|
|
447
928
|
import { Unowned } from "alchemy/AdoptPolicy";
|
|
448
|
-
import { isResolved as
|
|
449
|
-
import * as
|
|
929
|
+
import { isResolved as isResolved5 } from "alchemy/Diff";
|
|
930
|
+
import * as Effect15 from "effect/Effect";
|
|
450
931
|
import * as Result from "effect/Result";
|
|
451
932
|
var liveSpecDigest = (props, talosconfigPath) => talosctl(["get", "machineconfig", "v1alpha1", "-o", "yaml"], {
|
|
452
933
|
endpoints: [props.node],
|
|
453
934
|
nodes: [props.node],
|
|
454
935
|
talosconfigPath
|
|
455
|
-
}).pipe(
|
|
936
|
+
}).pipe(Effect15.flatMap((wrapperYaml) => {
|
|
456
937
|
const spec = extractMachineConfigSpec(wrapperYaml);
|
|
457
|
-
return spec === undefined ?
|
|
938
|
+
return spec === undefined ? Effect15.fail(new Error(`${props.node}: get machineconfig v1alpha1 returned no string 'spec' field — see ` + "values.ts extractMachineConfigSpec for the expected shape.")) : Effect15.succeed(configDigest(spec));
|
|
458
939
|
}));
|
|
459
940
|
var probeMaintenanceMode = (props, talosconfigPath) => talosctl(["version"], {
|
|
460
941
|
endpoints: [props.node],
|
|
461
942
|
insecure: true,
|
|
462
943
|
nodes: [props.node],
|
|
463
944
|
talosconfigPath
|
|
464
|
-
}).pipe(
|
|
465
|
-
var readMachineConfig = (props) =>
|
|
945
|
+
}).pipe(Effect15.as(true), Effect15.orElseSucceed(() => false));
|
|
946
|
+
var readMachineConfig = (props) => Effect15.scoped(Effect15.gen(function* () {
|
|
466
947
|
const credential = yield* mintTalosconfig(props.target);
|
|
467
|
-
const outcome = yield*
|
|
948
|
+
const outcome = yield* Effect15.result(liveSpecDigest(props, credential.talosconfigPath));
|
|
468
949
|
if (Result.isFailure(outcome)) {
|
|
469
950
|
const reachable = yield* probeMaintenanceMode(props, credential.talosconfigPath);
|
|
470
|
-
return reachable ? undefined : yield*
|
|
951
|
+
return reachable ? undefined : yield* Effect15.fail(outcome.failure);
|
|
471
952
|
}
|
|
472
953
|
const attrs = {
|
|
473
954
|
configDigest: props.configDigest,
|
|
@@ -477,8 +958,8 @@ var readMachineConfig = (props) => Effect8.scoped(Effect8.gen(function* () {
|
|
|
477
958
|
};
|
|
478
959
|
return attrs.converged === false ? Unowned(attrs) : attrs;
|
|
479
960
|
}));
|
|
480
|
-
var diffMachineConfig = (news, output) =>
|
|
481
|
-
if (output === undefined || !
|
|
961
|
+
var diffMachineConfig = (news, output) => Effect15.gen(function* () {
|
|
962
|
+
if (output === undefined || !isResolved5(news))
|
|
482
963
|
return;
|
|
483
964
|
const live = yield* readMachineConfig(news);
|
|
484
965
|
if (live !== undefined && live.converged !== false)
|
|
@@ -487,40 +968,40 @@ var diffMachineConfig = (news, output) => Effect8.gen(function* () {
|
|
|
487
968
|
});
|
|
488
969
|
|
|
489
970
|
// src/talos/machine-config-poll.ts
|
|
490
|
-
import * as
|
|
971
|
+
import * as Effect16 from "effect/Effect";
|
|
491
972
|
var isRebootish = (mode) => mode === "reboot" || mode === "auto";
|
|
492
973
|
var POLICY = {
|
|
493
974
|
immediate: { intervalMs: 2000, maxAttempts: 5 },
|
|
494
975
|
rebootish: { intervalMs: 5000, maxAttempts: 24 }
|
|
495
976
|
};
|
|
496
|
-
var pollUntilMatch = (check2, policy) =>
|
|
977
|
+
var pollUntilMatch = (check2, policy) => Effect16.gen(function* () {
|
|
497
978
|
for (let attempt = 1;attempt <= policy.maxAttempts; attempt++) {
|
|
498
|
-
const matched = yield* check2.pipe(
|
|
979
|
+
const matched = yield* check2.pipe(Effect16.orElseSucceed(() => false));
|
|
499
980
|
if (matched)
|
|
500
981
|
return true;
|
|
501
982
|
if (attempt < policy.maxAttempts)
|
|
502
|
-
yield*
|
|
983
|
+
yield* Effect16.sleep(policy.intervalMs);
|
|
503
984
|
}
|
|
504
985
|
return false;
|
|
505
986
|
});
|
|
506
|
-
var confirmConverged = (node, mode, check2, policies = POLICY) =>
|
|
987
|
+
var confirmConverged = (node, mode, check2, policies = POLICY) => Effect16.gen(function* () {
|
|
507
988
|
const rebootish = isRebootish(mode);
|
|
508
989
|
const policy = rebootish ? policies.rebootish : policies.immediate;
|
|
509
990
|
const matched = yield* pollUntilMatch(check2, policy);
|
|
510
991
|
if (!matched) {
|
|
511
|
-
return yield*
|
|
992
|
+
return yield* Effect16.fail(new TalosConvergenceTimeout({ attempts: policy.maxAttempts, mode, node }));
|
|
512
993
|
}
|
|
513
994
|
return rebootish ? "accepted" : "read-back";
|
|
514
995
|
});
|
|
515
996
|
|
|
516
997
|
// src/talos/talos-machine-config.ts
|
|
517
|
-
var TalosMachineConfig =
|
|
518
|
-
var reconcileMachineConfig = (props, output, pollPolicies) =>
|
|
998
|
+
var TalosMachineConfig = Resource5("Talos.MachineConfig");
|
|
999
|
+
var reconcileMachineConfig = (props, output, pollPolicies) => Effect17.scoped(Effect17.gen(function* () {
|
|
519
1000
|
const mode = props.mode ?? "auto";
|
|
520
1001
|
const raw = yield* readKvValue(props.target.mount, props.configKey, ["config"]);
|
|
521
1002
|
const gotDigest = configDigest(raw);
|
|
522
1003
|
if (gotDigest !== props.configDigest) {
|
|
523
|
-
return yield*
|
|
1004
|
+
return yield* Effect17.fail(new TalosConfigDigestMismatch({
|
|
524
1005
|
expected: props.configDigest,
|
|
525
1006
|
got: gotDigest,
|
|
526
1007
|
key: props.configKey,
|
|
@@ -539,7 +1020,7 @@ var reconcileMachineConfig = (props, output, pollPolicies) => Effect10.scoped(Ef
|
|
|
539
1020
|
...isCreate ? ["--insecure"] : []
|
|
540
1021
|
];
|
|
541
1022
|
yield* talosctl(args, { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
542
|
-
const converged = yield* confirmConverged(props.node, mode, liveSpecDigest(props, credential.talosconfigPath).pipe(
|
|
1023
|
+
const converged = yield* confirmConverged(props.node, mode, liveSpecDigest(props, credential.talosconfigPath).pipe(Effect17.map((live) => live === props.configDigest)), pollPolicies);
|
|
543
1024
|
return {
|
|
544
1025
|
configDigest: props.configDigest,
|
|
545
1026
|
converged,
|
|
@@ -547,30 +1028,51 @@ var reconcileMachineConfig = (props, output, pollPolicies) => Effect10.scoped(Ef
|
|
|
547
1028
|
node: props.node
|
|
548
1029
|
};
|
|
549
1030
|
}));
|
|
550
|
-
var
|
|
551
|
-
delete: () =>
|
|
1031
|
+
var handlers5 = {
|
|
1032
|
+
delete: () => Effect17.void,
|
|
552
1033
|
diff: ({
|
|
553
1034
|
news,
|
|
554
1035
|
output
|
|
555
1036
|
}) => diffMachineConfig(news, output),
|
|
556
|
-
list: () =>
|
|
1037
|
+
list: () => Effect17.succeed([]),
|
|
557
1038
|
read: ({ olds }) => readMachineConfig(olds),
|
|
558
1039
|
reconcile: ({
|
|
559
1040
|
news,
|
|
560
1041
|
output
|
|
561
1042
|
}) => reconcileMachineConfig(news, output)
|
|
562
1043
|
};
|
|
563
|
-
var TalosMachineConfigProvider = () =>
|
|
1044
|
+
var TalosMachineConfigProvider = () => Provider5.effect(TalosMachineConfig, Effect17.succeed(TalosMachineConfig.Provider.of(handlers5)));
|
|
564
1045
|
export {
|
|
1046
|
+
DEFAULT_TALOSCTL_BINARY,
|
|
1047
|
+
TALOSCTL_BINARY_ENV,
|
|
565
1048
|
TalosBootstrap,
|
|
566
1049
|
TalosBootstrapProvider,
|
|
567
1050
|
TalosClusterHealth,
|
|
568
1051
|
TalosClusterHealthProvider,
|
|
1052
|
+
TalosClusterIdentity,
|
|
1053
|
+
TalosClusterIdentityMismatch,
|
|
1054
|
+
TalosClusterIdentityMissing,
|
|
1055
|
+
TalosClusterIdentityProvider,
|
|
1056
|
+
TalosClusterIdentityTimeout,
|
|
1057
|
+
TalosClusterIdentityUnreadable,
|
|
1058
|
+
TalosClusterMoved,
|
|
569
1059
|
TalosKubeconfig,
|
|
570
1060
|
TalosKubeconfigProvider,
|
|
1061
|
+
TalosKubeconfigUnreadable,
|
|
571
1062
|
TalosMachineConfig,
|
|
572
|
-
TalosMachineConfigProvider
|
|
1063
|
+
TalosMachineConfigProvider,
|
|
1064
|
+
TalosOpenBaoAdapter,
|
|
1065
|
+
TalosOpenBaoAmbiguousUid,
|
|
1066
|
+
TalosOpenBaoAuthKind,
|
|
1067
|
+
TalosOpenBaoConnectTimeout,
|
|
1068
|
+
TalosOpenBaoLegacyAuth,
|
|
1069
|
+
TalosOpenBaoUnknownCluster,
|
|
1070
|
+
TalosUidNotLiteral,
|
|
1071
|
+
TalosVaultKeyMissing,
|
|
1072
|
+
connectTalosOpenBao,
|
|
1073
|
+
talosOpenBaoCluster,
|
|
1074
|
+
talosOpenBaoConnection
|
|
573
1075
|
};
|
|
574
1076
|
|
|
575
|
-
//# debugId=
|
|
1077
|
+
//# debugId=0069C805BB1B149764756E2164756E21
|
|
576
1078
|
//# sourceMappingURL=index.js.map
|