@homeflare/alchemy 0.47.1 → 0.48.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +116 -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 +683 -170
- 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 +29 -3
- 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 +45 -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 +193 -0
- package/src/talos/cluster-adapter-move.test.ts +131 -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 +250 -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.test.ts +44 -0
- package/src/talos/mint-temp-file.ts +68 -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-probe-kill.test.ts +56 -0
- package/src/talos/talosctl-vetting.test.ts +212 -0
- package/src/talos/talosctl.test.ts +163 -0
- package/src/talos/talosctl.ts +133 -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,89 +1,490 @@
|
|
|
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 (the identity read's own deadline, inside the 10 s connect deadline). ` + "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, unlinkSync, 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 {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
255
|
+
try {
|
|
256
|
+
writeFileSync(fd, raw);
|
|
257
|
+
} finally {
|
|
258
|
+
closeSync(fd);
|
|
259
|
+
}
|
|
260
|
+
} catch (cause) {
|
|
261
|
+
try {
|
|
262
|
+
unlinkSync(path);
|
|
263
|
+
} catch {}
|
|
264
|
+
throw cause;
|
|
44
265
|
}
|
|
45
266
|
},
|
|
46
267
|
catch: (cause) => new Error(`writing ${label} temp file: ${String(cause)}`)
|
|
47
|
-
}), () =>
|
|
268
|
+
}), () => Effect3.tryPromise({ try: () => unlink(path), catch: () => {
|
|
48
269
|
return;
|
|
49
|
-
} }).pipe(
|
|
270
|
+
} }).pipe(Effect3.orElseSucceed(() => {
|
|
50
271
|
return;
|
|
51
272
|
})));
|
|
52
273
|
return { path };
|
|
53
274
|
});
|
|
275
|
+
|
|
276
|
+
// src/talos/credentials.ts
|
|
277
|
+
var DEFAULT_TALOSCONFIG_KEY = "talosconfig";
|
|
278
|
+
var DEFAULT_KUBECONFIG_KEY = "kubeconfig";
|
|
279
|
+
var readKvValue = (mount, key, fields) => Effect4.gen(function* () {
|
|
280
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
281
|
+
const bao = yield* resolveBao();
|
|
282
|
+
const env = baoEnv();
|
|
283
|
+
if (env === undefined) {
|
|
284
|
+
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."));
|
|
285
|
+
}
|
|
286
|
+
const result = yield* ChildProcess.make(bao, ["kv", "get", "-format=json", `${mount}/${key}`], {
|
|
287
|
+
env,
|
|
288
|
+
extendEnv: false,
|
|
289
|
+
forceKillAfter: Duration2.seconds(1),
|
|
290
|
+
stderr: "pipe",
|
|
291
|
+
stdin: "ignore",
|
|
292
|
+
stdout: "pipe"
|
|
293
|
+
}).pipe(spawner.spawn, Effect4.flatMap((child) => Effect4.all({
|
|
294
|
+
exitCode: child.exitCode,
|
|
295
|
+
stderr: child.stderr.pipe(Stream.decodeText, Stream.mkString, Effect4.map((text) => text.trim())),
|
|
296
|
+
stdout: child.stdout.pipe(Stream.decodeText, Stream.mkString, Effect4.map((text) => text.trim()))
|
|
297
|
+
}, { concurrency: "unbounded" })), Effect4.scoped);
|
|
298
|
+
if (result.exitCode !== 0) {
|
|
299
|
+
return yield* Effect4.fail(new Error(`bao kv get ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
|
|
300
|
+
}
|
|
301
|
+
let parsed;
|
|
302
|
+
try {
|
|
303
|
+
parsed = JSON.parse(result.stdout);
|
|
304
|
+
} catch {
|
|
305
|
+
return yield* Effect4.fail(new Error(`bao kv get ${mount}/${key} returned output that is not JSON (not echoed).`));
|
|
306
|
+
}
|
|
307
|
+
const data = parsed.data?.data;
|
|
308
|
+
for (const field of fields) {
|
|
309
|
+
const raw = data?.[field];
|
|
310
|
+
if (typeof raw === "string" && raw.trim() !== "")
|
|
311
|
+
return raw;
|
|
312
|
+
}
|
|
313
|
+
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."));
|
|
314
|
+
});
|
|
315
|
+
var isVaultKeyAbsent = (error) => error instanceof Error && /no value found at/i.test(error.message);
|
|
54
316
|
var mintTalosconfig = (target) => readKvValue(target.mount, target.talosconfigKey ?? DEFAULT_TALOSCONFIG_KEY, [
|
|
55
317
|
"talosconfig",
|
|
56
318
|
"config"
|
|
57
|
-
]).pipe(
|
|
319
|
+
]).pipe(Effect4.flatMap((raw) => mintKvTempFile(raw, `${target.cluster}-talosconfig`)), Effect4.map(({ path }) => ({ talosconfigPath: path })));
|
|
320
|
+
|
|
321
|
+
// src/talos/yaml-parse.ts
|
|
322
|
+
import { YAMLParseError, parse } from "yaml";
|
|
323
|
+
var parseYaml = (text) => {
|
|
324
|
+
try {
|
|
325
|
+
return parse(text, { logLevel: "error", prettyErrors: false });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (error instanceof YAMLParseError)
|
|
328
|
+
return;
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/talos/kubeconfig-doc.ts
|
|
334
|
+
var pem = (data) => {
|
|
335
|
+
if (data === undefined || data.trim() === "")
|
|
336
|
+
return;
|
|
337
|
+
const decoded = Buffer.from(data, "base64").toString("utf8");
|
|
338
|
+
return decoded.includes("BEGIN ") ? decoded : undefined;
|
|
339
|
+
};
|
|
340
|
+
var kubeconfigTransport = (yamlText, contextName) => {
|
|
341
|
+
const parsed = parseYaml(yamlText);
|
|
342
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
343
|
+
return;
|
|
344
|
+
const doc = parsed;
|
|
345
|
+
const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;
|
|
346
|
+
const clusterName = ctx?.cluster;
|
|
347
|
+
const userName = ctx?.user;
|
|
348
|
+
if (clusterName === undefined || userName === undefined)
|
|
349
|
+
return;
|
|
350
|
+
const cluster = doc.clusters?.find((row) => row.name === clusterName)?.cluster;
|
|
351
|
+
const user = doc.users?.find((row) => row.name === userName)?.user;
|
|
352
|
+
const endpoint = cluster?.server;
|
|
353
|
+
const certificateAuthorityData = cluster?.["certificate-authority-data"];
|
|
354
|
+
const certificate = pem(user?.["client-certificate-data"]);
|
|
355
|
+
const key = pem(user?.["client-key-data"]);
|
|
356
|
+
if (endpoint === undefined || certificateAuthorityData === undefined || pem(certificateAuthorityData) === undefined || certificate === undefined || key === undefined) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
return { certificateAuthorityData, clientCert: { certificate, key }, endpoint };
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/talos/cluster-transport.ts
|
|
363
|
+
var talosVaultLocation = (props) => ({
|
|
364
|
+
context: props.context,
|
|
365
|
+
key: props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY,
|
|
366
|
+
mount: props.target.mount
|
|
367
|
+
});
|
|
368
|
+
var talosOpenBaoCluster = (props, pin) => ({
|
|
369
|
+
...talosVaultLocation(props),
|
|
370
|
+
uid: pin.uid,
|
|
371
|
+
...pin.retired === true ? { retired: true } : {}
|
|
372
|
+
});
|
|
373
|
+
var CONNECT_TIMEOUT = Duration3.seconds(10);
|
|
374
|
+
var withConnectDeadline = (cluster, effect, timeout = CONNECT_TIMEOUT) => effect.pipe(Effect5.timeoutOrElse({
|
|
375
|
+
duration: timeout,
|
|
376
|
+
orElse: () => Effect5.fail(new TalosOpenBaoConnectTimeout({ cluster, seconds: Duration3.toSeconds(timeout) }))
|
|
377
|
+
}));
|
|
378
|
+
var openTransport = (config) => Effect5.gen(function* () {
|
|
379
|
+
const { context, key, mount } = config;
|
|
380
|
+
const raw = yield* readKvValue(mount, key, ["kubeconfig", "config"]).pipe(Effect5.mapError((error) => isVaultKeyAbsent(error) ? new TalosVaultKeyMissing({ key, mount }) : error));
|
|
381
|
+
const material = yield* Effect5.sync(() => kubeconfigTransport(raw, context));
|
|
382
|
+
if (material === undefined) {
|
|
383
|
+
return yield* Effect5.fail(new TalosKubeconfigUnreadable({ context, key, mount }));
|
|
384
|
+
}
|
|
385
|
+
return { ...material, headers: Effect5.succeed({}) };
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/talos/cluster-adapter.ts
|
|
389
|
+
var talosOpenBaoConnection = (uid) => {
|
|
390
|
+
if (typeof uid !== "string" || uid === "")
|
|
391
|
+
throw new TalosUidNotLiteral({});
|
|
392
|
+
return { auth: { kind: "talos-openbao", uid } };
|
|
393
|
+
};
|
|
394
|
+
var connectTalosOpenBao = (configs, connection, timeout = CONNECT_TIMEOUT) => Effect6.gen(function* () {
|
|
395
|
+
if (connection === undefined || connection === null) {
|
|
396
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
397
|
+
}
|
|
398
|
+
const { auth } = connection;
|
|
399
|
+
if (auth === undefined || auth === null) {
|
|
400
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
401
|
+
}
|
|
402
|
+
if (auth.kind !== "talos-openbao") {
|
|
403
|
+
return yield* Effect6.fail(new TalosOpenBaoAuthKind({ kind: auth.kind }));
|
|
404
|
+
}
|
|
405
|
+
if (Object.hasOwn(auth, "cluster")) {
|
|
406
|
+
return yield* Effect6.fail(new TalosOpenBaoLegacyAuth({}));
|
|
407
|
+
}
|
|
408
|
+
const { uid } = auth;
|
|
409
|
+
if (typeof uid !== "string" || uid === "") {
|
|
410
|
+
return yield* Effect6.fail(new TalosClusterIdentityMissing({}));
|
|
411
|
+
}
|
|
412
|
+
const matches = Object.entries(configs).filter(([, entry]) => entry.uid === uid);
|
|
413
|
+
const [match] = matches;
|
|
414
|
+
if (match === undefined) {
|
|
415
|
+
return yield* Effect6.fail(new TalosOpenBaoUnknownCluster({
|
|
416
|
+
known: Object.values(configs).map((entry) => entry.uid),
|
|
417
|
+
uid
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
if (matches.length > 1) {
|
|
421
|
+
return yield* Effect6.fail(new TalosOpenBaoAmbiguousUid({ aliases: matches.map(([alias2]) => alias2), uid }));
|
|
422
|
+
}
|
|
423
|
+
const [alias, config] = match;
|
|
424
|
+
if (config.retired === true) {
|
|
425
|
+
return yield* Effect6.fail(new ClusterNotFoundError({ message: `talos-openbao cluster '${alias}' is retired` }));
|
|
426
|
+
}
|
|
427
|
+
return yield* withConnectDeadline(alias, Effect6.gen(function* () {
|
|
428
|
+
const transport = yield* openTransport(config);
|
|
429
|
+
yield* assertClusterUid(alias, uid, transport);
|
|
430
|
+
return transport;
|
|
431
|
+
}), timeout);
|
|
432
|
+
});
|
|
433
|
+
var TalosOpenBaoAdapter = (config) => Layer.effect(ClusterAdapter("talos-openbao"), Effect6.gen(function* () {
|
|
434
|
+
const context2 = yield* Effect6.context();
|
|
435
|
+
const service = {
|
|
436
|
+
kind: "Kubernetes.ClusterAdapter",
|
|
437
|
+
connect: (connection) => connectTalosOpenBao(config, connection).pipe(Effect6.provideContext(context2))
|
|
438
|
+
};
|
|
439
|
+
return service;
|
|
440
|
+
}));
|
|
441
|
+
// src/talos/kubeconfig.ts
|
|
442
|
+
import { readFile } from "node:fs/promises";
|
|
443
|
+
import { chmodSync } from "node:fs";
|
|
444
|
+
import { Resource } from "alchemy";
|
|
445
|
+
import { isResolved } from "alchemy/Diff";
|
|
446
|
+
import * as Provider from "alchemy/Provider";
|
|
447
|
+
import * as Effect9 from "effect/Effect";
|
|
58
448
|
|
|
59
449
|
// src/talos/credentials-write.ts
|
|
60
|
-
import * as
|
|
450
|
+
import * as Effect7 from "effect/Effect";
|
|
451
|
+
import * as Duration4 from "effect/Duration";
|
|
61
452
|
import * as Stream2 from "effect/Stream";
|
|
453
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
454
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
455
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
456
|
+
import { join as join3 } from "node:path";
|
|
62
457
|
import * as ChildProcess2 from "effect/process/ChildProcess";
|
|
63
458
|
import * as ChildProcessSpawner3 from "effect/process/ChildProcessSpawner";
|
|
64
|
-
var reservedTempPath = (label) =>
|
|
65
|
-
const path =
|
|
66
|
-
yield*
|
|
459
|
+
var reservedTempPath = (label) => Effect7.gen(function* () {
|
|
460
|
+
const path = join3(tmpdir2(), `hf-talos-${label}-${randomUUID2()}.yaml`);
|
|
461
|
+
yield* Effect7.addFinalizer(() => Effect7.tryPromise({ try: () => unlink2(path), catch: () => {
|
|
67
462
|
return;
|
|
68
|
-
} }).pipe(
|
|
463
|
+
} }).pipe(Effect7.orElseSucceed(() => {
|
|
69
464
|
return;
|
|
70
465
|
})));
|
|
71
466
|
return { path };
|
|
72
467
|
});
|
|
73
|
-
var writeKvValue = (mount, key, field, value) =>
|
|
468
|
+
var writeKvValue = (mount, key, field, value) => Effect7.gen(function* () {
|
|
74
469
|
const spawner = yield* ChildProcessSpawner3.ChildProcessSpawner;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
470
|
+
const bao = yield* resolveBao();
|
|
471
|
+
const env = baoEnv();
|
|
472
|
+
if (env === undefined) {
|
|
473
|
+
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."));
|
|
474
|
+
}
|
|
475
|
+
const result = yield* ChildProcess2.make(bao, ["kv", "put", `${mount}/${key}`, `${field}=-`], {
|
|
476
|
+
env,
|
|
477
|
+
extendEnv: false,
|
|
478
|
+
forceKillAfter: Duration4.seconds(1),
|
|
78
479
|
stderr: "pipe",
|
|
79
480
|
stdin: Stream2.fromIterable([new TextEncoder().encode(value)]),
|
|
80
481
|
stdout: "pipe"
|
|
81
|
-
}).pipe(spawner.spawn,
|
|
482
|
+
}).pipe(spawner.spawn, Effect7.flatMap((child) => Effect7.all({
|
|
82
483
|
exitCode: child.exitCode,
|
|
83
|
-
stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString,
|
|
84
|
-
}, { concurrency: "unbounded" })),
|
|
484
|
+
stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString, Effect7.map((text) => text.trim()))
|
|
485
|
+
}, { concurrency: "unbounded" })), Effect7.scoped);
|
|
85
486
|
if (result.exitCode !== 0) {
|
|
86
|
-
return yield*
|
|
487
|
+
return yield* Effect7.fail(new Error(`bao kv put ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
|
|
87
488
|
}
|
|
88
489
|
});
|
|
89
490
|
|
|
@@ -93,12 +494,7 @@ var canonicalText = (text) => text.trimEnd();
|
|
|
93
494
|
var sha256 = (text) => createHash("sha256").update(canonicalText(text)).digest("hex");
|
|
94
495
|
var configDigest = (text) => sha256(text);
|
|
95
496
|
var extractMachineConfigSpec = (wrapperYaml) => {
|
|
96
|
-
let doc;
|
|
97
|
-
try {
|
|
98
|
-
doc = Bun.YAML.parse(wrapperYaml);
|
|
99
|
-
} catch {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
497
|
+
let doc = parseYaml(wrapperYaml);
|
|
102
498
|
if (Array.isArray(doc)) {
|
|
103
499
|
if (doc.length !== 1)
|
|
104
500
|
return;
|
|
@@ -110,12 +506,10 @@ var extractMachineConfigSpec = (wrapperYaml) => {
|
|
|
110
506
|
return typeof spec === "string" ? spec : undefined;
|
|
111
507
|
};
|
|
112
508
|
var kubeconfigMetadata = (yamlText, contextName) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
doc = Bun.YAML.parse(yamlText);
|
|
116
|
-
} catch {
|
|
509
|
+
const parsed = parseYaml(yamlText);
|
|
510
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
117
511
|
return;
|
|
118
|
-
|
|
512
|
+
const doc = parsed;
|
|
119
513
|
const ctx = doc.contexts?.find((row) => row.name === contextName)?.context;
|
|
120
514
|
const clusterName = ctx?.cluster;
|
|
121
515
|
const userName = ctx?.user;
|
|
@@ -140,10 +534,6 @@ var generation = (meta) => sha256(`${meta.context}
|
|
|
140
534
|
${meta.endpoint}
|
|
141
535
|
${meta.caFingerprint}
|
|
142
536
|
${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
537
|
var buildAttrs = (raw, props) => {
|
|
148
538
|
const meta = kubeconfigMetadata(raw, props.context);
|
|
149
539
|
if (meta === undefined)
|
|
@@ -151,7 +541,6 @@ var buildAttrs = (raw, props) => {
|
|
|
151
541
|
return {
|
|
152
542
|
certificateAuthorityFingerprint: meta.caFingerprint,
|
|
153
543
|
clientCertificateFingerprint: meta.clientFingerprint,
|
|
154
|
-
connection: toConnection(props),
|
|
155
544
|
context: props.context,
|
|
156
545
|
credentialGeneration: generation({ ...meta, context: props.context }),
|
|
157
546
|
endpoint: meta.endpoint
|
|
@@ -159,23 +548,68 @@ var buildAttrs = (raw, props) => {
|
|
|
159
548
|
};
|
|
160
549
|
|
|
161
550
|
// src/talos/talosctl.ts
|
|
162
|
-
import * as
|
|
551
|
+
import * as Duration5 from "effect/Duration";
|
|
552
|
+
import * as Effect8 from "effect/Effect";
|
|
163
553
|
import * as Stream3 from "effect/Stream";
|
|
164
554
|
import * as ChildProcess3 from "effect/process/ChildProcess";
|
|
165
555
|
import * as ChildProcessSpawner5 from "effect/process/ChildProcessSpawner";
|
|
166
|
-
|
|
167
556
|
class TalosError extends Error {
|
|
168
557
|
command;
|
|
169
558
|
exitCode;
|
|
170
|
-
constructor(command, exitCode, detail) {
|
|
171
|
-
super(
|
|
559
|
+
constructor(command, exitCode, detail, binary = DEFAULT_TALOSCTL_BINARY) {
|
|
560
|
+
super(`${binary} ${command} -> ${String(exitCode)}: ${detail}`);
|
|
172
561
|
this.command = command;
|
|
173
562
|
this.exitCode = exitCode;
|
|
174
563
|
this.name = "TalosError";
|
|
175
564
|
}
|
|
176
565
|
}
|
|
177
|
-
var
|
|
566
|
+
var TALOSCTL_PINNED_VERSION = "v1.14.2";
|
|
567
|
+
var talosctlBinary = (override) => {
|
|
568
|
+
const fromOption = override?.trim();
|
|
569
|
+
if (fromOption !== undefined && fromOption !== "")
|
|
570
|
+
return fromOption;
|
|
571
|
+
const fromEnv = process.env[TALOSCTL_BINARY_ENV]?.trim();
|
|
572
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
573
|
+
return fromEnv;
|
|
574
|
+
return DEFAULT_TALOSCTL_BINARY;
|
|
575
|
+
};
|
|
576
|
+
var talosctlEnv = () => {
|
|
577
|
+
const env = {};
|
|
578
|
+
for (const name of ["PATH", "HOME", "TMPDIR"]) {
|
|
579
|
+
const value = process.env[name];
|
|
580
|
+
if (value !== undefined && value !== "")
|
|
581
|
+
env[name] = value;
|
|
582
|
+
}
|
|
583
|
+
return env;
|
|
584
|
+
};
|
|
585
|
+
var capture = (binary, argv) => Effect8.gen(function* () {
|
|
178
586
|
const spawner = yield* ChildProcessSpawner5.ChildProcessSpawner;
|
|
587
|
+
return yield* ChildProcess3.make(binary, argv, {
|
|
588
|
+
env: talosctlEnv(),
|
|
589
|
+
extendEnv: false,
|
|
590
|
+
forceKillAfter: Duration5.seconds(1),
|
|
591
|
+
stderr: "pipe",
|
|
592
|
+
stdin: "ignore",
|
|
593
|
+
stdout: "pipe"
|
|
594
|
+
}).pipe(spawner.spawn, Effect8.flatMap((child) => Effect8.all({
|
|
595
|
+
exitCode: child.exitCode,
|
|
596
|
+
stderr: child.stderr.pipe(Stream3.decodeText, Stream3.mkString, Effect8.map((text) => text.trim())),
|
|
597
|
+
stdout: child.stdout.pipe(Stream3.decodeText, Stream3.mkString, Effect8.map((text) => text.trim()))
|
|
598
|
+
}, { concurrency: "unbounded" })), Effect8.scoped);
|
|
599
|
+
});
|
|
600
|
+
var VERSION_PROBE_TIMEOUT = Duration5.seconds(10);
|
|
601
|
+
var reportsPinnedVersion = (stdout) => stdout.split(/[^\w.+-]+/).includes(TALOSCTL_PINNED_VERSION);
|
|
602
|
+
var checkOverrideVersion = (binary, source, timeout = VERSION_PROBE_TIMEOUT) => capture(binary, ["version", "--client"]).pipe(Effect8.timeoutOrElse({
|
|
603
|
+
duration: timeout,
|
|
604
|
+
orElse: () => Effect8.fail(new TalosBinaryRefused("version --client", `${source} did not answer within ${String(Duration5.toSeconds(timeout))}s`, binary))
|
|
605
|
+
}), 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))));
|
|
606
|
+
var talosctl = (args, options) => Effect8.gen(function* () {
|
|
607
|
+
const requested = talosctlBinary(options.binary);
|
|
608
|
+
const resolved = requested === DEFAULT_TALOSCTL_BINARY ? yield* resolveDefault(requested) : requested;
|
|
609
|
+
const fromOption = options.binary?.trim();
|
|
610
|
+
const source = requested === DEFAULT_TALOSCTL_BINARY ? "talosctl on PATH" : fromOption !== undefined && fromOption !== "" ? "the `binary` option" : TALOSCTL_BINARY_ENV;
|
|
611
|
+
const binary = yield* checkBinaryPath(resolved, source);
|
|
612
|
+
yield* checkOverrideVersion(binary, source, options.versionProbeTimeout);
|
|
179
613
|
const argv = [
|
|
180
614
|
...args,
|
|
181
615
|
"--talosconfig",
|
|
@@ -184,119 +618,108 @@ var talosctl = (args, options) => Effect3.gen(function* () {
|
|
|
184
618
|
...options.endpoints === undefined ? [] : ["--endpoints", options.endpoints.join(",")],
|
|
185
619
|
...options.insecure === true ? ["--insecure"] : []
|
|
186
620
|
];
|
|
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);
|
|
621
|
+
const result = yield* capture(binary, argv);
|
|
198
622
|
const command = args.join(" ");
|
|
199
623
|
if (result.exitCode !== 0) {
|
|
200
|
-
return yield*
|
|
624
|
+
return yield* Effect8.fail(new TalosError(command, result.exitCode, result.stderr.slice(0, 300), binary));
|
|
201
625
|
}
|
|
202
626
|
return result.stdout;
|
|
203
627
|
});
|
|
204
|
-
var talosctlOrAlready = (args, options) => talosctl(args, options).pipe(
|
|
628
|
+
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
629
|
|
|
206
630
|
// src/talos/kubeconfig.ts
|
|
207
631
|
var TalosKubeconfig = Resource("Talos.Kubeconfig");
|
|
208
|
-
var
|
|
209
|
-
var
|
|
210
|
-
var readKubeconfig = (props) => Effect4.gen(function* () {
|
|
632
|
+
var readVaultMeta = (props, key) => readKvValue(props.target.mount, key, ["kubeconfig", "config"]).pipe(Effect9.catchIf(isVaultKeyAbsent, () => Effect9.succeed(undefined)));
|
|
633
|
+
var readKubeconfig = (props) => Effect9.gen(function* () {
|
|
211
634
|
const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
|
|
212
635
|
const raw = yield* readVaultMeta(props, key);
|
|
213
636
|
if (raw === undefined)
|
|
214
637
|
return;
|
|
215
638
|
const meta = buildAttrs(raw, props);
|
|
216
639
|
if (meta === undefined) {
|
|
217
|
-
return yield*
|
|
640
|
+
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
641
|
}
|
|
219
642
|
return meta;
|
|
220
643
|
});
|
|
221
|
-
var diffKubeconfig = (news, output) =>
|
|
644
|
+
var diffKubeconfig = (news, output) => Effect9.gen(function* () {
|
|
222
645
|
if (output === undefined || !isResolved(news))
|
|
223
646
|
return;
|
|
224
647
|
const live = yield* readKubeconfig(news);
|
|
225
|
-
if (live !== undefined && live.credentialGeneration === output.credentialGeneration && live.context === output.context && live.endpoint === output.endpoint) {
|
|
648
|
+
if (live !== undefined && live.credentialGeneration === output.credentialGeneration && live.context === output.context && !("connection" in output) && live.endpoint === output.endpoint) {
|
|
226
649
|
return { action: "noop" };
|
|
227
650
|
}
|
|
228
651
|
return { action: "update" };
|
|
229
652
|
});
|
|
230
|
-
var reconcileKubeconfig = (props, output) =>
|
|
653
|
+
var reconcileKubeconfig = (props, output) => Effect9.scoped(Effect9.gen(function* () {
|
|
231
654
|
const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
|
|
232
655
|
if (output !== undefined && output.credentialGeneration !== "") {
|
|
233
656
|
const raw2 = yield* readKvValue(props.target.mount, key, ["kubeconfig", "config"]);
|
|
234
657
|
const meta2 = buildAttrs(raw2, props);
|
|
235
658
|
if (meta2 === undefined) {
|
|
236
|
-
return yield*
|
|
659
|
+
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
660
|
}
|
|
238
661
|
return meta2;
|
|
239
662
|
}
|
|
240
663
|
const credential = yield* mintTalosconfig(props.target);
|
|
241
664
|
const { path: outPath } = yield* reservedTempPath(`${props.target.cluster}-${props.node}-kubeconfig-out`);
|
|
242
665
|
yield* talosctl(["kubeconfig", outPath, "--merge=false", "--force", "--force-context-name", props.context], { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
243
|
-
const raw = yield*
|
|
666
|
+
const raw = yield* Effect9.tryPromise({
|
|
244
667
|
catch: (cause) => new Error(`${outPath}: reading generated kubeconfig: ${String(cause)}`),
|
|
245
|
-
try: () =>
|
|
668
|
+
try: () => readFile(outPath, "utf8")
|
|
246
669
|
});
|
|
247
670
|
if (raw.trim() === "") {
|
|
248
|
-
return yield*
|
|
671
|
+
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
672
|
}
|
|
250
|
-
yield*
|
|
673
|
+
yield* Effect9.try({
|
|
251
674
|
catch: (cause) => new Error(`${outPath}: chmod 0600: ${String(cause)}`),
|
|
252
675
|
try: () => chmodSync(outPath, 384)
|
|
253
676
|
});
|
|
254
677
|
const meta = buildAttrs(raw, props);
|
|
255
678
|
if (meta === undefined) {
|
|
256
|
-
return yield*
|
|
679
|
+
return yield* Effect9.die(new Error(`${props.node}: talosctl kubeconfig wrote a file that does not parse for context ` + `${props.context}.`));
|
|
257
680
|
}
|
|
258
681
|
yield* writeKvValue(props.target.mount, key, "kubeconfig", raw);
|
|
259
682
|
return meta;
|
|
260
683
|
}));
|
|
261
684
|
var handlers = {
|
|
262
|
-
delete: () =>
|
|
685
|
+
delete: () => Effect9.void,
|
|
263
686
|
diff: ({
|
|
264
687
|
news,
|
|
265
688
|
output
|
|
266
689
|
}) => diffKubeconfig(news, output),
|
|
267
|
-
list: () =>
|
|
690
|
+
list: () => Effect9.succeed([]),
|
|
268
691
|
read: ({ olds }) => readKubeconfig(olds),
|
|
269
692
|
reconcile: ({
|
|
270
693
|
news,
|
|
271
694
|
output
|
|
272
695
|
}) => reconcileKubeconfig(news, output)
|
|
273
696
|
};
|
|
274
|
-
var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig,
|
|
697
|
+
var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig, Effect9.succeed(TalosKubeconfig.Provider.of(handlers)));
|
|
275
698
|
// src/talos/talos-bootstrap.ts
|
|
276
699
|
import { Resource as Resource2 } from "alchemy";
|
|
277
700
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
278
701
|
import * as Provider2 from "alchemy/Provider";
|
|
279
|
-
import * as
|
|
702
|
+
import * as Effect11 from "effect/Effect";
|
|
280
703
|
|
|
281
704
|
// src/talos/talos-bootstrap-etcd.ts
|
|
282
|
-
import * as
|
|
705
|
+
import * as Effect10 from "effect/Effect";
|
|
283
706
|
|
|
284
707
|
// src/talos/talos-errors.ts
|
|
285
|
-
import * as
|
|
708
|
+
import * as Data2 from "effect/Data";
|
|
286
709
|
|
|
287
|
-
class TalosConfigDigestMismatch extends
|
|
710
|
+
class TalosConfigDigestMismatch extends Data2.TaggedError("TalosConfigDigestMismatch") {
|
|
288
711
|
get message() {
|
|
289
712
|
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
713
|
}
|
|
291
714
|
}
|
|
292
715
|
|
|
293
|
-
class TalosConvergenceTimeout extends
|
|
716
|
+
class TalosConvergenceTimeout extends Data2.TaggedError("TalosConvergenceTimeout") {
|
|
294
717
|
get message() {
|
|
295
718
|
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
719
|
}
|
|
297
720
|
}
|
|
298
721
|
|
|
299
|
-
class TalosReBootstrapRefused extends
|
|
722
|
+
class TalosReBootstrapRefused extends Data2.TaggedError("TalosReBootstrapRefused") {
|
|
300
723
|
get message() {
|
|
301
724
|
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
725
|
}
|
|
@@ -306,28 +729,28 @@ class TalosReBootstrapRefused extends Data.TaggedError("TalosReBootstrapRefused"
|
|
|
306
729
|
var isBootstrapped = (props, talosconfigPath) => talosctl(["get", "etcdmembers", "-o", "json"], {
|
|
307
730
|
nodes: [props.node],
|
|
308
731
|
talosconfigPath
|
|
309
|
-
}).pipe(
|
|
310
|
-
var assertNoLivePeers = (props, talosconfigPath) =>
|
|
311
|
-
onFailure: (cause) =>
|
|
732
|
+
}).pipe(Effect10.map((text) => text.includes('"id"') || text.includes('"member"')));
|
|
733
|
+
var assertNoLivePeers = (props, talosconfigPath) => Effect10.all((props.peers ?? []).map((peer) => isBootstrapped({ ...props, node: peer }, talosconfigPath).pipe(Effect10.matchEffect({
|
|
734
|
+
onFailure: (cause) => Effect10.fail(new TalosReBootstrapRefused({
|
|
312
735
|
detail: `peer ${peer}: live read failed before bootstrap: ${String(cause)}`,
|
|
313
736
|
node: props.node
|
|
314
737
|
})),
|
|
315
|
-
onSuccess: (hasMembers) => hasMembers ?
|
|
738
|
+
onSuccess: (hasMembers) => hasMembers ? Effect10.fail(new TalosReBootstrapRefused({
|
|
316
739
|
detail: `peer ${peer} already reports etcd members — a cluster may already exist; ` + "refusing to bootstrap a second one",
|
|
317
740
|
node: props.node
|
|
318
|
-
})) :
|
|
741
|
+
})) : Effect10.void
|
|
319
742
|
}))), { discard: true });
|
|
320
743
|
|
|
321
744
|
// src/talos/talos-bootstrap.ts
|
|
322
745
|
var TalosBootstrap = Resource2("Talos.Bootstrap", {
|
|
323
746
|
defaultRemovalPolicy: "retain"
|
|
324
747
|
});
|
|
325
|
-
var readBootstrap = (props) =>
|
|
748
|
+
var readBootstrap = (props) => Effect11.scoped(Effect11.gen(function* () {
|
|
326
749
|
const credential = yield* mintTalosconfig(props.target);
|
|
327
750
|
const bootstrapped = yield* isBootstrapped(props, credential.talosconfigPath);
|
|
328
751
|
return bootstrapped ? { bootstrapped: true, node: props.node } : undefined;
|
|
329
752
|
}));
|
|
330
|
-
var diffBootstrap = (news, output) =>
|
|
753
|
+
var diffBootstrap = (news, output) => Effect11.gen(function* () {
|
|
331
754
|
if (output === undefined || !isResolved2(news))
|
|
332
755
|
return;
|
|
333
756
|
if (output.bootstrapped)
|
|
@@ -335,15 +758,15 @@ var diffBootstrap = (news, output) => Effect6.gen(function* () {
|
|
|
335
758
|
const live = yield* readBootstrap(news);
|
|
336
759
|
return live?.bootstrapped === true ? { action: "noop" } : { action: "update" };
|
|
337
760
|
});
|
|
338
|
-
var reconcileBootstrap = (props, output) =>
|
|
761
|
+
var reconcileBootstrap = (props, output) => Effect11.scoped(Effect11.gen(function* () {
|
|
339
762
|
const credential = yield* mintTalosconfig(props.target);
|
|
340
763
|
if (output?.bootstrapped === true) {
|
|
341
|
-
const stillBootstrapped = yield* isBootstrapped(props, credential.talosconfigPath).pipe(
|
|
764
|
+
const stillBootstrapped = yield* isBootstrapped(props, credential.talosconfigPath).pipe(Effect11.catchIf(() => true, (cause) => Effect11.fail(new TalosReBootstrapRefused({
|
|
342
765
|
detail: `live read failed: ${String(cause)}`,
|
|
343
766
|
node: props.node
|
|
344
767
|
}))));
|
|
345
768
|
if (!stillBootstrapped) {
|
|
346
|
-
return yield*
|
|
769
|
+
return yield* Effect11.fail(new TalosReBootstrapRefused({
|
|
347
770
|
detail: "etcd members are absent on a live read against already-bootstrapped state",
|
|
348
771
|
node: props.node
|
|
349
772
|
}));
|
|
@@ -360,31 +783,104 @@ var reconcileBootstrap = (props, output) => Effect6.scoped(Effect6.gen(function*
|
|
|
360
783
|
}
|
|
361
784
|
const after = yield* isBootstrapped(props, credential.talosconfigPath);
|
|
362
785
|
if (!after) {
|
|
363
|
-
return yield*
|
|
786
|
+
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
787
|
}
|
|
365
788
|
return { bootstrapped: true, node: props.node };
|
|
366
789
|
}));
|
|
367
790
|
var handlers2 = {
|
|
368
|
-
delete: () =>
|
|
791
|
+
delete: () => Effect11.void,
|
|
369
792
|
diff: ({
|
|
370
793
|
news,
|
|
371
794
|
output
|
|
372
795
|
}) => diffBootstrap(news, output),
|
|
373
|
-
list: () =>
|
|
796
|
+
list: () => Effect11.succeed([]),
|
|
374
797
|
read: ({ olds }) => readBootstrap(olds),
|
|
375
798
|
reconcile: ({
|
|
376
799
|
news,
|
|
377
800
|
output
|
|
378
801
|
}) => reconcileBootstrap(news, output)
|
|
379
802
|
};
|
|
380
|
-
var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap,
|
|
381
|
-
// src/talos/talos-cluster-
|
|
803
|
+
var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap, Effect11.succeed(TalosBootstrap.Provider.of(handlers2)));
|
|
804
|
+
// src/talos/talos-cluster-identity.ts
|
|
382
805
|
import { Resource as Resource3 } from "alchemy";
|
|
383
806
|
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
384
807
|
import * as Provider3 from "alchemy/Provider";
|
|
385
|
-
import * as
|
|
386
|
-
var
|
|
808
|
+
import * as Effect12 from "effect/Effect";
|
|
809
|
+
var TalosClusterIdentity = Resource3("Talos.ClusterIdentity");
|
|
810
|
+
var readClusterIdentity = (props, output) => Effect12.gen(function* () {
|
|
811
|
+
const cluster = props.target.cluster;
|
|
812
|
+
const uid = yield* withConnectDeadline(cluster, Effect12.gen(function* () {
|
|
813
|
+
const transport = yield* openTransport(talosVaultLocation(props));
|
|
814
|
+
return yield* readClusterUid(cluster, transport);
|
|
815
|
+
}));
|
|
816
|
+
if (output !== undefined && uid !== output.uid) {
|
|
817
|
+
return yield* Effect12.fail(new TalosClusterMoved({ cluster, live: uid, saved: output.uid }));
|
|
818
|
+
}
|
|
819
|
+
return { uid };
|
|
820
|
+
});
|
|
821
|
+
var reconcileClusterIdentity = (props, output) => readClusterIdentity(props, output);
|
|
822
|
+
var diffClusterIdentity = (news, output) => Effect12.gen(function* () {
|
|
823
|
+
if (output === undefined || !isResolved3(news))
|
|
824
|
+
return;
|
|
825
|
+
yield* reconcileClusterIdentity(news, output);
|
|
826
|
+
return { action: "noop" };
|
|
827
|
+
});
|
|
828
|
+
var handlers3 = {
|
|
829
|
+
delete: () => Effect12.void,
|
|
830
|
+
diff: ({
|
|
831
|
+
news,
|
|
832
|
+
output
|
|
833
|
+
}) => diffClusterIdentity(news, output),
|
|
834
|
+
list: () => Effect12.succeed([]),
|
|
835
|
+
read: ({
|
|
836
|
+
olds,
|
|
837
|
+
output
|
|
838
|
+
}) => readClusterIdentity(olds, output),
|
|
839
|
+
reconcile: ({
|
|
840
|
+
news,
|
|
841
|
+
output
|
|
842
|
+
}) => reconcileClusterIdentity(news, output)
|
|
843
|
+
};
|
|
844
|
+
var TalosClusterIdentityProvider = () => Provider3.effect(TalosClusterIdentity, Effect12.succeed(TalosClusterIdentity.Provider.of(handlers3)));
|
|
845
|
+
// src/talos/talos-cluster-health.ts
|
|
846
|
+
import { Resource as Resource4 } from "alchemy";
|
|
847
|
+
import { isResolved as isResolved4 } from "alchemy/Diff";
|
|
848
|
+
import * as Provider4 from "alchemy/Provider";
|
|
849
|
+
import * as Effect14 from "effect/Effect";
|
|
850
|
+
|
|
851
|
+
// src/talos/literal-connection.ts
|
|
852
|
+
import { hasUnresolvedInputs } from "alchemy/Diff";
|
|
853
|
+
import * as Effect13 from "effect/Effect";
|
|
854
|
+
var literalConnectionRefusal = (connection) => {
|
|
855
|
+
const auth = connection?.auth;
|
|
856
|
+
return hasUnresolvedInputs(connection) || auth?.kind !== "talos-openbao" || typeof auth?.uid !== "string" || auth.uid === "" ? new TalosUidNotLiteral({}) : undefined;
|
|
857
|
+
};
|
|
858
|
+
var withLiteralConnection = (resource) => new Proxy(resource, {
|
|
859
|
+
apply: (target, thisArg, args) => {
|
|
860
|
+
const refusal = (given) => literalConnectionRefusal(given?.connection);
|
|
861
|
+
const props = args[1];
|
|
862
|
+
if (Effect13.isEffect(props)) {
|
|
863
|
+
const checked = props.pipe(Effect13.flatMap((resolved) => {
|
|
864
|
+
const refused2 = refusal(resolved);
|
|
865
|
+
return refused2 === undefined ? Effect13.succeed(resolved) : Effect13.fail(refused2);
|
|
866
|
+
}));
|
|
867
|
+
return Reflect.apply(target, thisArg, [args[0], checked, ...args.slice(2)]);
|
|
868
|
+
}
|
|
869
|
+
const refused = refusal(props);
|
|
870
|
+
return refused === undefined ? Reflect.apply(target, thisArg, args) : Effect13.fail(refused);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// src/talos/talos-cluster-health.ts
|
|
875
|
+
var TalosClusterHealth = withLiteralConnection(Resource4("Talos.ClusterHealth"));
|
|
387
876
|
var nodeCsv = (nodes) => nodes === undefined || nodes.length === 0 ? "" : nodes.join(",");
|
|
877
|
+
var attributes = (props, healthy) => ({
|
|
878
|
+
connection: props.connection,
|
|
879
|
+
controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
|
|
880
|
+
healthy,
|
|
881
|
+
workerNodes: nodeCsv(props.workerNodes)
|
|
882
|
+
});
|
|
883
|
+
var sameConnection = (left, right) => right?.auth?.kind === left.auth.kind && right.auth.uid === left.auth.uid && !Object.hasOwn(right.auth, "cluster");
|
|
388
884
|
var healthArgs = (props, waitTimeout) => {
|
|
389
885
|
const args = ["health", "--wait-timeout", waitTimeout];
|
|
390
886
|
const cp = nodeCsv(props.controlPlaneNodes);
|
|
@@ -395,79 +891,75 @@ var healthArgs = (props, waitTimeout) => {
|
|
|
395
891
|
args.push("--worker-nodes", workers);
|
|
396
892
|
return args;
|
|
397
893
|
};
|
|
398
|
-
var check = (props, waitTimeout) =>
|
|
894
|
+
var check = (props, waitTimeout) => Effect14.scoped(Effect14.gen(function* () {
|
|
399
895
|
const credential = yield* mintTalosconfig(props.target);
|
|
400
896
|
const contact = props.controlPlaneNodes[0];
|
|
401
897
|
if (contact === undefined) {
|
|
402
|
-
return yield*
|
|
898
|
+
return yield* Effect14.fail(new Error(`Talos.ClusterHealth ${props.target.cluster}: controlPlaneNodes is empty — need at ` + "least one node to contact."));
|
|
403
899
|
}
|
|
404
900
|
yield* talosctl(healthArgs(props, waitTimeout), {
|
|
405
901
|
endpoints: [contact],
|
|
406
902
|
nodes: [contact],
|
|
407
903
|
talosconfigPath: credential.talosconfigPath
|
|
408
904
|
});
|
|
409
|
-
return
|
|
410
|
-
controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
|
|
411
|
-
healthy: true,
|
|
412
|
-
workerNodes: nodeCsv(props.workerNodes)
|
|
413
|
-
};
|
|
905
|
+
return attributes(props, true);
|
|
414
906
|
}));
|
|
415
907
|
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))
|
|
908
|
+
var readClusterHealth = (props) => check(props, "5s").pipe(Effect14.catchIf(isTalosError, () => Effect14.succeed(attributes(props, false))));
|
|
909
|
+
var diffClusterHealth = (news, output) => Effect14.gen(function* () {
|
|
910
|
+
const refused = literalConnectionRefusal(news.connection);
|
|
911
|
+
if (refused !== undefined)
|
|
912
|
+
return yield* Effect14.fail(refused);
|
|
913
|
+
if (output === undefined || !isResolved4(news))
|
|
423
914
|
return;
|
|
424
915
|
const live = yield* readClusterHealth(news);
|
|
425
|
-
if (live.healthy)
|
|
916
|
+
if (live.healthy && sameConnection(live.connection, output.connection)) {
|
|
426
917
|
return { action: "noop" };
|
|
918
|
+
}
|
|
427
919
|
return { action: "update" };
|
|
428
920
|
});
|
|
429
|
-
var reconcileClusterHealth = (props) => check(props, props.waitTimeout ?? "20m0s").pipe(
|
|
430
|
-
var
|
|
431
|
-
delete: () =>
|
|
921
|
+
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)}`))));
|
|
922
|
+
var handlers4 = {
|
|
923
|
+
delete: () => Effect14.void,
|
|
432
924
|
diff: ({
|
|
433
925
|
news,
|
|
434
926
|
output
|
|
435
927
|
}) => diffClusterHealth(news, output),
|
|
436
|
-
list: () =>
|
|
928
|
+
list: () => Effect14.succeed([]),
|
|
437
929
|
read: ({ olds }) => readClusterHealth(olds),
|
|
438
930
|
reconcile: ({ news }) => reconcileClusterHealth(news)
|
|
439
931
|
};
|
|
440
|
-
var TalosClusterHealthProvider = () =>
|
|
932
|
+
var TalosClusterHealthProvider = () => Provider4.effect(TalosClusterHealth, Effect14.succeed(TalosClusterHealth.Provider.of(handlers4)));
|
|
441
933
|
// src/talos/talos-machine-config.ts
|
|
442
|
-
import { Resource as
|
|
443
|
-
import * as
|
|
444
|
-
import * as
|
|
934
|
+
import { Resource as Resource5 } from "alchemy";
|
|
935
|
+
import * as Provider5 from "alchemy/Provider";
|
|
936
|
+
import * as Effect17 from "effect/Effect";
|
|
445
937
|
|
|
446
938
|
// src/talos/machine-config-read.ts
|
|
447
939
|
import { Unowned } from "alchemy/AdoptPolicy";
|
|
448
|
-
import { isResolved as
|
|
449
|
-
import * as
|
|
940
|
+
import { isResolved as isResolved5 } from "alchemy/Diff";
|
|
941
|
+
import * as Effect15 from "effect/Effect";
|
|
450
942
|
import * as Result from "effect/Result";
|
|
451
943
|
var liveSpecDigest = (props, talosconfigPath) => talosctl(["get", "machineconfig", "v1alpha1", "-o", "yaml"], {
|
|
452
944
|
endpoints: [props.node],
|
|
453
945
|
nodes: [props.node],
|
|
454
946
|
talosconfigPath
|
|
455
|
-
}).pipe(
|
|
947
|
+
}).pipe(Effect15.flatMap((wrapperYaml) => {
|
|
456
948
|
const spec = extractMachineConfigSpec(wrapperYaml);
|
|
457
|
-
return spec === undefined ?
|
|
949
|
+
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
950
|
}));
|
|
459
951
|
var probeMaintenanceMode = (props, talosconfigPath) => talosctl(["version"], {
|
|
460
952
|
endpoints: [props.node],
|
|
461
953
|
insecure: true,
|
|
462
954
|
nodes: [props.node],
|
|
463
955
|
talosconfigPath
|
|
464
|
-
}).pipe(
|
|
465
|
-
var readMachineConfig = (props) =>
|
|
956
|
+
}).pipe(Effect15.as(true), Effect15.orElseSucceed(() => false));
|
|
957
|
+
var readMachineConfig = (props) => Effect15.scoped(Effect15.gen(function* () {
|
|
466
958
|
const credential = yield* mintTalosconfig(props.target);
|
|
467
|
-
const outcome = yield*
|
|
959
|
+
const outcome = yield* Effect15.result(liveSpecDigest(props, credential.talosconfigPath));
|
|
468
960
|
if (Result.isFailure(outcome)) {
|
|
469
961
|
const reachable = yield* probeMaintenanceMode(props, credential.talosconfigPath);
|
|
470
|
-
return reachable ? undefined : yield*
|
|
962
|
+
return reachable ? undefined : yield* Effect15.fail(outcome.failure);
|
|
471
963
|
}
|
|
472
964
|
const attrs = {
|
|
473
965
|
configDigest: props.configDigest,
|
|
@@ -477,8 +969,8 @@ var readMachineConfig = (props) => Effect8.scoped(Effect8.gen(function* () {
|
|
|
477
969
|
};
|
|
478
970
|
return attrs.converged === false ? Unowned(attrs) : attrs;
|
|
479
971
|
}));
|
|
480
|
-
var diffMachineConfig = (news, output) =>
|
|
481
|
-
if (output === undefined || !
|
|
972
|
+
var diffMachineConfig = (news, output) => Effect15.gen(function* () {
|
|
973
|
+
if (output === undefined || !isResolved5(news))
|
|
482
974
|
return;
|
|
483
975
|
const live = yield* readMachineConfig(news);
|
|
484
976
|
if (live !== undefined && live.converged !== false)
|
|
@@ -487,40 +979,40 @@ var diffMachineConfig = (news, output) => Effect8.gen(function* () {
|
|
|
487
979
|
});
|
|
488
980
|
|
|
489
981
|
// src/talos/machine-config-poll.ts
|
|
490
|
-
import * as
|
|
982
|
+
import * as Effect16 from "effect/Effect";
|
|
491
983
|
var isRebootish = (mode) => mode === "reboot" || mode === "auto";
|
|
492
984
|
var POLICY = {
|
|
493
985
|
immediate: { intervalMs: 2000, maxAttempts: 5 },
|
|
494
986
|
rebootish: { intervalMs: 5000, maxAttempts: 24 }
|
|
495
987
|
};
|
|
496
|
-
var pollUntilMatch = (check2, policy) =>
|
|
988
|
+
var pollUntilMatch = (check2, policy) => Effect16.gen(function* () {
|
|
497
989
|
for (let attempt = 1;attempt <= policy.maxAttempts; attempt++) {
|
|
498
|
-
const matched = yield* check2.pipe(
|
|
990
|
+
const matched = yield* check2.pipe(Effect16.orElseSucceed(() => false));
|
|
499
991
|
if (matched)
|
|
500
992
|
return true;
|
|
501
993
|
if (attempt < policy.maxAttempts)
|
|
502
|
-
yield*
|
|
994
|
+
yield* Effect16.sleep(policy.intervalMs);
|
|
503
995
|
}
|
|
504
996
|
return false;
|
|
505
997
|
});
|
|
506
|
-
var confirmConverged = (node, mode, check2, policies = POLICY) =>
|
|
998
|
+
var confirmConverged = (node, mode, check2, policies = POLICY) => Effect16.gen(function* () {
|
|
507
999
|
const rebootish = isRebootish(mode);
|
|
508
1000
|
const policy = rebootish ? policies.rebootish : policies.immediate;
|
|
509
1001
|
const matched = yield* pollUntilMatch(check2, policy);
|
|
510
1002
|
if (!matched) {
|
|
511
|
-
return yield*
|
|
1003
|
+
return yield* Effect16.fail(new TalosConvergenceTimeout({ attempts: policy.maxAttempts, mode, node }));
|
|
512
1004
|
}
|
|
513
1005
|
return rebootish ? "accepted" : "read-back";
|
|
514
1006
|
});
|
|
515
1007
|
|
|
516
1008
|
// src/talos/talos-machine-config.ts
|
|
517
|
-
var TalosMachineConfig =
|
|
518
|
-
var reconcileMachineConfig = (props, output, pollPolicies) =>
|
|
1009
|
+
var TalosMachineConfig = Resource5("Talos.MachineConfig");
|
|
1010
|
+
var reconcileMachineConfig = (props, output, pollPolicies) => Effect17.scoped(Effect17.gen(function* () {
|
|
519
1011
|
const mode = props.mode ?? "auto";
|
|
520
1012
|
const raw = yield* readKvValue(props.target.mount, props.configKey, ["config"]);
|
|
521
1013
|
const gotDigest = configDigest(raw);
|
|
522
1014
|
if (gotDigest !== props.configDigest) {
|
|
523
|
-
return yield*
|
|
1015
|
+
return yield* Effect17.fail(new TalosConfigDigestMismatch({
|
|
524
1016
|
expected: props.configDigest,
|
|
525
1017
|
got: gotDigest,
|
|
526
1018
|
key: props.configKey,
|
|
@@ -539,7 +1031,7 @@ var reconcileMachineConfig = (props, output, pollPolicies) => Effect10.scoped(Ef
|
|
|
539
1031
|
...isCreate ? ["--insecure"] : []
|
|
540
1032
|
];
|
|
541
1033
|
yield* talosctl(args, { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
|
|
542
|
-
const converged = yield* confirmConverged(props.node, mode, liveSpecDigest(props, credential.talosconfigPath).pipe(
|
|
1034
|
+
const converged = yield* confirmConverged(props.node, mode, liveSpecDigest(props, credential.talosconfigPath).pipe(Effect17.map((live) => live === props.configDigest)), pollPolicies);
|
|
543
1035
|
return {
|
|
544
1036
|
configDigest: props.configDigest,
|
|
545
1037
|
converged,
|
|
@@ -547,30 +1039,51 @@ var reconcileMachineConfig = (props, output, pollPolicies) => Effect10.scoped(Ef
|
|
|
547
1039
|
node: props.node
|
|
548
1040
|
};
|
|
549
1041
|
}));
|
|
550
|
-
var
|
|
551
|
-
delete: () =>
|
|
1042
|
+
var handlers5 = {
|
|
1043
|
+
delete: () => Effect17.void,
|
|
552
1044
|
diff: ({
|
|
553
1045
|
news,
|
|
554
1046
|
output
|
|
555
1047
|
}) => diffMachineConfig(news, output),
|
|
556
|
-
list: () =>
|
|
1048
|
+
list: () => Effect17.succeed([]),
|
|
557
1049
|
read: ({ olds }) => readMachineConfig(olds),
|
|
558
1050
|
reconcile: ({
|
|
559
1051
|
news,
|
|
560
1052
|
output
|
|
561
1053
|
}) => reconcileMachineConfig(news, output)
|
|
562
1054
|
};
|
|
563
|
-
var TalosMachineConfigProvider = () =>
|
|
1055
|
+
var TalosMachineConfigProvider = () => Provider5.effect(TalosMachineConfig, Effect17.succeed(TalosMachineConfig.Provider.of(handlers5)));
|
|
564
1056
|
export {
|
|
1057
|
+
DEFAULT_TALOSCTL_BINARY,
|
|
1058
|
+
TALOSCTL_BINARY_ENV,
|
|
565
1059
|
TalosBootstrap,
|
|
566
1060
|
TalosBootstrapProvider,
|
|
567
1061
|
TalosClusterHealth,
|
|
568
1062
|
TalosClusterHealthProvider,
|
|
1063
|
+
TalosClusterIdentity,
|
|
1064
|
+
TalosClusterIdentityMismatch,
|
|
1065
|
+
TalosClusterIdentityMissing,
|
|
1066
|
+
TalosClusterIdentityProvider,
|
|
1067
|
+
TalosClusterIdentityTimeout,
|
|
1068
|
+
TalosClusterIdentityUnreadable,
|
|
1069
|
+
TalosClusterMoved,
|
|
569
1070
|
TalosKubeconfig,
|
|
570
1071
|
TalosKubeconfigProvider,
|
|
1072
|
+
TalosKubeconfigUnreadable,
|
|
571
1073
|
TalosMachineConfig,
|
|
572
|
-
TalosMachineConfigProvider
|
|
1074
|
+
TalosMachineConfigProvider,
|
|
1075
|
+
TalosOpenBaoAdapter,
|
|
1076
|
+
TalosOpenBaoAmbiguousUid,
|
|
1077
|
+
TalosOpenBaoAuthKind,
|
|
1078
|
+
TalosOpenBaoConnectTimeout,
|
|
1079
|
+
TalosOpenBaoLegacyAuth,
|
|
1080
|
+
TalosOpenBaoUnknownCluster,
|
|
1081
|
+
TalosUidNotLiteral,
|
|
1082
|
+
TalosVaultKeyMissing,
|
|
1083
|
+
connectTalosOpenBao,
|
|
1084
|
+
talosOpenBaoCluster,
|
|
1085
|
+
talosOpenBaoConnection
|
|
573
1086
|
};
|
|
574
1087
|
|
|
575
|
-
//# debugId=
|
|
1088
|
+
//# debugId=E9E48AF60568B1C664756E2164756E21
|
|
576
1089
|
//# sourceMappingURL=index.js.map
|