@homeflare/alchemy 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +10 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +15 -6
  29. package/dist/proxmox/index.js.map +3 -3
  30. package/dist/proxmox/qemu-volume.d.ts +12 -2
  31. package/dist/proxmox/qemu-volume.d.ts.map +1 -1
  32. package/dist/talos/credentials-write.d.ts +46 -0
  33. package/dist/talos/credentials-write.d.ts.map +1 -0
  34. package/dist/talos/credentials.d.ts +51 -9
  35. package/dist/talos/credentials.d.ts.map +1 -1
  36. package/dist/talos/fake-process.d.ts +56 -0
  37. package/dist/talos/fake-process.d.ts.map +1 -0
  38. package/dist/talos/index.d.ts +19 -4
  39. package/dist/talos/index.d.ts.map +1 -1
  40. package/dist/talos/index.js +357 -173
  41. package/dist/talos/index.js.map +16 -10
  42. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  43. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  44. package/dist/talos/kubeconfig.d.ts +41 -23
  45. package/dist/talos/kubeconfig.d.ts.map +1 -1
  46. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  47. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  48. package/dist/talos/machine-config-poll.d.ts +43 -0
  49. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  50. package/dist/talos/machine-config-read.d.ts +49 -0
  51. package/dist/talos/machine-config-read.d.ts.map +1 -0
  52. package/dist/talos/resource.d.ts +18 -0
  53. package/dist/talos/resource.d.ts.map +1 -1
  54. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  55. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  56. package/dist/talos/talos-bootstrap.d.ts +87 -0
  57. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  58. package/dist/talos/talos-cluster-health.d.ts +43 -1
  59. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  60. package/dist/talos/talos-errors.d.ts +68 -0
  61. package/dist/talos/talos-errors.d.ts.map +1 -0
  62. package/dist/talos/talos-machine-config.d.ts +51 -15
  63. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  64. package/dist/talos/talosctl.d.ts +7 -0
  65. package/dist/talos/talosctl.d.ts.map +1 -1
  66. package/dist/talos/values.d.ts +23 -3
  67. package/dist/talos/values.d.ts.map +1 -1
  68. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  69. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  70. package/dist/unifi/acl-rule-form.d.ts +90 -0
  71. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  72. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  73. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  74. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  75. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  76. package/dist/unifi/acl-rule.d.ts +49 -0
  77. package/dist/unifi/acl-rule.d.ts.map +1 -0
  78. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  79. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  80. package/dist/unifi/dns-policy-form.d.ts +97 -0
  81. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  82. package/dist/unifi/dns-policy.d.ts +55 -0
  83. package/dist/unifi/dns-policy.d.ts.map +1 -0
  84. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  85. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  86. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  87. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  89. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  90. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  91. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  92. package/dist/unifi/firewall-policy.d.ts +57 -0
  93. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  94. package/dist/unifi/index.d.ts +7 -0
  95. package/dist/unifi/index.d.ts.map +1 -1
  96. package/dist/unifi/index.js +588 -3
  97. package/dist/unifi/index.js.map +24 -4
  98. package/dist/unifi/paginate.d.ts +0 -12
  99. package/dist/unifi/paginate.d.ts.map +1 -1
  100. package/dist/unifi/providers.d.ts +1 -1
  101. package/dist/unifi/providers.d.ts.map +1 -1
  102. package/dist/unifi/scan-source.d.ts +11 -0
  103. package/dist/unifi/scan-source.d.ts.map +1 -0
  104. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  105. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  106. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  107. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  108. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  109. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  110. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  111. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  112. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  113. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  114. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  115. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  116. package/docs/unifi-acl-rule.md +122 -0
  117. package/docs/unifi-dns-policy.md +75 -0
  118. package/docs/unifi-firewall-policy.md +109 -0
  119. package/docs/unifi-traffic-matching-list.md +82 -0
  120. package/docs/unifi-wifi.md +120 -0
  121. package/docs/unifi.md +10 -5
  122. package/package.json +6 -2
  123. package/src/ceph/auth-entity.ts +74 -0
  124. package/src/ceph/ceph-argv-config.ts +78 -0
  125. package/src/ceph/ceph-argv.test.ts +226 -0
  126. package/src/ceph/ceph-argv.ts +189 -0
  127. package/src/ceph/ceph-auth-form.test.ts +103 -0
  128. package/src/ceph/ceph-auth-form.ts +107 -0
  129. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  130. package/src/ceph/ceph-auth-parse.ts +121 -0
  131. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  132. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  133. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  134. package/src/ceph/ceph-config-options.test.ts +44 -0
  135. package/src/ceph/ceph-config-options.ts +36 -0
  136. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  137. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  138. package/src/ceph/ceph-transport.test.ts +125 -0
  139. package/src/ceph/ceph-transport.ts +117 -0
  140. package/src/ceph/fake-ceph-dial.ts +55 -0
  141. package/src/ceph/index.ts +12 -0
  142. package/src/index.ts +1 -0
  143. package/src/proxmox/qemu-volume.test.ts +153 -4
  144. package/src/proxmox/qemu-volume.ts +71 -14
  145. package/src/talos/credentials-write.test.ts +81 -0
  146. package/src/talos/credentials-write.ts +95 -0
  147. package/src/talos/credentials.test.ts +157 -0
  148. package/src/talos/credentials.ts +160 -90
  149. package/src/talos/fake-process.ts +94 -0
  150. package/src/talos/index.ts +39 -4
  151. package/src/talos/kubeconfig-attrs.ts +55 -0
  152. package/src/talos/kubeconfig.test.ts +256 -0
  153. package/src/talos/kubeconfig.ts +170 -90
  154. package/src/talos/machine-config-fixtures.ts +86 -0
  155. package/src/talos/machine-config-poll.test.ts +57 -0
  156. package/src/talos/machine-config-poll.ts +82 -0
  157. package/src/talos/machine-config-read.ts +118 -0
  158. package/src/talos/resource.ts +19 -0
  159. package/src/talos/secrets-gate.test.ts +63 -0
  160. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  161. package/src/talos/talos-bootstrap.test.ts +218 -0
  162. package/src/talos/talos-bootstrap.ts +151 -45
  163. package/src/talos/talos-cluster-health.test.ts +143 -0
  164. package/src/talos/talos-cluster-health.ts +91 -28
  165. package/src/talos/talos-errors.ts +85 -0
  166. package/src/talos/talos-machine-config.test.ts +189 -0
  167. package/src/talos/talos-machine-config.ts +102 -88
  168. package/src/talos/talosctl.ts +8 -0
  169. package/src/talos/values.test.ts +34 -6
  170. package/src/talos/values.ts +41 -7
  171. package/src/unifi/acl-rule-drift.ts +57 -0
  172. package/src/unifi/acl-rule-form.ts +135 -0
  173. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  174. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  175. package/src/unifi/acl-rule-ordering.ts +70 -0
  176. package/src/unifi/acl-rule.test.ts +183 -0
  177. package/src/unifi/acl-rule.ts +61 -0
  178. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  179. package/src/unifi/dns-policy-drift.ts +48 -0
  180. package/src/unifi/dns-policy-form.ts +142 -0
  181. package/src/unifi/dns-policy.test.ts +154 -0
  182. package/src/unifi/dns-policy.ts +67 -0
  183. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  184. package/src/unifi/firewall-policy-drift.ts +55 -0
  185. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  186. package/src/unifi/firewall-policy-form.ts +146 -0
  187. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  188. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  189. package/src/unifi/firewall-policy-ordering.ts +87 -0
  190. package/src/unifi/firewall-policy.test.ts +183 -0
  191. package/src/unifi/firewall-policy.ts +73 -0
  192. package/src/unifi/index.ts +63 -0
  193. package/src/unifi/paginate.test.ts +24 -0
  194. package/src/unifi/paginate.ts +15 -3
  195. package/src/unifi/providers.ts +19 -1
  196. package/src/unifi/scan-source.ts +64 -0
  197. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  198. package/src/unifi/traffic-matching-list-form.ts +77 -0
  199. package/src/unifi/traffic-matching-list.test.ts +202 -0
  200. package/src/unifi/traffic-matching-list.ts +78 -0
  201. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  202. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  203. package/src/unifi/wifi-broadcast-form.ts +211 -0
  204. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  205. package/src/unifi/wifi-broadcast.test.ts +155 -0
  206. package/src/unifi/wifi-broadcast.ts +90 -0
  207. package/src/unifi/write-op-reference.test.ts +8 -54
@@ -1,8 +1,9 @@
1
1
  // src/talos/kubeconfig.ts
2
+ import { chmodSync } from "node:fs";
2
3
  import { Resource } from "alchemy";
3
4
  import { isResolved } from "alchemy/Diff";
4
5
  import * as Provider from "alchemy/Provider";
5
- import * as Effect3 from "effect/Effect";
6
+ import * as Effect4 from "effect/Effect";
6
7
 
7
8
  // src/talos/credentials.ts
8
9
  import { closeSync, openSync, writeFileSync } from "node:fs";
@@ -10,96 +11,104 @@ import * as Effect from "effect/Effect";
10
11
  import * as Stream from "effect/Stream";
11
12
  import * as ChildProcess from "effect/unstable/process/ChildProcess";
12
13
  import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
13
- var defaultKey = (target) => target.talosconfigKey ?? "data/talosconfig";
14
- var mintTalosconfig = (target) => Effect.scoped(Effect.gen(function* () {
14
+ var DEFAULT_TALOSCONFIG_KEY = "talosconfig";
15
+ var DEFAULT_KUBECONFIG_KEY = "kubeconfig";
16
+ var readKvValue = (mount, key, fields) => Effect.gen(function* () {
15
17
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
16
- const key = defaultKey(target);
17
- const result = yield* ChildProcess.make("bao", ["kv", "get", "-format=json", `${target.mount}/${key}`], { detached: false, extendEnv: true, stderr: "pipe", stdin: "ignore", stdout: "pipe" }).pipe(spawner.spawn, Effect.flatMap((child) => Effect.all({
18
+ const result = yield* ChildProcess.make("bao", ["kv", "get", "-format=json", `${mount}/${key}`], { detached: false, extendEnv: true, stderr: "pipe", stdin: "ignore", stdout: "pipe" }).pipe(spawner.spawn, Effect.flatMap((child) => Effect.all({
18
19
  exitCode: child.exitCode,
19
20
  stderr: child.stderr.pipe(Stream.decodeText, Stream.mkString, Effect.map((text) => text.trim())),
20
21
  stdout: child.stdout.pipe(Stream.decodeText, Stream.mkString, Effect.map((text) => text.trim()))
21
22
  }, { concurrency: "unbounded" })), Effect.scoped);
22
23
  if (result.exitCode !== 0) {
23
- return yield* Effect.fail(new Error(`bao kv get ${target.mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
24
+ return yield* Effect.fail(new Error(`bao kv get ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
24
25
  }
25
26
  const parsed = JSON.parse(result.stdout);
26
27
  const data = parsed.data?.data;
27
- const raw = data?.["talosconfig"] ?? data?.["config"];
28
- if (typeof raw !== "string" || raw.trim() === "") {
29
- return yield* Effect.fail(new Error(`${target.mount}/${key} returned no talosconfig field. Expected a KV key named 'talosconfig' or 'config' — never store the value in Alchemy props.`));
28
+ for (const field of fields) {
29
+ const raw = data?.[field];
30
+ if (typeof raw === "string" && raw.trim() !== "")
31
+ return raw;
30
32
  }
31
- const talosconfigPath = `${Bun.env["TMPDIR"] ?? "/tmp"}/hf-talos-${target.cluster}-${Bun.randomUUIDv7()}.yaml`;
32
- yield* Effect.try({
33
+ return yield* Effect.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."));
34
+ });
35
+ var mintKvTempFile = (raw, label) => Effect.gen(function* () {
36
+ const path = `${Bun.env["TMPDIR"] ?? "/tmp"}/hf-talos-${label}-${Bun.randomUUIDv7()}.yaml`;
37
+ yield* Effect.acquireRelease(Effect.try({
33
38
  try: () => {
34
- const fd = openSync(talosconfigPath, "wx", 384);
39
+ const fd = openSync(path, "wx", 384);
35
40
  try {
36
41
  writeFileSync(fd, raw);
37
42
  } finally {
38
43
  closeSync(fd);
39
44
  }
40
45
  },
41
- catch: (cause) => new Error(`writing temp talosconfig: ${String(cause)}`)
42
- });
43
- yield* Effect.addFinalizer(() => Effect.tryPromise({
44
- try: () => Bun.file(talosconfigPath).delete(),
45
- catch: () => {
46
- return;
47
- }
48
- }).pipe(Effect.orElseSucceed(() => {
46
+ catch: (cause) => new Error(`writing ${label} temp file: ${String(cause)}`)
47
+ }), () => Effect.tryPromise({ try: () => Bun.file(path).delete(), catch: () => {
48
+ return;
49
+ } }).pipe(Effect.orElseSucceed(() => {
49
50
  return;
50
51
  })));
51
- return { talosconfigPath };
52
- }));
52
+ return { path };
53
+ });
54
+ var mintTalosconfig = (target) => readKvValue(target.mount, target.talosconfigKey ?? DEFAULT_TALOSCONFIG_KEY, [
55
+ "talosconfig",
56
+ "config"
57
+ ]).pipe(Effect.flatMap((raw) => mintKvTempFile(raw, `${target.cluster}-talosconfig`)), Effect.map(({ path }) => ({ talosconfigPath: path })));
53
58
 
54
- // src/talos/talosctl.ts
59
+ // src/talos/credentials-write.ts
55
60
  import * as Effect2 from "effect/Effect";
56
61
  import * as Stream2 from "effect/Stream";
57
62
  import * as ChildProcess2 from "effect/unstable/process/ChildProcess";
58
63
  import * as ChildProcessSpawner3 from "effect/unstable/process/ChildProcessSpawner";
59
-
60
- class TalosError extends Error {
61
- command;
62
- exitCode;
63
- constructor(command, exitCode, detail) {
64
- super(`talosctl ${command} -> ${String(exitCode)}: ${detail}`);
65
- this.command = command;
66
- this.exitCode = exitCode;
67
- this.name = "TalosError";
68
- }
69
- }
70
- var talosctl = (args, options) => Effect2.gen(function* () {
64
+ var reservedTempPath = (label) => Effect2.gen(function* () {
65
+ const path = `${Bun.env["TMPDIR"] ?? "/tmp"}/hf-talos-${label}-${Bun.randomUUIDv7()}.yaml`;
66
+ yield* Effect2.addFinalizer(() => Effect2.tryPromise({ try: () => Bun.file(path).delete(), catch: () => {
67
+ return;
68
+ } }).pipe(Effect2.orElseSucceed(() => {
69
+ return;
70
+ })));
71
+ return { path };
72
+ });
73
+ var writeKvValue = (mount, key, field, value) => Effect2.gen(function* () {
71
74
  const spawner = yield* ChildProcessSpawner3.ChildProcessSpawner;
72
- const argv = [
73
- ...args,
74
- "--talosconfig",
75
- options.talosconfigPath,
76
- ...options.nodes === undefined ? [] : ["--nodes", options.nodes.join(",")],
77
- ...options.endpoints === undefined ? [] : ["--endpoints", options.endpoints.join(",")]
78
- ];
79
- const result = yield* ChildProcess2.make("talosctl", argv, {
75
+ const result = yield* ChildProcess2.make("bao", ["kv", "put", `${mount}/${key}`, `${field}=-`], {
80
76
  detached: false,
81
77
  extendEnv: true,
82
78
  stderr: "pipe",
83
- stdin: "ignore",
79
+ stdin: Stream2.fromIterable([new TextEncoder().encode(value)]),
84
80
  stdout: "pipe"
85
81
  }).pipe(spawner.spawn, Effect2.flatMap((child) => Effect2.all({
86
82
  exitCode: child.exitCode,
87
- stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString, Effect2.map((text) => text.trim())),
88
- stdout: child.stdout.pipe(Stream2.decodeText, Stream2.mkString, Effect2.map((text) => text.trim()))
83
+ stderr: child.stderr.pipe(Stream2.decodeText, Stream2.mkString, Effect2.map((text) => text.trim()))
89
84
  }, { concurrency: "unbounded" })), Effect2.scoped);
90
- const command = args.join(" ");
91
85
  if (result.exitCode !== 0) {
92
- return yield* Effect2.fail(new TalosError(command, result.exitCode, result.stderr.slice(0, 300)));
86
+ return yield* Effect2.fail(new Error(`bao kv put ${mount}/${key} exited ${String(result.exitCode)}: ${result.stderr}`));
93
87
  }
94
- return result.stdout;
95
88
  });
96
- var talosctlOrAlready = (args, options) => talosctl(args, options).pipe(Effect2.catchIf((cause) => cause instanceof TalosError && cause.exitCode !== 0 && /already|exists|bootstrapped/i.test(cause.message), () => Effect2.succeed("")));
97
89
 
98
90
  // src/talos/values.ts
99
91
  import { createHash } from "node:crypto";
100
92
  var canonicalText = (text) => text.trimEnd();
101
93
  var sha256 = (text) => createHash("sha256").update(canonicalText(text)).digest("hex");
102
94
  var configDigest = (text) => sha256(text);
95
+ var extractMachineConfigSpec = (wrapperYaml) => {
96
+ let doc;
97
+ try {
98
+ doc = Bun.YAML.parse(wrapperYaml);
99
+ } catch {
100
+ return;
101
+ }
102
+ if (Array.isArray(doc)) {
103
+ if (doc.length !== 1)
104
+ return;
105
+ doc = doc[0];
106
+ }
107
+ if (doc === null || typeof doc !== "object")
108
+ return;
109
+ const spec = doc.spec;
110
+ return typeof spec === "string" ? spec : undefined;
111
+ };
103
112
  var kubeconfigMetadata = (yamlText, contextName) => {
104
113
  let doc;
105
114
  try {
@@ -125,27 +134,18 @@ var kubeconfigMetadata = (yamlText, contextName) => {
125
134
  endpoint
126
135
  };
127
136
  };
128
- var resolveConfigPath = (stackDir, configFile) => configFile.startsWith("/") ? configFile : `${stackDir}/${configFile}`;
129
137
 
130
- // src/talos/kubeconfig.ts
131
- var TalosKubeconfig = Resource("Talos.Kubeconfig");
138
+ // src/talos/kubeconfig-attrs.ts
132
139
  var generation = (meta) => sha256(`${meta.context}
133
140
  ${meta.endpoint}
134
141
  ${meta.caFingerprint}
135
142
  ${meta.clientFingerprint}`);
143
+ var UNWIRED_KUBECONFIG_PATH = "/talos-first-boot-unwired/wire-Kubernetes.ClusterAdapter-to-mintKubeconfig";
136
144
  var toConnection = (props) => ({
137
- auth: { context: props.context, kind: "kubeconfig", path: props.runtimePath }
145
+ auth: { context: props.context, kind: "kubeconfig", path: UNWIRED_KUBECONFIG_PATH }
138
146
  });
139
- var readFileMeta = (props) => Effect3.gen(function* () {
140
- const text = yield* Effect3.tryPromise({
141
- try: () => Bun.file(props.runtimePath).text(),
142
- catch: (cause) => new Error(String(cause))
143
- }).pipe(Effect3.orElseSucceed(() => {
144
- return;
145
- }));
146
- if (text === undefined)
147
- return;
148
- const meta = kubeconfigMetadata(text, props.context);
147
+ var buildAttrs = (raw, props) => {
148
+ const meta = kubeconfigMetadata(raw, props.context);
149
149
  if (meta === undefined)
150
150
  return;
151
151
  return {
@@ -156,80 +156,203 @@ var readFileMeta = (props) => Effect3.gen(function* () {
156
156
  credentialGeneration: generation({ ...meta, context: props.context }),
157
157
  endpoint: meta.endpoint
158
158
  };
159
+ };
160
+
161
+ // src/talos/talosctl.ts
162
+ import * as Effect3 from "effect/Effect";
163
+ import * as Stream3 from "effect/Stream";
164
+ import * as ChildProcess3 from "effect/unstable/process/ChildProcess";
165
+ import * as ChildProcessSpawner5 from "effect/unstable/process/ChildProcessSpawner";
166
+
167
+ class TalosError extends Error {
168
+ command;
169
+ exitCode;
170
+ constructor(command, exitCode, detail) {
171
+ super(`talosctl ${command} -> ${String(exitCode)}: ${detail}`);
172
+ this.command = command;
173
+ this.exitCode = exitCode;
174
+ this.name = "TalosError";
175
+ }
176
+ }
177
+ var talosctl = (args, options) => Effect3.gen(function* () {
178
+ const spawner = yield* ChildProcessSpawner5.ChildProcessSpawner;
179
+ const argv = [
180
+ ...args,
181
+ "--talosconfig",
182
+ options.talosconfigPath,
183
+ ...options.nodes === undefined ? [] : ["--nodes", options.nodes.join(",")],
184
+ ...options.endpoints === undefined ? [] : ["--endpoints", options.endpoints.join(",")],
185
+ ...options.insecure === true ? ["--insecure"] : []
186
+ ];
187
+ const result = yield* ChildProcess3.make("talosctl", argv, {
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);
198
+ const command = args.join(" ");
199
+ if (result.exitCode !== 0) {
200
+ return yield* Effect3.fail(new TalosError(command, result.exitCode, result.stderr.slice(0, 300)));
201
+ }
202
+ return result.stdout;
203
+ });
204
+ var talosctlOrAlready = (args, options) => talosctl(args, options).pipe(Effect3.catchIf((cause) => cause instanceof TalosError && cause.exitCode !== 0 && /already|exists|bootstrapped/i.test(cause.message), () => Effect3.succeed("")));
205
+
206
+ // src/talos/kubeconfig.ts
207
+ var TalosKubeconfig = Resource("Talos.Kubeconfig");
208
+ var isVaultKeyAbsent = (error) => error instanceof Error && /no value found at/i.test(error.message);
209
+ var readVaultMeta = (props, key) => readKvValue(props.target.mount, key, ["kubeconfig", "config"]).pipe(Effect4.catchIf(isVaultKeyAbsent, () => Effect4.succeed(undefined)));
210
+ var readKubeconfig = (props) => Effect4.gen(function* () {
211
+ const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
212
+ const raw = yield* readVaultMeta(props, key);
213
+ if (raw === undefined)
214
+ return;
215
+ const meta = buildAttrs(raw, props);
216
+ if (meta === undefined) {
217
+ return yield* Effect4.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
+ }
219
+ return meta;
159
220
  });
160
- var read = (props) => readFileMeta(props).pipe(Effect3.map((meta) => meta === undefined ? {
161
- certificateAuthorityFingerprint: "",
162
- clientCertificateFingerprint: "",
163
- connection: toConnection(props),
164
- context: props.context,
165
- credentialGeneration: "",
166
- endpoint: ""
167
- } : meta));
168
- var diff = (news, output) => Effect3.gen(function* () {
221
+ var diffKubeconfig = (news, output) => Effect4.gen(function* () {
169
222
  if (output === undefined || !isResolved(news))
170
223
  return;
171
- const live = yield* read(news);
172
- if (live.credentialGeneration !== "" && output.credentialGeneration === live.credentialGeneration && output.context === live.context && output.endpoint === live.endpoint) {
224
+ const live = yield* readKubeconfig(news);
225
+ if (live !== undefined && live.credentialGeneration === output.credentialGeneration && live.context === output.context && live.endpoint === output.endpoint) {
173
226
  return { action: "noop" };
174
227
  }
175
228
  return { action: "update" };
176
229
  });
177
- var reconcile = (props) => Effect3.gen(function* () {
230
+ var reconcileKubeconfig = (props, output) => Effect4.scoped(Effect4.gen(function* () {
231
+ const key = props.kubeconfigKey ?? DEFAULT_KUBECONFIG_KEY;
232
+ if (output !== undefined && output.credentialGeneration !== "") {
233
+ const raw2 = yield* readKvValue(props.target.mount, key, ["kubeconfig", "config"]);
234
+ const meta2 = buildAttrs(raw2, props);
235
+ if (meta2 === undefined) {
236
+ return yield* Effect4.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
+ }
238
+ return meta2;
239
+ }
178
240
  const credential = yield* mintTalosconfig(props.target);
179
- yield* talosctl([
180
- "kubeconfig",
181
- props.runtimePath,
182
- "--merge=false",
183
- "--force",
184
- "--force-context-name",
185
- props.context
186
- ], { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
187
- const meta = yield* readFileMeta(props);
241
+ const { path: outPath } = yield* reservedTempPath(`${props.target.cluster}-${props.node}-kubeconfig-out`);
242
+ yield* talosctl(["kubeconfig", outPath, "--merge=false", "--force", "--force-context-name", props.context], { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
243
+ const raw = yield* Effect4.tryPromise({
244
+ catch: (cause) => new Error(`${outPath}: reading generated kubeconfig: ${String(cause)}`),
245
+ try: () => Bun.file(outPath).text()
246
+ });
247
+ if (raw.trim() === "") {
248
+ return yield* Effect4.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
+ }
250
+ yield* Effect4.try({
251
+ catch: (cause) => new Error(`${outPath}: chmod 0600: ${String(cause)}`),
252
+ try: () => chmodSync(outPath, 384)
253
+ });
254
+ const meta = buildAttrs(raw, props);
188
255
  if (meta === undefined) {
189
- return yield* Effect3.die(new Error(`${props.runtimePath}: talosctl kubeconfig returned no error but the file is missing ` + "or unparsable. Read back rather than trusting the exit code alone."));
256
+ return yield* Effect4.die(new Error(`${props.node}: talosctl kubeconfig wrote a file that does not parse for context ` + `${props.context}.`));
190
257
  }
258
+ yield* writeKvValue(props.target.mount, key, "kubeconfig", raw);
191
259
  return meta;
192
- });
260
+ }));
193
261
  var handlers = {
194
- delete: () => Effect3.void,
262
+ delete: () => Effect4.void,
195
263
  diff: ({
196
264
  news,
197
265
  output
198
- }) => diff(news, output),
199
- list: () => Effect3.succeed([]),
200
- read: ({ olds }) => read(olds),
201
- reconcile: ({ news }) => reconcile(news)
266
+ }) => diffKubeconfig(news, output),
267
+ list: () => Effect4.succeed([]),
268
+ read: ({ olds }) => readKubeconfig(olds),
269
+ reconcile: ({
270
+ news,
271
+ output
272
+ }) => reconcileKubeconfig(news, output)
202
273
  };
203
- var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig, Effect3.succeed(TalosKubeconfig.Provider.of(handlers)));
274
+ var TalosKubeconfigProvider = () => Provider.effect(TalosKubeconfig, Effect4.succeed(TalosKubeconfig.Provider.of(handlers)));
204
275
  // src/talos/talos-bootstrap.ts
205
276
  import { Resource as Resource2 } from "alchemy";
206
277
  import { isResolved as isResolved2 } from "alchemy/Diff";
207
278
  import * as Provider2 from "alchemy/Provider";
208
- import * as Effect4 from "effect/Effect";
209
- var TalosBootstrap = Resource2("Talos.Bootstrap", {
210
- defaultRemovalPolicy: "retain"
211
- });
279
+ import * as Effect6 from "effect/Effect";
280
+
281
+ // src/talos/talos-bootstrap-etcd.ts
282
+ import * as Effect5 from "effect/Effect";
283
+
284
+ // src/talos/talos-errors.ts
285
+ import * as Data from "effect/Data";
286
+
287
+ class TalosConfigDigestMismatch extends Data.TaggedError("TalosConfigDigestMismatch") {
288
+ get message() {
289
+ 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
+ }
291
+ }
292
+
293
+ class TalosConvergenceTimeout extends Data.TaggedError("TalosConvergenceTimeout") {
294
+ get message() {
295
+ 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
+ }
297
+ }
298
+
299
+ class TalosReBootstrapRefused extends Data.TaggedError("TalosReBootstrapRefused") {
300
+ get message() {
301
+ 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
+ }
303
+ }
304
+
305
+ // src/talos/talos-bootstrap-etcd.ts
212
306
  var isBootstrapped = (props, talosconfigPath) => talosctl(["get", "etcdmembers", "-o", "json"], {
213
307
  nodes: [props.node],
214
308
  talosconfigPath
215
- }).pipe(Effect4.map((text) => text.includes('"id"') || text.includes('"member"')), Effect4.orElseSucceed(() => false));
216
- var read2 = (props) => Effect4.gen(function* () {
309
+ }).pipe(Effect5.map((text) => text.includes('"id"') || text.includes('"member"')));
310
+ var assertNoLivePeers = (props, talosconfigPath) => Effect5.all((props.peers ?? []).map((peer) => isBootstrapped({ ...props, node: peer }, talosconfigPath).pipe(Effect5.matchEffect({
311
+ onFailure: (cause) => Effect5.fail(new TalosReBootstrapRefused({
312
+ detail: `peer ${peer}: live read failed before bootstrap: ${String(cause)}`,
313
+ node: props.node
314
+ })),
315
+ onSuccess: (hasMembers) => hasMembers ? Effect5.fail(new TalosReBootstrapRefused({
316
+ detail: `peer ${peer} already reports etcd members — a cluster may already exist; ` + "refusing to bootstrap a second one",
317
+ node: props.node
318
+ })) : Effect5.void
319
+ }))), { discard: true });
320
+
321
+ // src/talos/talos-bootstrap.ts
322
+ var TalosBootstrap = Resource2("Talos.Bootstrap", {
323
+ defaultRemovalPolicy: "retain"
324
+ });
325
+ var readBootstrap = (props) => Effect6.scoped(Effect6.gen(function* () {
217
326
  const credential = yield* mintTalosconfig(props.target);
218
327
  const bootstrapped = yield* isBootstrapped(props, credential.talosconfigPath);
219
- return { bootstrapped, node: props.node };
220
- });
221
- var diff2 = (news, output) => Effect4.gen(function* () {
328
+ return bootstrapped ? { bootstrapped: true, node: props.node } : undefined;
329
+ }));
330
+ var diffBootstrap = (news, output) => Effect6.gen(function* () {
222
331
  if (output === undefined || !isResolved2(news))
223
332
  return;
224
- const live = yield* read2(news);
225
- if (live.bootstrapped)
333
+ if (output.bootstrapped)
226
334
  return { action: "noop" };
227
- return { action: "update" };
335
+ const live = yield* readBootstrap(news);
336
+ return live?.bootstrapped === true ? { action: "noop" } : { action: "update" };
228
337
  });
229
- var reconcile2 = (props) => Effect4.gen(function* () {
338
+ var reconcileBootstrap = (props, output) => Effect6.scoped(Effect6.gen(function* () {
230
339
  const credential = yield* mintTalosconfig(props.target);
340
+ if (output?.bootstrapped === true) {
341
+ const stillBootstrapped = yield* isBootstrapped(props, credential.talosconfigPath).pipe(Effect6.catchIf(() => true, (cause) => Effect6.fail(new TalosReBootstrapRefused({
342
+ detail: `live read failed: ${String(cause)}`,
343
+ node: props.node
344
+ }))));
345
+ if (!stillBootstrapped) {
346
+ return yield* Effect6.fail(new TalosReBootstrapRefused({
347
+ detail: "etcd members are absent on a live read against already-bootstrapped state",
348
+ node: props.node
349
+ }));
350
+ }
351
+ return { bootstrapped: true, node: props.node };
352
+ }
231
353
  const before = yield* isBootstrapped(props, credential.talosconfigPath);
232
354
  if (!before) {
355
+ yield* assertNoLivePeers(props, credential.talosconfigPath);
233
356
  yield* talosctlOrAlready(["bootstrap"], {
234
357
  nodes: [props.node],
235
358
  talosconfigPath: credential.talosconfigPath
@@ -237,26 +360,29 @@ var reconcile2 = (props) => Effect4.gen(function* () {
237
360
  }
238
361
  const after = yield* isBootstrapped(props, credential.talosconfigPath);
239
362
  if (!after) {
240
- return yield* Effect4.die(new Error(`${props.node}: bootstrap returned no error but etcd members are still absent. ` + "Read back rather than trusting the exit code alone."));
363
+ return yield* Effect6.die(new Error(`${props.node}: bootstrap returned no error but etcd members are still absent. ` + "Read back rather than trusting the exit code alone."));
241
364
  }
242
365
  return { bootstrapped: true, node: props.node };
243
- });
366
+ }));
244
367
  var handlers2 = {
245
- delete: () => Effect4.void,
368
+ delete: () => Effect6.void,
246
369
  diff: ({
247
370
  news,
248
371
  output
249
- }) => diff2(news, output),
250
- list: () => Effect4.succeed([]),
251
- read: ({ olds }) => read2(olds),
252
- reconcile: ({ news }) => reconcile2(news)
372
+ }) => diffBootstrap(news, output),
373
+ list: () => Effect6.succeed([]),
374
+ read: ({ olds }) => readBootstrap(olds),
375
+ reconcile: ({
376
+ news,
377
+ output
378
+ }) => reconcileBootstrap(news, output)
253
379
  };
254
- var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap, Effect4.succeed(TalosBootstrap.Provider.of(handlers2)));
380
+ var TalosBootstrapProvider = () => Provider2.effect(TalosBootstrap, Effect6.succeed(TalosBootstrap.Provider.of(handlers2)));
255
381
  // src/talos/talos-cluster-health.ts
256
382
  import { Resource as Resource3 } from "alchemy";
257
383
  import { isResolved as isResolved3 } from "alchemy/Diff";
258
384
  import * as Provider3 from "alchemy/Provider";
259
- import * as Effect5 from "effect/Effect";
385
+ import * as Effect7 from "effect/Effect";
260
386
  var TalosClusterHealth = Resource3("Talos.ClusterHealth");
261
387
  var nodeCsv = (nodes) => nodes === undefined || nodes.length === 0 ? "" : nodes.join(",");
262
388
  var healthArgs = (props, waitTimeout) => {
@@ -269,10 +395,15 @@ var healthArgs = (props, waitTimeout) => {
269
395
  args.push("--worker-nodes", workers);
270
396
  return args;
271
397
  };
272
- var check = (props, waitTimeout) => Effect5.gen(function* () {
398
+ var check = (props, waitTimeout) => Effect7.scoped(Effect7.gen(function* () {
273
399
  const credential = yield* mintTalosconfig(props.target);
400
+ const contact = props.controlPlaneNodes[0];
401
+ if (contact === undefined) {
402
+ return yield* Effect7.fail(new Error(`Talos.ClusterHealth ${props.target.cluster}: controlPlaneNodes is empty — need at ` + "least one node to contact."));
403
+ }
274
404
  yield* talosctl(healthArgs(props, waitTimeout), {
275
- nodes: [...props.controlPlaneNodes],
405
+ endpoints: [contact],
406
+ nodes: [contact],
276
407
  talosconfigPath: credential.talosconfigPath
277
408
  });
278
409
  return {
@@ -280,113 +411,166 @@ var check = (props, waitTimeout) => Effect5.gen(function* () {
280
411
  healthy: true,
281
412
  workerNodes: nodeCsv(props.workerNodes)
282
413
  };
283
- });
284
- var read3 = (props) => check(props, "5s").pipe(Effect5.orElseSucceed(() => ({
414
+ }));
415
+ var isTalosError = (cause) => cause instanceof TalosError;
416
+ var readClusterHealth = (props) => check(props, "5s").pipe(Effect7.catchIf(isTalosError, () => Effect7.succeed({
285
417
  controlPlaneNodes: nodeCsv(props.controlPlaneNodes),
286
418
  healthy: false,
287
419
  workerNodes: nodeCsv(props.workerNodes)
288
420
  })));
289
- var diff3 = (news, output) => Effect5.gen(function* () {
421
+ var diffClusterHealth = (news, output) => Effect7.gen(function* () {
290
422
  if (output === undefined || !isResolved3(news))
291
423
  return;
292
- const live = yield* read3(news);
424
+ const live = yield* readClusterHealth(news);
293
425
  if (live.healthy)
294
426
  return { action: "noop" };
295
427
  return { action: "update" };
296
428
  });
297
- var reconcile3 = (props) => check(props, props.waitTimeout ?? "20m0s").pipe(Effect5.mapError((cause) => new Error(`Talos.ClusterHealth ${props.target.cluster}: cluster not healthy within ` + `${props.waitTimeout ?? "20m0s"} — ${String(cause)}`)));
429
+ var reconcileClusterHealth = (props) => check(props, props.waitTimeout ?? "20m0s").pipe(Effect7.catchIf(isTalosError, (cause) => Effect7.fail(new Error(`Talos.ClusterHealth ${props.target.cluster}: cluster not healthy within ` + `${props.waitTimeout ?? "20m0s"} — ${String(cause)}`))));
298
430
  var handlers3 = {
299
- delete: () => Effect5.void,
431
+ delete: () => Effect7.void,
300
432
  diff: ({
301
433
  news,
302
434
  output
303
- }) => diff3(news, output),
304
- list: () => Effect5.succeed([]),
305
- read: ({ olds }) => read3(olds),
306
- reconcile: ({ news }) => reconcile3(news)
435
+ }) => diffClusterHealth(news, output),
436
+ list: () => Effect7.succeed([]),
437
+ read: ({ olds }) => readClusterHealth(olds),
438
+ reconcile: ({ news }) => reconcileClusterHealth(news)
307
439
  };
308
- var TalosClusterHealthProvider = () => Provider3.effect(TalosClusterHealth, Effect5.succeed(TalosClusterHealth.Provider.of(handlers3)));
440
+ var TalosClusterHealthProvider = () => Provider3.effect(TalosClusterHealth, Effect7.succeed(TalosClusterHealth.Provider.of(handlers3)));
309
441
  // src/talos/talos-machine-config.ts
310
442
  import { Resource as Resource4 } from "alchemy";
311
- import { isResolved as isResolved4 } from "alchemy/Diff";
312
443
  import * as Provider4 from "alchemy/Provider";
313
- import * as Effect6 from "effect/Effect";
314
- var STACK_DIR = new URL("..", import.meta.url).pathname;
315
- var TalosMachineConfig = Resource4("Talos.MachineConfig");
316
- var loadDigest = (props) => Effect6.gen(function* () {
317
- const path = resolveConfigPath(STACK_DIR, props.configFile);
318
- const text = yield* Effect6.tryPromise({
319
- try: () => Bun.file(path).text(),
320
- catch: (cause) => new Error(`reading ${props.configFile}: ${String(cause)}`)
321
- });
322
- return configDigest(text);
323
- });
324
- var liveDigest = (props, talosconfigPath) => talosctl(["get", "machineconfig", "-o", "yaml"], {
444
+ import * as Effect10 from "effect/Effect";
445
+
446
+ // src/talos/machine-config-read.ts
447
+ import { Unowned } from "alchemy/AdoptPolicy";
448
+ import { isResolved as isResolved4 } from "alchemy/Diff";
449
+ import * as Effect8 from "effect/Effect";
450
+ import * as Result from "effect/Result";
451
+ var liveSpecDigest = (props, talosconfigPath) => talosctl(["get", "machineconfig", "v1alpha1", "-o", "yaml"], {
452
+ endpoints: [props.node],
325
453
  nodes: [props.node],
326
454
  talosconfigPath
327
- }).pipe(Effect6.map((yaml) => configDigest(yaml)), Effect6.orElseSucceed(() => {
328
- return;
455
+ }).pipe(Effect8.flatMap((wrapperYaml) => {
456
+ const spec = extractMachineConfigSpec(wrapperYaml);
457
+ return spec === undefined ? Effect8.fail(new Error(`${props.node}: get machineconfig v1alpha1 returned no string 'spec' field — see ` + "values.ts extractMachineConfigSpec for the expected shape.")) : Effect8.succeed(configDigest(spec));
329
458
  }));
330
- var read4 = (props) => Effect6.gen(function* () {
331
- const wanted = yield* loadDigest(props);
459
+ var probeMaintenanceMode = (props, talosconfigPath) => talosctl(["version"], {
460
+ endpoints: [props.node],
461
+ insecure: true,
462
+ nodes: [props.node],
463
+ talosconfigPath
464
+ }).pipe(Effect8.as(true), Effect8.orElseSucceed(() => false));
465
+ var readMachineConfig = (props) => Effect8.scoped(Effect8.gen(function* () {
332
466
  const credential = yield* mintTalosconfig(props.target);
333
- const live = yield* liveDigest(props, credential.talosconfigPath);
334
- return {
335
- configDigest: wanted,
336
- converged: live === wanted,
467
+ const outcome = yield* Effect8.result(liveSpecDigest(props, credential.talosconfigPath));
468
+ if (Result.isFailure(outcome)) {
469
+ const reachable = yield* probeMaintenanceMode(props, credential.talosconfigPath);
470
+ return reachable ? undefined : yield* Effect8.fail(outcome.failure);
471
+ }
472
+ const attrs = {
473
+ configDigest: props.configDigest,
474
+ converged: outcome.success === props.configDigest ? "read-back" : false,
337
475
  mode: props.mode ?? "auto",
338
476
  node: props.node
339
477
  };
340
- });
341
- var diff4 = (news, output) => Effect6.gen(function* () {
478
+ return attrs.converged === false ? Unowned(attrs) : attrs;
479
+ }));
480
+ var diffMachineConfig = (news, output) => Effect8.gen(function* () {
342
481
  if (output === undefined || !isResolved4(news))
343
482
  return;
344
- const live = yield* read4(news);
345
- if (live.converged)
483
+ const live = yield* readMachineConfig(news);
484
+ if (live !== undefined && live.converged !== false)
346
485
  return { action: "noop" };
347
486
  return { action: "update" };
348
487
  });
349
- var reconcile4 = (props) => Effect6.gen(function* () {
350
- const path = resolveConfigPath(STACK_DIR, props.configFile);
351
- const wanted = yield* loadDigest(props);
488
+
489
+ // src/talos/machine-config-poll.ts
490
+ import * as Effect9 from "effect/Effect";
491
+ var isRebootish = (mode) => mode === "reboot" || mode === "auto";
492
+ var POLICY = {
493
+ immediate: { intervalMs: 2000, maxAttempts: 5 },
494
+ rebootish: { intervalMs: 5000, maxAttempts: 24 }
495
+ };
496
+ var pollUntilMatch = (check2, policy) => Effect9.gen(function* () {
497
+ for (let attempt = 1;attempt <= policy.maxAttempts; attempt++) {
498
+ const matched = yield* check2.pipe(Effect9.orElseSucceed(() => false));
499
+ if (matched)
500
+ return true;
501
+ if (attempt < policy.maxAttempts)
502
+ yield* Effect9.sleep(policy.intervalMs);
503
+ }
504
+ return false;
505
+ });
506
+ var confirmConverged = (node, mode, check2, policies = POLICY) => Effect9.gen(function* () {
507
+ const rebootish = isRebootish(mode);
508
+ const policy = rebootish ? policies.rebootish : policies.immediate;
509
+ const matched = yield* pollUntilMatch(check2, policy);
510
+ if (!matched) {
511
+ return yield* Effect9.fail(new TalosConvergenceTimeout({ attempts: policy.maxAttempts, mode, node }));
512
+ }
513
+ return rebootish ? "accepted" : "read-back";
514
+ });
515
+
516
+ // src/talos/talos-machine-config.ts
517
+ var TalosMachineConfig = Resource4("Talos.MachineConfig");
518
+ var reconcileMachineConfig = (props, output, pollPolicies) => Effect10.scoped(Effect10.gen(function* () {
519
+ const mode = props.mode ?? "auto";
520
+ const raw = yield* readKvValue(props.target.mount, props.configKey, ["config"]);
521
+ const gotDigest = configDigest(raw);
522
+ if (gotDigest !== props.configDigest) {
523
+ return yield* Effect10.fail(new TalosConfigDigestMismatch({
524
+ expected: props.configDigest,
525
+ got: gotDigest,
526
+ key: props.configKey,
527
+ mount: props.target.mount
528
+ }));
529
+ }
352
530
  const credential = yield* mintTalosconfig(props.target);
531
+ const { path } = yield* mintKvTempFile(raw, `${props.target.cluster}-${props.node}-config`);
532
+ const isCreate = output === undefined;
353
533
  const args = [
354
534
  "apply-config",
355
535
  "--file",
356
536
  path,
357
537
  "--mode",
358
- props.mode ?? "auto",
359
- ...props.insecure === true ? ["--insecure"] : []
538
+ mode,
539
+ ...isCreate ? ["--insecure"] : []
360
540
  ];
361
541
  yield* talosctl(args, { nodes: [props.node], talosconfigPath: credential.talosconfigPath });
362
- const live = yield* liveDigest(props, credential.talosconfigPath);
363
- if (live !== wanted) {
364
- return yield* Effect6.die(new Error(`${props.node}: apply-config returned no error but machineconfig digest still differs. ` + "Read back rather than trusting the exit code alone."));
365
- }
542
+ const converged = yield* confirmConverged(props.node, mode, liveSpecDigest(props, credential.talosconfigPath).pipe(Effect10.map((live) => live === props.configDigest)), pollPolicies);
366
543
  return {
367
- configDigest: wanted,
368
- converged: true,
369
- mode: props.mode ?? "auto",
544
+ configDigest: props.configDigest,
545
+ converged,
546
+ mode,
370
547
  node: props.node
371
548
  };
372
- });
549
+ }));
373
550
  var handlers4 = {
374
- delete: () => Effect6.void,
551
+ delete: () => Effect10.void,
375
552
  diff: ({
376
553
  news,
377
554
  output
378
- }) => diff4(news, output),
379
- list: () => Effect6.succeed([]),
380
- read: ({ olds }) => read4(olds),
381
- reconcile: ({ news }) => reconcile4(news)
555
+ }) => diffMachineConfig(news, output),
556
+ list: () => Effect10.succeed([]),
557
+ read: ({ olds }) => readMachineConfig(olds),
558
+ reconcile: ({
559
+ news,
560
+ output
561
+ }) => reconcileMachineConfig(news, output)
382
562
  };
383
- var TalosMachineConfigProvider = () => Provider4.effect(TalosMachineConfig, Effect6.succeed(TalosMachineConfig.Provider.of(handlers4)));
563
+ var TalosMachineConfigProvider = () => Provider4.effect(TalosMachineConfig, Effect10.succeed(TalosMachineConfig.Provider.of(handlers4)));
384
564
  export {
565
+ TalosBootstrap,
385
566
  TalosBootstrapProvider,
567
+ TalosClusterHealth,
386
568
  TalosClusterHealthProvider,
569
+ TalosKubeconfig,
387
570
  TalosKubeconfigProvider,
571
+ TalosMachineConfig,
388
572
  TalosMachineConfigProvider
389
573
  };
390
574
 
391
- //# debugId=6CD90391393B548364756E2164756E21
575
+ //# debugId=D7E7AB43E3608FE064756E2164756E21
392
576
  //# sourceMappingURL=index.js.map