@homeflare/alchemy 0.19.0 → 0.20.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 (39) hide show
  1. package/dist/linux/index.js +140 -93
  2. package/dist/linux/index.js.map +11 -10
  3. package/dist/linux/region.d.ts +11 -0
  4. package/dist/linux/region.d.ts.map +1 -1
  5. package/dist/linux/remote-file-lifecycle.d.ts.map +1 -1
  6. package/dist/linux/remote-file-plan.d.ts +22 -0
  7. package/dist/linux/remote-file-plan.d.ts.map +1 -1
  8. package/dist/linux/remote-file.d.ts +4 -1
  9. package/dist/linux/remote-file.d.ts.map +1 -1
  10. package/dist/linux/unit-handlers.d.ts.map +1 -1
  11. package/dist/linux/unit-lifecycle.d.ts +1 -5
  12. package/dist/linux/unit-lifecycle.d.ts.map +1 -1
  13. package/dist/linux/unit-preflight.d.ts +50 -0
  14. package/dist/linux/unit-preflight.d.ts.map +1 -0
  15. package/dist/proxmox/index.d.ts +15 -3
  16. package/dist/proxmox/index.d.ts.map +1 -1
  17. package/dist/proxmox/index.js +4 -1
  18. package/dist/proxmox/index.js.map +1 -1
  19. package/docs/adopted-deploys.md +13 -0
  20. package/docs/linux-host.md +11 -1
  21. package/docs/provider-standard.md +138 -0
  22. package/docs/upstream-conformance.md +164 -0
  23. package/package.json +1 -1
  24. package/src/linux/region.ts +15 -0
  25. package/src/linux/remote-file-lifecycle.test.ts +0 -88
  26. package/src/linux/remote-file-lifecycle.ts +25 -7
  27. package/src/linux/remote-file-plan.ts +37 -1
  28. package/src/linux/remote-file-region.test.ts +181 -0
  29. package/src/linux/remote-file.ts +4 -1
  30. package/src/linux/unit-handlers.ts +12 -5
  31. package/src/linux/unit-lifecycle.ts +12 -30
  32. package/src/linux/unit-preflight.test.ts +128 -0
  33. package/src/linux/unit-preflight.ts +116 -0
  34. package/src/linux/unit-rename.test.ts +76 -0
  35. package/src/proxmox/ceph-adopt.test.ts +210 -0
  36. package/src/proxmox/constraints-forms.test.ts +14 -6
  37. package/src/proxmox/constraints-node-forms.test.ts +12 -3
  38. package/src/proxmox/index.test.ts +7 -4
  39. package/src/proxmox/index.ts +15 -3
@@ -624,6 +624,7 @@ var tokens = (spec) => {
624
624
  end: `${comment} END ${spec.name}`
625
625
  };
626
626
  };
627
+ var sameRegion = (a, b) => a === undefined || b === undefined ? a === undefined && b === undefined : a.name === b.name && (a.comment ?? DEFAULT_COMMENT) === (b.comment ?? DEFAULT_COMMENT);
627
628
  var regionProblems = (spec) => {
628
629
  const found = [];
629
630
  const comment = spec.comment ?? DEFAULT_COMMENT;
@@ -748,6 +749,16 @@ var identity = async (runner, props) => {
748
749
  return { ...uid === undefined ? {} : { uid }, ...gid === undefined ? {} : { gid } };
749
750
  };
750
751
  var digestOf = (text) => sha256Hex(encoder.encode(text));
752
+ var relocation = (props, output) => {
753
+ if (props.path !== output.path)
754
+ return "path";
755
+ if (sameRegion(props.region, output.region))
756
+ return "none";
757
+ if (props.region === undefined || output.region === undefined) {
758
+ throw refuse3(props.path, `changes between owning the whole file and owning the managed region ` + `${JSON.stringify((props.region ?? output.region)?.name ?? "")} at the same path. Those ` + "are two different claims on one file: destroy this resource and declare a new one.");
759
+ }
760
+ return "region";
761
+ };
751
762
  var ownedDigest = (file, region) => {
752
763
  if (file === undefined)
753
764
  return;
@@ -814,7 +825,7 @@ var planWrite = async (runner, props, want) => {
814
825
  // src/linux/remote-file-lifecycle.ts
815
826
  var diffFile = async (runner, news, output) => {
816
827
  const want = await identity(runner, news);
817
- if (news.path !== output.path) {
828
+ if (relocation(news, output) === "path") {
818
829
  await runner.checkWrite?.(news.path, { mode: news.mode ?? DEFAULT_MODE2, ...want });
819
830
  return { action: "replace" };
820
831
  }
@@ -830,8 +841,8 @@ var reconcileFile = async (runner, props, output, adopt = false) => {
830
841
  if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
831
842
  throw refuse3(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy through ` + "a root destination or a privileged HostRunner. This provider never calls sudo itself.");
832
843
  }
833
- const moved = output !== undefined && output.path !== props.path;
834
- const prior = moved ? undefined : output;
844
+ const move = output === undefined ? "none" : relocation(props, output);
845
+ const prior = move === "none" ? output : undefined;
835
846
  const planned = await planWrite(runner, props, want);
836
847
  const live = ownedDigest(planned.before, props.region);
837
848
  const declared = digestOf(props.content);
@@ -849,9 +860,16 @@ var reconcileFile = async (runner, props, output, adopt = false) => {
849
860
  }
850
861
  throw refuse3(props.path, "the write returned but the file on disk does not match the declaration");
851
862
  }
852
- if (moved && output !== undefined)
853
- await deleteFile(runner, output);
854
- return after;
863
+ if (move === "none" || output === undefined)
864
+ return after;
865
+ await deleteFile(runner, output);
866
+ if (move === "path")
867
+ return after;
868
+ const settled = await readFileAttributes(runner, props.path, props.region);
869
+ if (settled === undefined || settled.contentSha256 !== declared) {
870
+ throw refuse3(props.path, "removing the previous managed block did not leave ours intact");
871
+ }
872
+ return settled;
855
873
  };
856
874
  var deleteFile = async (runner, output) => {
857
875
  const stat = await runner.stat(output.path);
@@ -914,6 +932,80 @@ import { Unowned as Unowned4 } from "alchemy/AdoptPolicy";
914
932
  import { isResolved as isResolved3 } from "alchemy/Diff";
915
933
  import * as Effect8 from "effect/Effect";
916
934
 
935
+ // src/linux/systemctl.ts
936
+ var SYSTEMCTL = "systemctl";
937
+ var SHOW_PROPERTIES = [
938
+ "Id",
939
+ "LoadState",
940
+ "ActiveState",
941
+ "SubState",
942
+ "UnitFileState",
943
+ "FragmentPath",
944
+ "NeedDaemonReload"
945
+ ];
946
+
947
+ class SystemctlError extends Error {
948
+ constructor(command, exitCode, stderr) {
949
+ super(`systemctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
950
+ this.name = "SystemctlError";
951
+ }
952
+ }
953
+ var parseShow = (stdout) => {
954
+ const fields = new Map;
955
+ for (const line of stdout.split(`
956
+ `)) {
957
+ const at = line.indexOf("=");
958
+ if (at <= 0)
959
+ continue;
960
+ const key = line.slice(0, at);
961
+ if (!fields.has(key))
962
+ fields.set(key, line.slice(at + 1));
963
+ }
964
+ return fields;
965
+ };
966
+ var statusOf = (stdout) => {
967
+ const fields = parseShow(stdout);
968
+ const loadState = fields.get("LoadState") ?? "not-found";
969
+ const unitFileState = fields.get("UnitFileState") ?? "";
970
+ const fragmentPath = fields.get("FragmentPath") ?? "";
971
+ const subState = fields.get("SubState") ?? "";
972
+ return {
973
+ activeState: fields.get("ActiveState") ?? "inactive",
974
+ known: loadState !== "not-found" || unitFileState !== "",
975
+ loadState,
976
+ needDaemonReload: fields.get("NeedDaemonReload") === "yes",
977
+ ...subState === "" ? {} : { subState },
978
+ ...unitFileState === "" ? {} : { unitFileState },
979
+ ...fragmentPath === "" ? {} : { fragmentPath }
980
+ };
981
+ };
982
+ var showUnit = async (runner, name) => {
983
+ const result = await runner.exec([
984
+ SYSTEMCTL,
985
+ "show",
986
+ "--no-pager",
987
+ "-p",
988
+ SHOW_PROPERTIES.join(","),
989
+ "--",
990
+ name
991
+ ]);
992
+ if (result.exitCode !== 0)
993
+ throw new SystemctlError(`show ${name}`, result.exitCode, result.stderr);
994
+ return statusOf(result.stdout);
995
+ };
996
+ var run = async (runner, args) => {
997
+ const result = await runner.exec([SYSTEMCTL, ...args]);
998
+ if (result.exitCode !== 0) {
999
+ throw new SystemctlError(args.join(" "), result.exitCode, result.stderr);
1000
+ }
1001
+ };
1002
+ var daemonReload = (runner) => run(runner, ["daemon-reload"]);
1003
+ var enableUnit = (runner, name) => run(runner, ["enable", "--", name]);
1004
+ var disableUnit = (runner, name) => run(runner, ["disable", "--", name]);
1005
+ var startUnit = (runner, name) => run(runner, ["start", "--", name]);
1006
+ var stopUnit = (runner, name) => run(runner, ["stop", "--", name]);
1007
+ var restartUnit = (runner, name) => run(runner, ["restart", "--", name]);
1008
+
917
1009
  // src/linux/unit-form.ts
918
1010
  var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
919
1011
  var UNIT_SEARCH_DIRECTORIES = [
@@ -994,80 +1086,6 @@ var textProblems = (text) => {
994
1086
  return found;
995
1087
  };
996
1088
 
997
- // src/linux/systemctl.ts
998
- var SYSTEMCTL = "systemctl";
999
- var SHOW_PROPERTIES = [
1000
- "Id",
1001
- "LoadState",
1002
- "ActiveState",
1003
- "SubState",
1004
- "UnitFileState",
1005
- "FragmentPath",
1006
- "NeedDaemonReload"
1007
- ];
1008
-
1009
- class SystemctlError extends Error {
1010
- constructor(command, exitCode, stderr) {
1011
- super(`systemctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
1012
- this.name = "SystemctlError";
1013
- }
1014
- }
1015
- var parseShow = (stdout) => {
1016
- const fields = new Map;
1017
- for (const line of stdout.split(`
1018
- `)) {
1019
- const at = line.indexOf("=");
1020
- if (at <= 0)
1021
- continue;
1022
- const key = line.slice(0, at);
1023
- if (!fields.has(key))
1024
- fields.set(key, line.slice(at + 1));
1025
- }
1026
- return fields;
1027
- };
1028
- var statusOf = (stdout) => {
1029
- const fields = parseShow(stdout);
1030
- const loadState = fields.get("LoadState") ?? "not-found";
1031
- const unitFileState = fields.get("UnitFileState") ?? "";
1032
- const fragmentPath = fields.get("FragmentPath") ?? "";
1033
- const subState = fields.get("SubState") ?? "";
1034
- return {
1035
- activeState: fields.get("ActiveState") ?? "inactive",
1036
- known: loadState !== "not-found" || unitFileState !== "",
1037
- loadState,
1038
- needDaemonReload: fields.get("NeedDaemonReload") === "yes",
1039
- ...subState === "" ? {} : { subState },
1040
- ...unitFileState === "" ? {} : { unitFileState },
1041
- ...fragmentPath === "" ? {} : { fragmentPath }
1042
- };
1043
- };
1044
- var showUnit = async (runner, name) => {
1045
- const result = await runner.exec([
1046
- SYSTEMCTL,
1047
- "show",
1048
- "--no-pager",
1049
- "-p",
1050
- SHOW_PROPERTIES.join(","),
1051
- "--",
1052
- name
1053
- ]);
1054
- if (result.exitCode !== 0)
1055
- throw new SystemctlError(`show ${name}`, result.exitCode, result.stderr);
1056
- return statusOf(result.stdout);
1057
- };
1058
- var run = async (runner, args) => {
1059
- const result = await runner.exec([SYSTEMCTL, ...args]);
1060
- if (result.exitCode !== 0) {
1061
- throw new SystemctlError(args.join(" "), result.exitCode, result.stderr);
1062
- }
1063
- };
1064
- var daemonReload = (runner) => run(runner, ["daemon-reload"]);
1065
- var enableUnit = (runner, name) => run(runner, ["enable", "--", name]);
1066
- var disableUnit = (runner, name) => run(runner, ["disable", "--", name]);
1067
- var startUnit = (runner, name) => run(runner, ["start", "--", name]);
1068
- var stopUnit = (runner, name) => run(runner, ["stop", "--", name]);
1069
- var restartUnit = (runner, name) => run(runner, ["restart", "--", name]);
1070
-
1071
1089
  // src/linux/unit-settle.ts
1072
1090
  var refuse4 = (name, message) => new Error(`Systemd.Unit ${name}: ${message}`);
1073
1091
  var attributesOf = (props, unitSha256, status) => ({
@@ -1123,21 +1141,13 @@ var settle = async (runner, props, step) => {
1123
1141
  return settled;
1124
1142
  };
1125
1143
 
1126
- // src/linux/unit-lifecycle.ts
1127
- var encoder2 = new TextEncoder;
1144
+ // src/linux/unit-preflight.ts
1128
1145
  var decoder2 = new TextDecoder;
1129
1146
  var assertValid = (props, expect) => {
1130
1147
  const found = unitProblems(props, expect);
1131
1148
  if (found.length > 0)
1132
1149
  throw refuse4(props.name, found.join("; "));
1133
1150
  };
1134
- var readUnit = async (runner, props) => {
1135
- const bytes = await runner.readFile(unitPathFor(props));
1136
- const status = await showUnit(runner, props.name);
1137
- if (bytes === undefined && !status.known)
1138
- return;
1139
- return attributesOf(props, bytes === undefined ? "" : digestOf2(decoder2.decode(bytes)), status);
1140
- };
1141
1151
  var assertMayWrite = (runner, name) => {
1142
1152
  if (canActAsRoot(runner))
1143
1153
  return;
@@ -1148,10 +1158,40 @@ var assertUsable = (props, status) => {
1148
1158
  throw refuse4(props.name, "is masked. If that is stale, `systemctl unmask` it deliberately, then redeploy. Nothing " + "was written.");
1149
1159
  }
1150
1160
  };
1161
+ var assertUnclaimed = async (runner, props) => {
1162
+ const path = unitPathFor(props);
1163
+ const bytes = await runner.readFile(path);
1164
+ const live = bytes === undefined ? undefined : digestOf2(decoder2.decode(bytes));
1165
+ if (live !== undefined && live !== digestOf2(unitText(props))) {
1166
+ throw refuse4(props.name, `${path} is already on this host and is not this resource. Remove it, or declare it as a ` + "new resource and deploy with --adopt.");
1167
+ }
1168
+ };
1169
+ var assertRenameTarget = async (runner, old, name) => {
1170
+ assertMayWrite(runner, old.name);
1171
+ assertMayWrite(runner, name);
1172
+ assertUsable({ name }, await showUnit(runner, name));
1173
+ };
1174
+ var assertReplaceable = async (runner, next, old, expect) => {
1175
+ assertValid(next, expect);
1176
+ await assertRenameTarget(runner, old, next.name);
1177
+ await assertUnclaimed(runner, next);
1178
+ await runner.checkWrite?.(unitPathFor(next), UNIT_WRITE);
1179
+ };
1180
+
1181
+ // src/linux/unit-lifecycle.ts
1182
+ var encoder2 = new TextEncoder;
1183
+ var decoder3 = new TextDecoder;
1184
+ var readUnit = async (runner, props) => {
1185
+ const bytes = await runner.readFile(unitPathFor(props));
1186
+ const status = await showUnit(runner, props.name);
1187
+ if (bytes === undefined && !status.known)
1188
+ return;
1189
+ return attributesOf(props, bytes === undefined ? "" : digestOf2(decoder3.decode(bytes)), status);
1190
+ };
1151
1191
  var diffUnit = async (runner, news, output, expect) => {
1152
1192
  assertValid(news, expect);
1153
1193
  if (news.name !== output.name || unitPathFor(news) !== output.unitPath) {
1154
- assertMayWrite(runner, news.name);
1194
+ await assertReplaceable(runner, news, output, expect);
1155
1195
  return { action: "replace", deleteFirst: true };
1156
1196
  }
1157
1197
  const desired2 = digestOf2(unitText(news));
@@ -1183,8 +1223,10 @@ var reconcileUnit = async (runner, props, output, adopt = false, expect) => {
1183
1223
  const text = unitText(props);
1184
1224
  const desired2 = digestOf2(text);
1185
1225
  const before = await runner.readFile(path);
1186
- const liveSha = before === undefined ? undefined : digestOf2(decoder2.decode(before));
1226
+ const liveSha = before === undefined ? undefined : digestOf2(decoder3.decode(before));
1187
1227
  const moved = output !== undefined && (output.name !== props.name || output.unitPath !== path);
1228
+ if (moved && output !== undefined)
1229
+ await assertReplaceable(runner, props, output, expect);
1188
1230
  const prior = moved ? undefined : output;
1189
1231
  if (prior === undefined && !(adopt && output === undefined) && liveSha !== undefined && liveSha !== desired2) {
1190
1232
  throw refuse4(props.name, `${path} is already on this host and is not this resource. Remove it, or declare it as a ` + "new resource and deploy with --adopt.");
@@ -1232,7 +1274,12 @@ var diffHandler = (runner, instanceId, news, output, expect) => {
1232
1274
  if (isResolved3(news))
1233
1275
  return lift(() => diffUnit(runner, news, output, expect));
1234
1276
  const name = resolvedString(news, "name");
1235
- return Effect8.succeed(name !== undefined && name !== output.name ? { action: "replace", deleteFirst: true } : undefined);
1277
+ if (name === undefined || name === output.name)
1278
+ return Effect8.succeed(undefined);
1279
+ return lift(async () => {
1280
+ await assertRenameTarget(runner, output, name);
1281
+ return { action: "replace", deleteFirst: true };
1282
+ });
1236
1283
  };
1237
1284
  var reconcileHandler = (runner, args, expect) => Effect8.flatMap(adoptsAtApply(args), (adopt) => lift(() => reconcileUnit(runner, args.news, args.output, adopt, expect)));
1238
1285
  var deleteHandler = (runner, output) => lift(() => deleteUnit(runner, output));
@@ -1550,5 +1597,5 @@ export {
1550
1597
  sshRunner
1551
1598
  };
1552
1599
 
1553
- //# debugId=36C2172DF713B75564756E2164756E21
1600
+ //# debugId=999A0F25FFF74BEF64756E2164756E21
1554
1601
  //# sourceMappingURL=index.js.map