@homeflare/alchemy 0.31.2 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/dist/grafana/dashboard-model.d.ts +77 -0
- package/dist/grafana/dashboard-model.d.ts.map +1 -0
- package/dist/grafana/dashboard.d.ts +98 -0
- package/dist/grafana/dashboard.d.ts.map +1 -0
- package/dist/grafana/folder.d.ts +100 -0
- package/dist/grafana/folder.d.ts.map +1 -0
- package/dist/grafana/index.d.ts +9 -8
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +158 -6
- package/dist/grafana/index.js.map +8 -4
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/grafana/provisioned.d.ts +11 -0
- package/dist/grafana/provisioned.d.ts.map +1 -0
- package/dist/linux/container-form.d.ts +129 -0
- package/dist/linux/container-form.d.ts.map +1 -0
- package/dist/linux/container-generator.d.ts +74 -0
- package/dist/linux/container-generator.d.ts.map +1 -0
- package/dist/linux/container-handlers.d.ts +15 -0
- package/dist/linux/container-handlers.d.ts.map +1 -0
- package/dist/linux/container-lifecycle.d.ts +37 -0
- package/dist/linux/container-lifecycle.d.ts.map +1 -0
- package/dist/linux/container-preflight.d.ts +34 -0
- package/dist/linux/container-preflight.d.ts.map +1 -0
- package/dist/linux/container-secrets.d.ts +37 -0
- package/dist/linux/container-secrets.d.ts.map +1 -0
- package/dist/linux/container-settle.d.ts +34 -0
- package/dist/linux/container-settle.d.ts.map +1 -0
- package/dist/linux/container.d.ts +30 -0
- package/dist/linux/container.d.ts.map +1 -0
- package/dist/linux/fake-quadlet.d.ts +25 -0
- package/dist/linux/fake-quadlet.d.ts.map +1 -0
- package/dist/linux/index.d.ts +6 -3
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +641 -268
- package/dist/linux/index.js.map +15 -7
- package/dist/linux/providers.d.ts +1 -1
- package/dist/linux/providers.d.ts.map +1 -1
- package/dist/linux/systemctl.d.ts +7 -1
- package/dist/linux/systemctl.d.ts.map +1 -1
- package/dist/opnsense/alias-form.d.ts +29 -0
- package/dist/opnsense/alias-form.d.ts.map +1 -0
- package/dist/opnsense/alias.d.ts +81 -0
- package/dist/opnsense/alias.d.ts.map +1 -0
- package/dist/opnsense/category-form.d.ts +10 -0
- package/dist/opnsense/category-form.d.ts.map +1 -0
- package/dist/opnsense/category.d.ts +58 -0
- package/dist/opnsense/category.d.ts.map +1 -0
- package/dist/opnsense/credentials.d.ts +36 -0
- package/dist/opnsense/credentials.d.ts.map +1 -0
- package/dist/opnsense/fake-opnsense.d.ts +51 -0
- package/dist/opnsense/fake-opnsense.d.ts.map +1 -0
- package/dist/opnsense/group-form.d.ts +11 -0
- package/dist/opnsense/group-form.d.ts.map +1 -0
- package/dist/opnsense/group.d.ts +62 -0
- package/dist/opnsense/group.d.ts.map +1 -0
- package/dist/opnsense/index.d.ts +16 -0
- package/dist/opnsense/index.d.ts.map +1 -0
- package/dist/opnsense/index.js +211 -0
- package/dist/opnsense/index.js.map +20 -0
- package/dist/opnsense/policy.d.ts +20 -0
- package/dist/opnsense/policy.d.ts.map +1 -0
- package/dist/opnsense/providers.d.ts +13 -0
- package/dist/opnsense/providers.d.ts.map +1 -0
- package/dist/opnsense/resource.d.ts +74 -0
- package/dist/opnsense/resource.d.ts.map +1 -0
- package/dist/opnsense/wire.d.ts +39 -0
- package/dist/opnsense/wire.d.ts.map +1 -0
- package/dist/proxmox/index.js +170 -80
- package/dist/proxmox/index.js.map +9 -7
- package/dist/proxmox/storage-form.d.ts +54 -0
- package/dist/proxmox/storage-form.d.ts.map +1 -0
- package/dist/proxmox/storage-wire.d.ts +56 -0
- package/dist/proxmox/storage-wire.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +55 -60
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-form.d.ts +124 -0
- package/dist/proxmox/zfs-pool-form.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-wire.d.ts +78 -0
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +27 -52
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/dist/unifi/fake-unifi.d.ts +48 -0
- package/dist/unifi/fake-unifi.d.ts.map +1 -0
- package/dist/unifi/firewall-zone-form.d.ts +40 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -0
- package/dist/unifi/firewall-zone.d.ts +50 -0
- package/dist/unifi/firewall-zone.d.ts.map +1 -0
- package/dist/unifi/index.d.ts +13 -0
- package/dist/unifi/index.d.ts.map +1 -0
- package/dist/unifi/index.js +190 -0
- package/dist/unifi/index.js.map +16 -0
- package/dist/unifi/network-form.d.ts +67 -0
- package/dist/unifi/network-form.d.ts.map +1 -0
- package/dist/unifi/network.d.ts +67 -0
- package/dist/unifi/network.d.ts.map +1 -0
- package/dist/unifi/policy.d.ts +20 -0
- package/dist/unifi/policy.d.ts.map +1 -0
- package/dist/unifi/providers.d.ts +13 -0
- package/dist/unifi/providers.d.ts.map +1 -0
- package/dist/unifi/resource.d.ts +64 -0
- package/dist/unifi/resource.d.ts.map +1 -0
- package/docs/grafana-folder-dashboard.md +145 -0
- package/docs/grafana.md +11 -10
- package/docs/opnsense.md +146 -0
- package/docs/provider-standard.md +1 -1
- package/docs/quadlet-container.md +132 -0
- package/docs/unifi.md +93 -0
- package/docs/upstream-conformance.md +9 -4
- package/package.json +12 -2
- package/src/grafana/dashboard-model.test.ts +111 -0
- package/src/grafana/dashboard-model.ts +111 -0
- package/src/grafana/dashboard.test.ts +232 -0
- package/src/grafana/dashboard.ts +161 -0
- package/src/grafana/folder.test.ts +212 -0
- package/src/grafana/folder.ts +161 -0
- package/src/grafana/index.ts +21 -8
- package/src/grafana/providers.ts +7 -1
- package/src/grafana/provisioned.ts +49 -0
- package/src/linux/container-fixture.test.ts +92 -0
- package/src/linux/container-form.ts +243 -0
- package/src/linux/container-generator.test.ts +195 -0
- package/src/linux/container-generator.ts +113 -0
- package/src/linux/container-handlers.ts +73 -0
- package/src/linux/container-lifecycle.test.ts +209 -0
- package/src/linux/container-lifecycle.ts +217 -0
- package/src/linux/container-preflight.ts +97 -0
- package/src/linux/container-secrets.test.ts +171 -0
- package/src/linux/container-secrets.ts +112 -0
- package/src/linux/container-settle.ts +97 -0
- package/src/linux/container.ts +60 -0
- package/src/linux/fake-quadlet.ts +127 -0
- package/src/linux/index.ts +11 -3
- package/src/linux/providers.ts +2 -0
- package/src/linux/systemctl.ts +9 -0
- package/src/opnsense/alias-form.test.ts +89 -0
- package/src/opnsense/alias-form.ts +53 -0
- package/src/opnsense/alias.test.ts +203 -0
- package/src/opnsense/alias.ts +117 -0
- package/src/opnsense/category-form.test.ts +54 -0
- package/src/opnsense/category-form.ts +20 -0
- package/src/opnsense/category.test.ts +138 -0
- package/src/opnsense/category.ts +82 -0
- package/src/opnsense/credentials.ts +41 -0
- package/src/opnsense/fake-opnsense.ts +79 -0
- package/src/opnsense/group-form.test.ts +75 -0
- package/src/opnsense/group-form.ts +25 -0
- package/src/opnsense/group.test.ts +141 -0
- package/src/opnsense/group.ts +78 -0
- package/src/opnsense/index.ts +45 -0
- package/src/opnsense/policy.ts +49 -0
- package/src/opnsense/providers.ts +21 -0
- package/src/opnsense/resource.ts +146 -0
- package/src/opnsense/wire.ts +56 -0
- package/src/opnsense/write-refusal.test.ts +148 -0
- package/src/proxmox/constraints-host-forms.test.ts +1 -1
- package/src/proxmox/storage-form.ts +144 -0
- package/src/proxmox/storage-read-failure.test.ts +214 -0
- package/src/proxmox/storage-wire.ts +124 -0
- package/src/proxmox/storage.ts +165 -175
- package/src/proxmox/zfs-pool-adopt.test.ts +26 -0
- package/src/proxmox/zfs-pool-form.ts +236 -0
- package/src/proxmox/zfs-pool-read-failure.test.ts +118 -0
- package/src/proxmox/zfs-pool-wire.ts +132 -0
- package/src/proxmox/zfs-pool.ts +106 -76
- package/src/unifi/fake-unifi.ts +60 -0
- package/src/unifi/firewall-zone-form.ts +65 -0
- package/src/unifi/firewall-zone.test.ts +88 -0
- package/src/unifi/firewall-zone.ts +66 -0
- package/src/unifi/index.ts +26 -0
- package/src/unifi/network-form.ts +207 -0
- package/src/unifi/network.test.ts +209 -0
- package/src/unifi/network.ts +83 -0
- package/src/unifi/policy.ts +37 -0
- package/src/unifi/providers.ts +15 -0
- package/src/unifi/resource.test.ts +133 -0
- package/src/unifi/resource.ts +131 -0
- package/dist/proxmox/zfs-pool-spec.d.ts +0 -27
- package/dist/proxmox/zfs-pool-spec.d.ts.map +0 -1
- package/dist/proxmox/zfs-pool-write.d.ts +0 -128
- package/dist/proxmox/zfs-pool-write.d.ts.map +0 -1
- package/src/proxmox/zfs-pool-spec.ts +0 -76
- package/src/proxmox/zfs-pool-write.ts +0 -196
package/dist/linux/index.js
CHANGED
|
@@ -419,11 +419,14 @@ var HostDirectoryProvider = () => Provider.effect(HostDirectory, Effect6.gen(fun
|
|
|
419
419
|
// src/linux/providers.ts
|
|
420
420
|
import * as Layer2 from "effect/Layer";
|
|
421
421
|
|
|
422
|
-
// src/linux/
|
|
422
|
+
// src/linux/container.ts
|
|
423
423
|
import { Resource as Resource2 } from "alchemy";
|
|
424
|
+
import * as Provider2 from "alchemy/Provider";
|
|
425
|
+
import * as Effect8 from "effect/Effect";
|
|
426
|
+
|
|
427
|
+
// src/linux/container-handlers.ts
|
|
424
428
|
import { Unowned as Unowned3 } from "alchemy/AdoptPolicy";
|
|
425
429
|
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
426
|
-
import * as Provider2 from "alchemy/Provider";
|
|
427
430
|
import * as Effect7 from "effect/Effect";
|
|
428
431
|
|
|
429
432
|
// src/launchd/job-form.ts
|
|
@@ -594,27 +597,556 @@ var jobPlistDict = (props) => {
|
|
|
594
597
|
if (calendar !== undefined) {
|
|
595
598
|
dict["StartCalendarInterval"] = Array.isArray(calendar) ? calendar.map(calendarDict) : calendarDict(calendar);
|
|
596
599
|
}
|
|
597
|
-
if (props.throttleInterval !== undefined)
|
|
598
|
-
dict["ThrottleInterval"] = props.throttleInterval;
|
|
599
|
-
if (props.standardOutPath !== undefined)
|
|
600
|
-
dict["StandardOutPath"] = props.standardOutPath;
|
|
601
|
-
if (props.standardErrorPath !== undefined)
|
|
602
|
-
dict["StandardErrorPath"] = props.standardErrorPath;
|
|
603
|
-
if (props.workingDirectory !== undefined)
|
|
604
|
-
dict["WorkingDirectory"] = props.workingDirectory;
|
|
605
|
-
if (props.userName !== undefined)
|
|
606
|
-
dict["UserName"] = props.userName;
|
|
607
|
-
if (props.groupName !== undefined)
|
|
608
|
-
dict["GroupName"] = props.groupName;
|
|
609
|
-
return dict;
|
|
600
|
+
if (props.throttleInterval !== undefined)
|
|
601
|
+
dict["ThrottleInterval"] = props.throttleInterval;
|
|
602
|
+
if (props.standardOutPath !== undefined)
|
|
603
|
+
dict["StandardOutPath"] = props.standardOutPath;
|
|
604
|
+
if (props.standardErrorPath !== undefined)
|
|
605
|
+
dict["StandardErrorPath"] = props.standardErrorPath;
|
|
606
|
+
if (props.workingDirectory !== undefined)
|
|
607
|
+
dict["WorkingDirectory"] = props.workingDirectory;
|
|
608
|
+
if (props.userName !== undefined)
|
|
609
|
+
dict["UserName"] = props.userName;
|
|
610
|
+
if (props.groupName !== undefined)
|
|
611
|
+
dict["GroupName"] = props.groupName;
|
|
612
|
+
return dict;
|
|
613
|
+
};
|
|
614
|
+
var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
615
|
+
var renderJob = (props) => {
|
|
616
|
+
const text = renderPlist(jobPlistDict(props));
|
|
617
|
+
const bytes = new TextEncoder().encode(text);
|
|
618
|
+
return { bytes, sha256: sha256Hex(bytes), text };
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/linux/unit-form.ts
|
|
622
|
+
var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
623
|
+
var UNIT_SEARCH_DIRECTORIES = [
|
|
624
|
+
"/etc/systemd/system",
|
|
625
|
+
"/run/systemd/system",
|
|
626
|
+
"/usr/local/lib/systemd/system",
|
|
627
|
+
"/usr/lib/systemd/system",
|
|
628
|
+
"/lib/systemd/system"
|
|
629
|
+
];
|
|
630
|
+
var UNIT_WRITE = { gid: 0, mode: 420, uid: 0 };
|
|
631
|
+
var digestOf = (text) => sha256Hex(new TextEncoder().encode(text));
|
|
632
|
+
var configDigest = (restartOn) => digestOf((restartOn ?? []).join(`
|
|
633
|
+
`));
|
|
634
|
+
var isUnitRunning = (activeState, subState, started) => {
|
|
635
|
+
if (activeState === "active")
|
|
636
|
+
return true;
|
|
637
|
+
if (activeState !== "activating")
|
|
638
|
+
return false;
|
|
639
|
+
if (subState === "auto-restart" || subState === "auto-restart-queued")
|
|
640
|
+
return true;
|
|
641
|
+
return started;
|
|
642
|
+
};
|
|
643
|
+
var renderUnit = (sections) => `${sections.map((section) => [`[${section.name}]`, ...section.lines.map(([key, value]) => `${key}=${value}`)].join(`
|
|
644
|
+
`)).join(`
|
|
645
|
+
|
|
646
|
+
`)}
|
|
647
|
+
`;
|
|
648
|
+
var unitText = (props) => props.content ?? renderUnit(props.sections ?? []);
|
|
649
|
+
var unitPathFor = (props) => `${props.directory ?? DEFAULT_UNIT_DIRECTORY}/${props.name}`;
|
|
650
|
+
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.@:-]*\.([a-z]+)$/;
|
|
651
|
+
var TYPES = new Set([
|
|
652
|
+
"automount",
|
|
653
|
+
"device",
|
|
654
|
+
"mount",
|
|
655
|
+
"path",
|
|
656
|
+
"scope",
|
|
657
|
+
"service",
|
|
658
|
+
"slice",
|
|
659
|
+
"socket",
|
|
660
|
+
"swap",
|
|
661
|
+
"target",
|
|
662
|
+
"timer"
|
|
663
|
+
]);
|
|
664
|
+
var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
|
|
665
|
+
var unitProblems = (props, expect) => {
|
|
666
|
+
const found = [];
|
|
667
|
+
const type = NAME.exec(props.name)?.[1];
|
|
668
|
+
if (type === undefined || !TYPES.has(type)) {
|
|
669
|
+
found.push(`name must be <unit>.<type> with a systemd unit type, got ${JSON.stringify(props.name)}`);
|
|
670
|
+
} else if (expect !== undefined && type !== expect) {
|
|
671
|
+
found.push(`this resource declares a .${expect} unit; ${props.name} is a .${type}`);
|
|
672
|
+
}
|
|
673
|
+
if (props.content === undefined === (props.sections === undefined)) {
|
|
674
|
+
found.push("declare exactly one of content or sections");
|
|
675
|
+
}
|
|
676
|
+
const directory = props.directory ?? DEFAULT_UNIT_DIRECTORY;
|
|
677
|
+
if (!directory.startsWith("/") || directory.endsWith("/") || directory.includes("/..")) {
|
|
678
|
+
found.push("directory must be an absolute, normalised path with no trailing slash");
|
|
679
|
+
}
|
|
680
|
+
found.push(...textProblems(unitText(props)));
|
|
681
|
+
if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory)) {
|
|
682
|
+
found.push(`an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(", ")}); ` + `${directory} is not one of them. Move it, or declare enabled: false.`);
|
|
683
|
+
}
|
|
684
|
+
if (props.enabled !== false && !hasInstall(props)) {
|
|
685
|
+
found.push("enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one " + 'systemd calls the unit "static" and `systemctl enable` has nothing to link. Add one, or ' + "declare enabled: false and let something else pull it in.");
|
|
686
|
+
}
|
|
687
|
+
if (type === "timer" && !unitText(props).includes("[Timer]")) {
|
|
688
|
+
found.push("a .timer needs a [Timer] section");
|
|
689
|
+
}
|
|
690
|
+
return found;
|
|
691
|
+
};
|
|
692
|
+
var hasInstall = (props) => {
|
|
693
|
+
const text = unitText(props);
|
|
694
|
+
if (!text.includes("[Install]"))
|
|
695
|
+
return false;
|
|
696
|
+
const after = text.slice(text.indexOf("[Install]"));
|
|
697
|
+
return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
|
|
698
|
+
};
|
|
699
|
+
var textProblems = (text) => {
|
|
700
|
+
const found = [];
|
|
701
|
+
if (text.trim() === "")
|
|
702
|
+
found.push("the unit file is empty");
|
|
703
|
+
if (text.includes("\x00"))
|
|
704
|
+
found.push("the unit file contains NUL");
|
|
705
|
+
if (!/^\s*\[[A-Za-z]+]/.test(text))
|
|
706
|
+
found.push("a unit file must begin with a [Section]");
|
|
707
|
+
return found;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/linux/systemctl.ts
|
|
711
|
+
var SYSTEMCTL = "systemctl";
|
|
712
|
+
var SHOW_PROPERTIES = [
|
|
713
|
+
"Id",
|
|
714
|
+
"LoadState",
|
|
715
|
+
"ActiveState",
|
|
716
|
+
"SubState",
|
|
717
|
+
"UnitFileState",
|
|
718
|
+
"FragmentPath",
|
|
719
|
+
"SourcePath",
|
|
720
|
+
"NeedDaemonReload"
|
|
721
|
+
];
|
|
722
|
+
|
|
723
|
+
class SystemctlError extends Error {
|
|
724
|
+
constructor(command, exitCode, stderr) {
|
|
725
|
+
super(`systemctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
|
|
726
|
+
this.name = "SystemctlError";
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
var parseShow = (stdout) => {
|
|
730
|
+
const fields = new Map;
|
|
731
|
+
for (const line of stdout.split(`
|
|
732
|
+
`)) {
|
|
733
|
+
const at = line.indexOf("=");
|
|
734
|
+
if (at <= 0)
|
|
735
|
+
continue;
|
|
736
|
+
const key = line.slice(0, at);
|
|
737
|
+
if (!fields.has(key))
|
|
738
|
+
fields.set(key, line.slice(at + 1));
|
|
739
|
+
}
|
|
740
|
+
return fields;
|
|
741
|
+
};
|
|
742
|
+
var statusOf = (stdout) => {
|
|
743
|
+
const fields = parseShow(stdout);
|
|
744
|
+
const loadState = fields.get("LoadState") ?? "not-found";
|
|
745
|
+
const unitFileState = fields.get("UnitFileState") ?? "";
|
|
746
|
+
const fragmentPath = fields.get("FragmentPath") ?? "";
|
|
747
|
+
const sourcePath = fields.get("SourcePath") ?? "";
|
|
748
|
+
const subState = fields.get("SubState") ?? "";
|
|
749
|
+
return {
|
|
750
|
+
activeState: fields.get("ActiveState") ?? "inactive",
|
|
751
|
+
known: loadState !== "not-found" || unitFileState !== "",
|
|
752
|
+
loadState,
|
|
753
|
+
needDaemonReload: fields.get("NeedDaemonReload") === "yes",
|
|
754
|
+
...subState === "" ? {} : { subState },
|
|
755
|
+
...unitFileState === "" ? {} : { unitFileState },
|
|
756
|
+
...fragmentPath === "" ? {} : { fragmentPath },
|
|
757
|
+
...sourcePath === "" ? {} : { sourcePath }
|
|
758
|
+
};
|
|
759
|
+
};
|
|
760
|
+
var showUnit = async (runner, name) => {
|
|
761
|
+
const result = await runner.exec([
|
|
762
|
+
SYSTEMCTL,
|
|
763
|
+
"show",
|
|
764
|
+
"--no-pager",
|
|
765
|
+
"-p",
|
|
766
|
+
SHOW_PROPERTIES.join(","),
|
|
767
|
+
"--",
|
|
768
|
+
name
|
|
769
|
+
]);
|
|
770
|
+
if (result.exitCode !== 0)
|
|
771
|
+
throw new SystemctlError(`show ${name}`, result.exitCode, result.stderr);
|
|
772
|
+
return statusOf(result.stdout);
|
|
773
|
+
};
|
|
774
|
+
var run = async (runner, args) => {
|
|
775
|
+
const result = await runner.exec([SYSTEMCTL, ...args]);
|
|
776
|
+
if (result.exitCode !== 0) {
|
|
777
|
+
throw new SystemctlError(args.join(" "), result.exitCode, result.stderr);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var daemonReload = (runner) => run(runner, ["daemon-reload"]);
|
|
781
|
+
var enableUnit = (runner, name) => run(runner, ["enable", "--", name]);
|
|
782
|
+
var disableUnit = (runner, name) => run(runner, ["disable", "--", name]);
|
|
783
|
+
var startUnit = (runner, name) => run(runner, ["start", "--", name]);
|
|
784
|
+
var stopUnit = (runner, name) => run(runner, ["stop", "--", name]);
|
|
785
|
+
var restartUnit = (runner, name) => run(runner, ["restart", "--", name]);
|
|
786
|
+
|
|
787
|
+
// src/linux/container-generator.ts
|
|
788
|
+
class QuadletGeneratorError extends Error {
|
|
789
|
+
constructor(name, detail) {
|
|
790
|
+
super(`Podman.Container ${name}: ${detail}`);
|
|
791
|
+
this.name = "QuadletGeneratorError";
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
var generatedMatches = (containerPath, status) => status.known && status.loadState === "loaded" && (status.sourcePath === undefined || status.sourcePath === containerPath);
|
|
795
|
+
var needsVerificationReload = (containerPath, status) => !generatedMatches(containerPath, status);
|
|
796
|
+
var verifyGenerated = (name, containerPath, serviceName, status) => {
|
|
797
|
+
if (!status.known || status.loadState !== "loaded") {
|
|
798
|
+
throw new QuadletGeneratorError(name, `${serviceName} does not exist after daemon-reload (LoadState=${status.loadState}). The ` + ".container file was written and reloaded, so Quadlet’s generator refused it — a bad " + "key, a bad value, or an interaction this validation does not check. Debug on the host " + "with '/usr/lib/systemd/system-generators/podman-system-generator --dryrun' or " + `'systemd-analyze --generators=true verify ${serviceName}' (podman-systemd.unit(5), Podman 5.4).`);
|
|
799
|
+
}
|
|
800
|
+
if (status.sourcePath !== undefined && status.sourcePath !== containerPath) {
|
|
801
|
+
throw new QuadletGeneratorError(name, `${serviceName}'s SourcePath is ${status.sourcePath}, not ${containerPath} — a same-named ` + ".container file earlier in Quadlet’s search path is generating this unit instead of ours " + "(podman-systemd.unit(5) “Podman Unit Search Path”, Podman 5.4).");
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/linux/container-secrets.ts
|
|
806
|
+
var SECRET_KEY_NAME = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|CREDENTIAL|ACCESS_KEY|CLIENT_SECRET|AUTH_?KEY|DSN)/i;
|
|
807
|
+
var SECRET_VALUE_PREFIX = /^(Bearer\s|Basic\s|sk-|ghp_|gho_|glpat-|xox[baprs]-|AKIA|eyJ)/;
|
|
808
|
+
var SECRET_VALUE_ANYWHERE = new RegExp(SECRET_VALUE_PREFIX.source.replace(/^\^/, ""));
|
|
809
|
+
var EMBEDDED_SECRET_ASSIGNMENT = new RegExp(`[A-Za-z0-9_]*(?:${SECRET_KEY_NAME.source})[A-Za-z0-9_]*\\s*=\\s*\\S+`, "i");
|
|
810
|
+
var ENV_FLAG = /^(?:-e|--env)(?:=|$|[A-Za-z_])/;
|
|
811
|
+
var secretLikeEnvironment = (environment) => {
|
|
812
|
+
const found = [];
|
|
813
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
814
|
+
if (SECRET_KEY_NAME.test(key)) {
|
|
815
|
+
found.push(`environment key ${JSON.stringify(key)} looks like a secret by its name. Environment= is ` + "rendered into a world-readable file and stored in Alchemy state; use environmentFile " + "instead, pointing at a host path a secret renderer maintains.");
|
|
816
|
+
} else if (SECRET_VALUE_PREFIX.test(value)) {
|
|
817
|
+
found.push(`environment key ${JSON.stringify(key)}'s value looks like a credential. Environment= is ` + "rendered into a world-readable file and stored in Alchemy state; use environmentFile " + "instead, pointing at a host path a secret renderer maintains.");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return found;
|
|
821
|
+
};
|
|
822
|
+
var secretLikeLines = (lines) => {
|
|
823
|
+
const found = [];
|
|
824
|
+
for (const [key, value] of lines) {
|
|
825
|
+
if (SECRET_VALUE_ANYWHERE.test(value)) {
|
|
826
|
+
found.push(`${key}=${JSON.stringify(value)} looks like it carries a credential by its value. This ` + "line is rendered into a world-readable file and stored in Alchemy state; use " + "container.environmentFile instead, pointing at a host path a secret renderer maintains.");
|
|
827
|
+
} else if (EMBEDDED_SECRET_ASSIGNMENT.test(value)) {
|
|
828
|
+
found.push(`${key}=${JSON.stringify(value)} embeds a KEY=VALUE pair whose key looks like a secret ` + "name. This line is rendered into a world-readable file and stored in Alchemy state; use " + "container.environment (checked for secrets) or container.environmentFile instead.");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return found;
|
|
832
|
+
};
|
|
833
|
+
var podmanArgsProblems = (args) => args.filter((arg) => ENV_FLAG.test(arg)).map((arg) => `podmanArgs entry ${JSON.stringify(arg)} sets a container environment variable outside ` + "the typed props, which this family never checks for a secret. Use container.environment " + "(checked) or container.environmentFile (for anything secret) instead.");
|
|
834
|
+
|
|
835
|
+
// src/linux/container-form.ts
|
|
836
|
+
var QUADLET_SEARCH_DIRECTORIES = [
|
|
837
|
+
"/run/containers/systemd",
|
|
838
|
+
"/etc/containers/systemd",
|
|
839
|
+
"/usr/share/containers/systemd"
|
|
840
|
+
];
|
|
841
|
+
var QUADLET_DEFAULT_DIRECTORY = "/etc/containers/systemd";
|
|
842
|
+
var NAME2 = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
843
|
+
var containerPathFor = (props) => `${props.directory ?? QUADLET_DEFAULT_DIRECTORY}/${props.name}.container`;
|
|
844
|
+
var serviceNameFor = (props) => `${props.name}.service`;
|
|
845
|
+
var unitLines = (props) => [
|
|
846
|
+
...props.unit?.description === undefined ? [] : [["Description", props.unit.description]],
|
|
847
|
+
...props.unit?.documentation === undefined ? [] : [["Documentation", props.unit.documentation]],
|
|
848
|
+
...props.unit?.lines ?? []
|
|
849
|
+
];
|
|
850
|
+
var containerLines = (c) => [
|
|
851
|
+
["Image", c.image],
|
|
852
|
+
...c.containerName === undefined ? [] : [["ContainerName", c.containerName]],
|
|
853
|
+
...c.network === undefined ? [] : [["Network", c.network]],
|
|
854
|
+
...c.user === undefined ? [] : [["User", c.user]],
|
|
855
|
+
...c.exec === undefined ? [] : [["Exec", c.exec]],
|
|
856
|
+
...c.autoUpdate === undefined ? [] : [["AutoUpdate", c.autoUpdate]],
|
|
857
|
+
...(c.environmentFile ?? []).map((file) => ["EnvironmentFile", file]),
|
|
858
|
+
...Object.keys(c.environment ?? {}).sort().map((key) => [
|
|
859
|
+
"Environment",
|
|
860
|
+
`${key}=${c.environment[key]}`
|
|
861
|
+
]),
|
|
862
|
+
...(c.volume ?? []).map((volume) => ["Volume", volume]),
|
|
863
|
+
...(c.publishPort ?? []).map((port) => ["PublishPort", port]),
|
|
864
|
+
...(c.podmanArgs ?? []).map((arg) => ["PodmanArgs", arg])
|
|
865
|
+
];
|
|
866
|
+
var serviceLines = (props) => [
|
|
867
|
+
...props.service?.restart === undefined ? [] : [["Restart", props.service.restart]],
|
|
868
|
+
...props.service?.restartSec === undefined ? [] : [["RestartSec", props.service.restartSec]],
|
|
869
|
+
...props.service?.lines ?? []
|
|
870
|
+
];
|
|
871
|
+
var installLines = (props) => [
|
|
872
|
+
...(props.install?.wantedBy ?? []).map((target) => ["WantedBy", target]),
|
|
873
|
+
...(props.install?.requiredBy ?? []).map((target) => ["RequiredBy", target]),
|
|
874
|
+
...(props.install?.alias ?? []).map((name) => ["Alias", name])
|
|
875
|
+
];
|
|
876
|
+
var renderContainerFile = (props) => {
|
|
877
|
+
const sections = [
|
|
878
|
+
{ name: "Unit", lines: unitLines(props) },
|
|
879
|
+
{ name: "Container", lines: containerLines(props.container) },
|
|
880
|
+
{ name: "Service", lines: serviceLines(props) },
|
|
881
|
+
{ name: "Install", lines: installLines(props) }
|
|
882
|
+
];
|
|
883
|
+
return renderUnit(sections.filter((section) => section.lines.length > 0));
|
|
884
|
+
};
|
|
885
|
+
var refuseContainer = (name, message) => new Error(`Podman.Container ${name}: ${message}`);
|
|
886
|
+
var containerProblems = (props) => {
|
|
887
|
+
const found = [];
|
|
888
|
+
if (!NAME2.test(props.name)) {
|
|
889
|
+
found.push(`name must match ${String(NAME2)}, with no '.container' suffix; got ${JSON.stringify(props.name)}`);
|
|
890
|
+
}
|
|
891
|
+
const directory = props.directory ?? QUADLET_DEFAULT_DIRECTORY;
|
|
892
|
+
if (!directory.startsWith("/") || directory.endsWith("/") || directory.includes("/..")) {
|
|
893
|
+
found.push("directory must be an absolute, normalised path with no trailing slash");
|
|
894
|
+
} else if (!QUADLET_SEARCH_DIRECTORIES.includes(directory)) {
|
|
895
|
+
found.push(`directory must be one of Quadlet's own rootful search path (${QUADLET_SEARCH_DIRECTORIES.join(", ")}); ` + `${directory} is never read by the generator (podman-systemd.unit(5), Podman 5.4).`);
|
|
896
|
+
}
|
|
897
|
+
if (props.container.image.trim() === "")
|
|
898
|
+
found.push("container.image is required");
|
|
899
|
+
if (props.container.network === "host" && (props.container.publishPort?.length ?? 0) > 0) {
|
|
900
|
+
found.push("container.publishPort has no effect with container.network 'host': the container already " + "binds the host’s ports directly. Drop publishPort, or use a different network.");
|
|
901
|
+
}
|
|
902
|
+
found.push(...secretLikeEnvironment(props.container.environment ?? {}).map((problem) => `container.${problem}`));
|
|
903
|
+
found.push(...podmanArgsProblems(props.container.podmanArgs ?? []).map((problem) => `container.${problem}`));
|
|
904
|
+
found.push(...secretLikeLines([
|
|
905
|
+
...unitLines(props),
|
|
906
|
+
...containerLines(props.container).filter(([key]) => key !== "Environment"),
|
|
907
|
+
...serviceLines(props),
|
|
908
|
+
...installLines(props)
|
|
909
|
+
]));
|
|
910
|
+
if (renderContainerFile(props).includes("\x00"))
|
|
911
|
+
found.push("the rendered .container file contains NUL");
|
|
912
|
+
return found;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/linux/container-preflight.ts
|
|
916
|
+
var decoder = new TextDecoder;
|
|
917
|
+
var assertValid = (props) => {
|
|
918
|
+
const found = containerProblems(props);
|
|
919
|
+
if (found.length > 0)
|
|
920
|
+
throw refuseContainer(props.name, found.join("; "));
|
|
921
|
+
};
|
|
922
|
+
var assertMayWrite = (runner, name) => {
|
|
923
|
+
if (canActAsRoot(runner))
|
|
924
|
+
return;
|
|
925
|
+
throw refuseContainer(name, "a Quadlet .container file is root-owned and daemon-reload/systemctl need root. Point the " + "runner at a destination whose ssh user is root, or provide a privileged HostRunner. This " + "provider never calls sudo.");
|
|
926
|
+
};
|
|
927
|
+
var assertUsable = (props, status) => {
|
|
928
|
+
if (status.loadState === "masked" || status.unitFileState === "masked") {
|
|
929
|
+
throw refuseContainer(props.name, `${serviceNameFor(props)} is masked. If that is stale, \`systemctl unmask\` it deliberately, ` + "then redeploy. Nothing was written.");
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
var assertUnclaimed = async (runner, props) => {
|
|
933
|
+
const path = containerPathFor(props);
|
|
934
|
+
const bytes = await runner.readFile(path);
|
|
935
|
+
const live = bytes === undefined ? undefined : digestOf(decoder.decode(bytes));
|
|
936
|
+
if (live !== undefined && live !== digestOf(renderContainerFile(props))) {
|
|
937
|
+
throw refuseContainer(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.");
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
var assertRenameTarget = async (runner, old, name) => {
|
|
941
|
+
assertMayWrite(runner, old.name);
|
|
942
|
+
assertMayWrite(runner, name);
|
|
943
|
+
assertUsable({ name }, await showUnit(runner, serviceNameFor({ name })));
|
|
944
|
+
};
|
|
945
|
+
var assertReplaceable = async (runner, next, old) => {
|
|
946
|
+
assertValid(next);
|
|
947
|
+
await assertRenameTarget(runner, old, next.name);
|
|
948
|
+
await assertUnclaimed(runner, next);
|
|
949
|
+
await runner.checkWrite?.(containerPathFor(next), UNIT_WRITE);
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// src/linux/container-settle.ts
|
|
953
|
+
var attributesOf = (props, configSha256, containerSha256, containerPath, serviceName, status) => ({
|
|
954
|
+
active: status.activeState === "active" || status.activeState === "activating",
|
|
955
|
+
activeState: status.activeState,
|
|
956
|
+
configSha256,
|
|
957
|
+
containerPath,
|
|
958
|
+
containerSha256,
|
|
959
|
+
loadState: status.loadState,
|
|
960
|
+
name: props.name,
|
|
961
|
+
serviceName,
|
|
962
|
+
...status.unitFileState === undefined ? {} : { unitFileState: status.unitFileState },
|
|
963
|
+
...status.sourcePath === undefined ? {} : { sourcePath: status.sourcePath }
|
|
964
|
+
});
|
|
965
|
+
var settle = async (runner, props, configSha256, step) => {
|
|
966
|
+
const wantStarted = props.started !== false;
|
|
967
|
+
const running = isUnitRunning(step.status.activeState, step.status.subState, wantStarted);
|
|
968
|
+
const undo = async (detail) => {
|
|
969
|
+
if (step.before === undefined) {
|
|
970
|
+
await runner.removeFile(step.path).catch(() => {
|
|
971
|
+
return;
|
|
972
|
+
});
|
|
973
|
+
await daemonReload(runner).catch(() => {
|
|
974
|
+
return;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
throw refuseContainer(props.name, `${detail} The container's unit is NOT running.`);
|
|
978
|
+
};
|
|
979
|
+
try {
|
|
980
|
+
if (!wantStarted) {
|
|
981
|
+
if (running)
|
|
982
|
+
await stopUnit(runner, step.serviceName);
|
|
983
|
+
} else if (!running)
|
|
984
|
+
await startUnit(runner, step.serviceName);
|
|
985
|
+
else if (step.changed || step.configChanged)
|
|
986
|
+
await restartUnit(runner, step.serviceName);
|
|
987
|
+
} catch (cause) {
|
|
988
|
+
return undo(cause instanceof Error ? cause.message : String(cause));
|
|
989
|
+
}
|
|
990
|
+
const after = await showUnit(runner, step.serviceName);
|
|
991
|
+
if (wantStarted && !isUnitRunning(after.activeState, after.subState, wantStarted)) {
|
|
992
|
+
return undo(`systemctl reported success but the unit is ${after.activeState}.`);
|
|
993
|
+
}
|
|
994
|
+
return attributesOf(props, configSha256, step.desired, step.path, step.serviceName, after);
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// src/linux/container-lifecycle.ts
|
|
998
|
+
var encoder = new TextEncoder;
|
|
999
|
+
var decoder2 = new TextDecoder;
|
|
1000
|
+
var readContainer = async (runner, props) => {
|
|
1001
|
+
const path = containerPathFor(props);
|
|
1002
|
+
const bytes = await runner.readFile(path);
|
|
1003
|
+
const status = await showUnit(runner, serviceNameFor(props));
|
|
1004
|
+
if (bytes === undefined && !status.known)
|
|
1005
|
+
return;
|
|
1006
|
+
const sha = bytes === undefined ? "" : digestOf(decoder2.decode(bytes));
|
|
1007
|
+
return attributesOf(props, configDigest(props.restartOn), sha, path, serviceNameFor(props), status);
|
|
1008
|
+
};
|
|
1009
|
+
var diffContainer = async (runner, news, output) => {
|
|
1010
|
+
assertValid(news);
|
|
1011
|
+
if (news.name !== output.name || containerPathFor(news) !== output.containerPath) {
|
|
1012
|
+
await assertReplaceable(runner, news, output);
|
|
1013
|
+
return { action: "replace", deleteFirst: true };
|
|
1014
|
+
}
|
|
1015
|
+
const desired2 = digestOf(renderContainerFile(news));
|
|
1016
|
+
const update = async () => {
|
|
1017
|
+
assertMayWrite(runner, news.name);
|
|
1018
|
+
await runner.checkWrite?.(containerPathFor(news), UNIT_WRITE);
|
|
1019
|
+
return { action: "update" };
|
|
1020
|
+
};
|
|
1021
|
+
if (desired2 !== output.containerSha256)
|
|
1022
|
+
return update();
|
|
1023
|
+
if (configDigest(news.restartOn) !== output.configSha256)
|
|
1024
|
+
return update();
|
|
1025
|
+
const live = await readContainer(runner, news);
|
|
1026
|
+
if (live === undefined || live.containerSha256 !== desired2)
|
|
1027
|
+
return update();
|
|
1028
|
+
const status = await showUnit(runner, serviceNameFor(news));
|
|
1029
|
+
const wantStarted = news.started !== false;
|
|
1030
|
+
if (isUnitRunning(status.activeState, status.subState, wantStarted) !== wantStarted)
|
|
1031
|
+
return update();
|
|
1032
|
+
return status.needDaemonReload ? update() : { action: "noop" };
|
|
1033
|
+
};
|
|
1034
|
+
var reconcileContainer = async (runner, props, output, adopt = false) => {
|
|
1035
|
+
assertValid(props);
|
|
1036
|
+
assertMayWrite(runner, props.name);
|
|
1037
|
+
const path = containerPathFor(props);
|
|
1038
|
+
const serviceName = serviceNameFor(props);
|
|
1039
|
+
const text = renderContainerFile(props);
|
|
1040
|
+
const desired2 = digestOf(text);
|
|
1041
|
+
const before = await runner.readFile(path);
|
|
1042
|
+
const liveSha = before === undefined ? undefined : digestOf(decoder2.decode(before));
|
|
1043
|
+
const moved = output !== undefined && (output.name !== props.name || output.containerPath !== path);
|
|
1044
|
+
if (moved && output !== undefined)
|
|
1045
|
+
await assertReplaceable(runner, props, output);
|
|
1046
|
+
const prior = moved ? undefined : output;
|
|
1047
|
+
if (prior === undefined && !(adopt && output === undefined) && liveSha !== undefined && liveSha !== desired2) {
|
|
1048
|
+
throw refuseContainer(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.");
|
|
1049
|
+
}
|
|
1050
|
+
if (moved && output !== undefined)
|
|
1051
|
+
await deleteContainer(runner, output);
|
|
1052
|
+
const wrote = liveSha !== desired2;
|
|
1053
|
+
const stale = prior !== undefined && prior.containerSha256 !== desired2;
|
|
1054
|
+
const preStatus = await showUnit(runner, serviceName);
|
|
1055
|
+
assertUsable(props, preStatus);
|
|
1056
|
+
const needsReload = wrote || stale || preStatus.needDaemonReload || prior === undefined && needsVerificationReload(path, preStatus);
|
|
1057
|
+
if (wrote)
|
|
1058
|
+
await runner.writeFileAtomic(path, encoder.encode(text), UNIT_WRITE);
|
|
1059
|
+
if (needsReload)
|
|
1060
|
+
await daemonReload(runner);
|
|
1061
|
+
const status = needsReload ? await showUnit(runner, serviceName) : preStatus;
|
|
1062
|
+
try {
|
|
1063
|
+
verifyGenerated(props.name, path, serviceName, status);
|
|
1064
|
+
} catch (cause) {
|
|
1065
|
+
if (wrote) {
|
|
1066
|
+
if (before === undefined)
|
|
1067
|
+
await runner.removeFile(path).catch(() => {
|
|
1068
|
+
return;
|
|
1069
|
+
});
|
|
1070
|
+
else
|
|
1071
|
+
await runner.writeFileAtomic(path, before, UNIT_WRITE).catch(() => {
|
|
1072
|
+
return;
|
|
1073
|
+
});
|
|
1074
|
+
await daemonReload(runner).catch(() => {
|
|
1075
|
+
return;
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
throw cause;
|
|
1079
|
+
}
|
|
1080
|
+
const configChanged = prior !== undefined && prior.configSha256 !== configDigest(props.restartOn);
|
|
1081
|
+
const changed = wrote || stale || preStatus.needDaemonReload;
|
|
1082
|
+
return settle(runner, props, configDigest(props.restartOn), {
|
|
1083
|
+
before,
|
|
1084
|
+
changed,
|
|
1085
|
+
configChanged,
|
|
1086
|
+
desired: desired2,
|
|
1087
|
+
path,
|
|
1088
|
+
serviceName,
|
|
1089
|
+
status
|
|
1090
|
+
});
|
|
610
1091
|
};
|
|
611
|
-
var
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
1092
|
+
var deleteContainer = async (runner, output) => {
|
|
1093
|
+
assertMayWrite(runner, output.name);
|
|
1094
|
+
const status = await showUnit(runner, output.serviceName);
|
|
1095
|
+
if (status.activeState === "active" || status.activeState === "activating") {
|
|
1096
|
+
await stopUnit(runner, output.serviceName);
|
|
1097
|
+
}
|
|
1098
|
+
const stat = await runner.stat(output.containerPath);
|
|
1099
|
+
if (stat !== undefined) {
|
|
1100
|
+
if (stat.kind !== "file")
|
|
1101
|
+
throw refuseContainer(output.name, `${output.containerPath} is a ${stat.kind}`);
|
|
1102
|
+
await runner.removeFile(output.containerPath);
|
|
1103
|
+
}
|
|
1104
|
+
await daemonReload(runner);
|
|
616
1105
|
};
|
|
617
1106
|
|
|
1107
|
+
// src/linux/container-handlers.ts
|
|
1108
|
+
var readHandler = (runner, olds, output) => lift(async () => {
|
|
1109
|
+
const found = await readContainer(runner, olds);
|
|
1110
|
+
if (found === undefined)
|
|
1111
|
+
return;
|
|
1112
|
+
return output === undefined ? Unowned3(found) : found;
|
|
1113
|
+
});
|
|
1114
|
+
var diffHandler = (runner, instanceId, news, output) => {
|
|
1115
|
+
if (output === undefined)
|
|
1116
|
+
return noteUnfinished(instanceId);
|
|
1117
|
+
if (isResolved2(news))
|
|
1118
|
+
return lift(() => diffContainer(runner, news, output));
|
|
1119
|
+
const name = resolvedString(news, "name");
|
|
1120
|
+
if (name === undefined || name === output.name)
|
|
1121
|
+
return Effect7.succeed(undefined);
|
|
1122
|
+
return lift(async () => {
|
|
1123
|
+
await assertRenameTarget(runner, output, name);
|
|
1124
|
+
return { action: "replace", deleteFirst: true };
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
var reconcileHandler = (runner, args) => Effect7.flatMap(adoptsAtApply(args), (adopt) => lift(() => reconcileContainer(runner, args.news, args.output, adopt)));
|
|
1128
|
+
var deleteHandler = (runner, output) => lift(() => deleteContainer(runner, output));
|
|
1129
|
+
|
|
1130
|
+
// src/linux/container.ts
|
|
1131
|
+
var PodmanContainer = Resource2("Podman.Container");
|
|
1132
|
+
var PodmanContainerProvider = () => Provider2.effect(PodmanContainer, Effect8.gen(function* () {
|
|
1133
|
+
const runner = yield* HostRunnerService;
|
|
1134
|
+
return PodmanContainer.Provider.of({
|
|
1135
|
+
list: () => Effect8.succeed([]),
|
|
1136
|
+
read: ({ olds, output }) => readHandler(runner, olds, output),
|
|
1137
|
+
diff: ({ instanceId, news, output }) => diffHandler(runner, instanceId, news, output),
|
|
1138
|
+
reconcile: (args) => reconcileHandler(runner, args),
|
|
1139
|
+
delete: ({ output }) => deleteHandler(runner, output)
|
|
1140
|
+
});
|
|
1141
|
+
}));
|
|
1142
|
+
|
|
1143
|
+
// src/linux/remote-file.ts
|
|
1144
|
+
import { Resource as Resource3 } from "alchemy";
|
|
1145
|
+
import { Unowned as Unowned4 } from "alchemy/AdoptPolicy";
|
|
1146
|
+
import { isResolved as isResolved3 } from "alchemy/Diff";
|
|
1147
|
+
import * as Provider3 from "alchemy/Provider";
|
|
1148
|
+
import * as Effect9 from "effect/Effect";
|
|
1149
|
+
|
|
618
1150
|
// src/linux/region.ts
|
|
619
1151
|
var DEFAULT_COMMENT = "#";
|
|
620
1152
|
var tokens = (spec) => {
|
|
@@ -726,8 +1258,8 @@ var fileProblems2 = (props) => {
|
|
|
726
1258
|
};
|
|
727
1259
|
|
|
728
1260
|
// src/linux/remote-file-plan.ts
|
|
729
|
-
var
|
|
730
|
-
var
|
|
1261
|
+
var decoder3 = new TextDecoder;
|
|
1262
|
+
var encoder2 = new TextEncoder;
|
|
731
1263
|
var refuse3 = (path, message) => new Error(`Remote.File ${path}: ${message}`);
|
|
732
1264
|
var numericId2 = (value) => typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : undefined;
|
|
733
1265
|
var identity = async (runner, props) => {
|
|
@@ -748,7 +1280,7 @@ var identity = async (runner, props) => {
|
|
|
748
1280
|
}
|
|
749
1281
|
return { ...uid === undefined ? {} : { uid }, ...gid === undefined ? {} : { gid } };
|
|
750
1282
|
};
|
|
751
|
-
var
|
|
1283
|
+
var digestOf2 = (text) => sha256Hex(encoder2.encode(text));
|
|
752
1284
|
var relocation = (props, output) => {
|
|
753
1285
|
if (props.path !== output.path)
|
|
754
1286
|
return "path";
|
|
@@ -763,16 +1295,16 @@ var ownedDigest = (file, region) => {
|
|
|
763
1295
|
if (file === undefined)
|
|
764
1296
|
return;
|
|
765
1297
|
if (region === undefined)
|
|
766
|
-
return
|
|
1298
|
+
return digestOf2(file);
|
|
767
1299
|
const body = readRegion(file, region);
|
|
768
|
-
return body === undefined ? undefined :
|
|
1300
|
+
return body === undefined ? undefined : digestOf2(body);
|
|
769
1301
|
};
|
|
770
1302
|
var readFileAttributes = async (runner, path, region) => {
|
|
771
1303
|
const stat = await runner.stat(path);
|
|
772
1304
|
if (stat === undefined)
|
|
773
1305
|
return;
|
|
774
1306
|
const bytes = stat.kind === "file" ? await runner.readFile(path) : undefined;
|
|
775
|
-
const text = bytes === undefined ? undefined :
|
|
1307
|
+
const text = bytes === undefined ? undefined : decoder3.decode(bytes);
|
|
776
1308
|
return {
|
|
777
1309
|
contentSha256: ownedDigest(text, region) ?? "",
|
|
778
1310
|
gid: stat.gid,
|
|
@@ -791,13 +1323,13 @@ var planWrite = async (runner, props, want) => {
|
|
|
791
1323
|
throw refuse3(props.path, `is a ${stat.kind}; remove it from whatever owns it before declaring it here`);
|
|
792
1324
|
}
|
|
793
1325
|
const current = stat === undefined ? undefined : await runner.readFile(props.path);
|
|
794
|
-
const before = current === undefined ? undefined :
|
|
1326
|
+
const before = current === undefined ? undefined : decoder3.decode(current);
|
|
795
1327
|
const declaredMode = props.mode ?? DEFAULT_MODE2;
|
|
796
1328
|
if (props.region === undefined) {
|
|
797
1329
|
const write = { mode: declaredMode, ...want };
|
|
798
1330
|
return {
|
|
799
1331
|
before,
|
|
800
|
-
bytes:
|
|
1332
|
+
bytes: encoder2.encode(props.content),
|
|
801
1333
|
converged: before === props.content && matches(stat, write),
|
|
802
1334
|
write
|
|
803
1335
|
};
|
|
@@ -808,7 +1340,7 @@ var planWrite = async (runner, props, want) => {
|
|
|
808
1340
|
}
|
|
809
1341
|
return {
|
|
810
1342
|
before,
|
|
811
|
-
bytes:
|
|
1343
|
+
bytes: encoder2.encode(spliceRegion("", props.region, props.content)),
|
|
812
1344
|
converged: false,
|
|
813
1345
|
write: { mode: declaredMode, ...want }
|
|
814
1346
|
};
|
|
@@ -816,7 +1348,7 @@ var planWrite = async (runner, props, want) => {
|
|
|
816
1348
|
const next = spliceRegion(before, props.region, props.content);
|
|
817
1349
|
return {
|
|
818
1350
|
before,
|
|
819
|
-
bytes:
|
|
1351
|
+
bytes: encoder2.encode(next),
|
|
820
1352
|
converged: next === before,
|
|
821
1353
|
write: { gid: stat?.gid ?? 0, mode: stat?.mode ?? declaredMode, uid: stat?.uid ?? 0 }
|
|
822
1354
|
};
|
|
@@ -830,7 +1362,7 @@ var diffFile = async (runner, news, output) => {
|
|
|
830
1362
|
return { action: "replace" };
|
|
831
1363
|
}
|
|
832
1364
|
const planned = await planWrite(runner, news, want);
|
|
833
|
-
if (planned.converged && output.contentSha256 ===
|
|
1365
|
+
if (planned.converged && output.contentSha256 === digestOf2(news.content)) {
|
|
834
1366
|
return { action: "noop" };
|
|
835
1367
|
}
|
|
836
1368
|
await runner.checkWrite?.(news.path, planned.write);
|
|
@@ -845,7 +1377,7 @@ var reconcileFile = async (runner, props, output, adopt = false) => {
|
|
|
845
1377
|
const prior = move === "none" ? output : undefined;
|
|
846
1378
|
const planned = await planWrite(runner, props, want);
|
|
847
1379
|
const live = ownedDigest(planned.before, props.region);
|
|
848
|
-
const declared =
|
|
1380
|
+
const declared = digestOf2(props.content);
|
|
849
1381
|
if (prior === undefined && live !== undefined && live !== declared && !(adopt && output === undefined)) {
|
|
850
1382
|
throw refuse3(props.path, `${props.region === undefined ? "already exists" : "already carries that managed region"}` + " and is not this resource. Remove it, or declare it as a new resource and deploy --adopt.");
|
|
851
1383
|
}
|
|
@@ -884,11 +1416,11 @@ var deleteFile = async (runner, output) => {
|
|
|
884
1416
|
const bytes = await runner.readFile(output.path);
|
|
885
1417
|
if (bytes === undefined)
|
|
886
1418
|
return;
|
|
887
|
-
const before =
|
|
1419
|
+
const before = decoder3.decode(bytes);
|
|
888
1420
|
const next = removeRegion(before, output.region);
|
|
889
1421
|
if (next === before)
|
|
890
1422
|
return;
|
|
891
|
-
await runner.writeFileAtomic(output.path,
|
|
1423
|
+
await runner.writeFileAtomic(output.path, encoder2.encode(next), {
|
|
892
1424
|
gid: stat.gid,
|
|
893
1425
|
mode: stat.mode,
|
|
894
1426
|
uid: stat.uid
|
|
@@ -896,208 +1428,45 @@ var deleteFile = async (runner, output) => {
|
|
|
896
1428
|
};
|
|
897
1429
|
|
|
898
1430
|
// src/linux/remote-file.ts
|
|
899
|
-
var RemoteFile =
|
|
900
|
-
var RemoteFileProvider = () =>
|
|
1431
|
+
var RemoteFile = Resource3("Remote.File");
|
|
1432
|
+
var RemoteFileProvider = () => Provider3.effect(RemoteFile, Effect9.gen(function* () {
|
|
901
1433
|
const runner = yield* HostRunnerService;
|
|
902
1434
|
return RemoteFile.Provider.of({
|
|
903
|
-
list: () =>
|
|
1435
|
+
list: () => Effect9.succeed([]),
|
|
904
1436
|
read: ({ olds, output }) => lift(async () => {
|
|
905
1437
|
const found = await readFileAttributes(runner, olds.path, olds.region);
|
|
906
1438
|
if (found === undefined)
|
|
907
1439
|
return;
|
|
908
1440
|
if (olds.region !== undefined && found.contentSha256 === "")
|
|
909
1441
|
return;
|
|
910
|
-
return output === undefined ?
|
|
1442
|
+
return output === undefined ? Unowned4(found) : found;
|
|
911
1443
|
}),
|
|
912
1444
|
diff: ({ instanceId, news, output }) => {
|
|
913
1445
|
if (output === undefined)
|
|
914
1446
|
return noteUnfinished(instanceId);
|
|
915
|
-
if (
|
|
1447
|
+
if (isResolved3(news))
|
|
916
1448
|
return lift(() => diffFile(runner, news, output));
|
|
917
1449
|
const path = resolvedString(news, "path");
|
|
918
|
-
return
|
|
1450
|
+
return Effect9.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
|
|
919
1451
|
},
|
|
920
|
-
reconcile: ({ fqn, instanceId, news, output }) =>
|
|
1452
|
+
reconcile: ({ fqn, instanceId, news, output }) => Effect9.flatMap(adoptsAtApply({ fqn, instanceId, output }), (adopt) => lift(() => reconcileFile(runner, news, output, adopt))),
|
|
921
1453
|
delete: ({ output }) => lift(() => deleteFile(runner, output))
|
|
922
1454
|
});
|
|
923
1455
|
}));
|
|
924
1456
|
|
|
925
1457
|
// src/linux/unit.ts
|
|
926
|
-
import { Resource as
|
|
927
|
-
import * as
|
|
928
|
-
import * as
|
|
1458
|
+
import { Resource as Resource4 } from "alchemy";
|
|
1459
|
+
import * as Provider4 from "alchemy/Provider";
|
|
1460
|
+
import * as Effect11 from "effect/Effect";
|
|
929
1461
|
|
|
930
1462
|
// src/linux/unit-handlers.ts
|
|
931
|
-
import { Unowned as
|
|
932
|
-
import { isResolved as
|
|
933
|
-
import * as
|
|
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
|
-
|
|
1009
|
-
// src/linux/unit-form.ts
|
|
1010
|
-
var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
1011
|
-
var UNIT_SEARCH_DIRECTORIES = [
|
|
1012
|
-
"/etc/systemd/system",
|
|
1013
|
-
"/run/systemd/system",
|
|
1014
|
-
"/usr/local/lib/systemd/system",
|
|
1015
|
-
"/usr/lib/systemd/system",
|
|
1016
|
-
"/lib/systemd/system"
|
|
1017
|
-
];
|
|
1018
|
-
var UNIT_WRITE = { gid: 0, mode: 420, uid: 0 };
|
|
1019
|
-
var digestOf2 = (text) => sha256Hex(new TextEncoder().encode(text));
|
|
1020
|
-
var configDigest = (restartOn) => digestOf2((restartOn ?? []).join(`
|
|
1021
|
-
`));
|
|
1022
|
-
var isUnitRunning = (activeState, subState, started) => {
|
|
1023
|
-
if (activeState === "active")
|
|
1024
|
-
return true;
|
|
1025
|
-
if (activeState !== "activating")
|
|
1026
|
-
return false;
|
|
1027
|
-
if (subState === "auto-restart" || subState === "auto-restart-queued")
|
|
1028
|
-
return true;
|
|
1029
|
-
return started;
|
|
1030
|
-
};
|
|
1031
|
-
var renderUnit = (sections) => `${sections.map((section) => [`[${section.name}]`, ...section.lines.map(([key, value]) => `${key}=${value}`)].join(`
|
|
1032
|
-
`)).join(`
|
|
1033
|
-
|
|
1034
|
-
`)}
|
|
1035
|
-
`;
|
|
1036
|
-
var unitText = (props) => props.content ?? renderUnit(props.sections ?? []);
|
|
1037
|
-
var unitPathFor = (props) => `${props.directory ?? DEFAULT_UNIT_DIRECTORY}/${props.name}`;
|
|
1038
|
-
var NAME = /^[A-Za-z0-9][A-Za-z0-9_.@:-]*\.([a-z]+)$/;
|
|
1039
|
-
var TYPES = new Set([
|
|
1040
|
-
"automount",
|
|
1041
|
-
"device",
|
|
1042
|
-
"mount",
|
|
1043
|
-
"path",
|
|
1044
|
-
"scope",
|
|
1045
|
-
"service",
|
|
1046
|
-
"slice",
|
|
1047
|
-
"socket",
|
|
1048
|
-
"swap",
|
|
1049
|
-
"target",
|
|
1050
|
-
"timer"
|
|
1051
|
-
]);
|
|
1052
|
-
var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
|
|
1053
|
-
var unitProblems = (props, expect) => {
|
|
1054
|
-
const found = [];
|
|
1055
|
-
const type = NAME.exec(props.name)?.[1];
|
|
1056
|
-
if (type === undefined || !TYPES.has(type)) {
|
|
1057
|
-
found.push(`name must be <unit>.<type> with a systemd unit type, got ${JSON.stringify(props.name)}`);
|
|
1058
|
-
} else if (expect !== undefined && type !== expect) {
|
|
1059
|
-
found.push(`this resource declares a .${expect} unit; ${props.name} is a .${type}`);
|
|
1060
|
-
}
|
|
1061
|
-
if (props.content === undefined === (props.sections === undefined)) {
|
|
1062
|
-
found.push("declare exactly one of content or sections");
|
|
1063
|
-
}
|
|
1064
|
-
const directory = props.directory ?? DEFAULT_UNIT_DIRECTORY;
|
|
1065
|
-
if (!directory.startsWith("/") || directory.endsWith("/") || directory.includes("/..")) {
|
|
1066
|
-
found.push("directory must be an absolute, normalised path with no trailing slash");
|
|
1067
|
-
}
|
|
1068
|
-
found.push(...textProblems(unitText(props)));
|
|
1069
|
-
if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory)) {
|
|
1070
|
-
found.push(`an enabled unit must live where systemd looks for it by name (${UNIT_SEARCH_DIRECTORIES.join(", ")}); ` + `${directory} is not one of them. Move it, or declare enabled: false.`);
|
|
1071
|
-
}
|
|
1072
|
-
if (props.enabled !== false && !hasInstall(props)) {
|
|
1073
|
-
found.push("enabled units need an [Install] section with WantedBy/RequiredBy/Also/Alias; without one " + 'systemd calls the unit "static" and `systemctl enable` has nothing to link. Add one, or ' + "declare enabled: false and let something else pull it in.");
|
|
1074
|
-
}
|
|
1075
|
-
if (type === "timer" && !unitText(props).includes("[Timer]")) {
|
|
1076
|
-
found.push("a .timer needs a [Timer] section");
|
|
1077
|
-
}
|
|
1078
|
-
return found;
|
|
1079
|
-
};
|
|
1080
|
-
var hasInstall = (props) => {
|
|
1081
|
-
const text = unitText(props);
|
|
1082
|
-
if (!text.includes("[Install]"))
|
|
1083
|
-
return false;
|
|
1084
|
-
const after = text.slice(text.indexOf("[Install]"));
|
|
1085
|
-
return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
|
|
1086
|
-
};
|
|
1087
|
-
var textProblems = (text) => {
|
|
1088
|
-
const found = [];
|
|
1089
|
-
if (text.trim() === "")
|
|
1090
|
-
found.push("the unit file is empty");
|
|
1091
|
-
if (text.includes("\x00"))
|
|
1092
|
-
found.push("the unit file contains NUL");
|
|
1093
|
-
if (!/^\s*\[[A-Za-z]+]/.test(text))
|
|
1094
|
-
found.push("a unit file must begin with a [Section]");
|
|
1095
|
-
return found;
|
|
1096
|
-
};
|
|
1463
|
+
import { Unowned as Unowned5 } from "alchemy/AdoptPolicy";
|
|
1464
|
+
import { isResolved as isResolved4 } from "alchemy/Diff";
|
|
1465
|
+
import * as Effect10 from "effect/Effect";
|
|
1097
1466
|
|
|
1098
1467
|
// src/linux/unit-settle.ts
|
|
1099
1468
|
var refuse4 = (name, message) => new Error(`Systemd.Unit ${name}: ${message}`);
|
|
1100
|
-
var
|
|
1469
|
+
var attributesOf2 = (props, unitSha256, status) => ({
|
|
1101
1470
|
active: status.activeState === "active" || status.activeState === "activating",
|
|
1102
1471
|
activeState: status.activeState,
|
|
1103
1472
|
configSha256: configDigest(props.restartOn),
|
|
@@ -1108,7 +1477,7 @@ var attributesOf = (props, unitSha256, status) => ({
|
|
|
1108
1477
|
unitSha256,
|
|
1109
1478
|
...status.unitFileState === undefined ? {} : { unitFileState: status.unitFileState }
|
|
1110
1479
|
});
|
|
1111
|
-
var
|
|
1480
|
+
var settle2 = async (runner, props, step) => {
|
|
1112
1481
|
const wantEnabled = props.enabled !== false;
|
|
1113
1482
|
const wantStarted = props.started !== false;
|
|
1114
1483
|
const isEnabled = step.status.unitFileState === "enabled" || step.status.unitFileState === "enabled-runtime";
|
|
@@ -1143,7 +1512,7 @@ var settle = async (runner, props, step) => {
|
|
|
1143
1512
|
if (wantStarted && !isUnitRunning(after.activeState, after.subState, wantStarted)) {
|
|
1144
1513
|
return undo(`systemctl reported success but the unit is ${after.activeState}.`);
|
|
1145
1514
|
}
|
|
1146
|
-
const settled =
|
|
1515
|
+
const settled = attributesOf2(props, step.desired, after);
|
|
1147
1516
|
if (wantEnabled && !settled.enabled) {
|
|
1148
1517
|
return undo(`systemctl enable reported success but systemd calls the unit ` + `${JSON.stringify(after.unitFileState ?? "")}, not enabled.`);
|
|
1149
1518
|
}
|
|
@@ -1151,61 +1520,61 @@ var settle = async (runner, props, step) => {
|
|
|
1151
1520
|
};
|
|
1152
1521
|
|
|
1153
1522
|
// src/linux/unit-preflight.ts
|
|
1154
|
-
var
|
|
1155
|
-
var
|
|
1523
|
+
var decoder4 = new TextDecoder;
|
|
1524
|
+
var assertValid2 = (props, expect) => {
|
|
1156
1525
|
const found = unitProblems(props, expect);
|
|
1157
1526
|
if (found.length > 0)
|
|
1158
1527
|
throw refuse4(props.name, found.join("; "));
|
|
1159
1528
|
};
|
|
1160
|
-
var
|
|
1529
|
+
var assertMayWrite2 = (runner, name) => {
|
|
1161
1530
|
if (canActAsRoot(runner))
|
|
1162
1531
|
return;
|
|
1163
1532
|
throw refuse4(name, "a systemd unit is root-owned and systemctl needs root. Point the runner at a destination " + "whose ssh user is root, or provide a privileged HostRunner. This provider never calls sudo.");
|
|
1164
1533
|
};
|
|
1165
|
-
var
|
|
1534
|
+
var assertUsable2 = (props, status) => {
|
|
1166
1535
|
if (status.loadState === "masked" || status.unitFileState === "masked") {
|
|
1167
1536
|
throw refuse4(props.name, "is masked. If that is stale, `systemctl unmask` it deliberately, then redeploy. Nothing " + "was written.");
|
|
1168
1537
|
}
|
|
1169
1538
|
};
|
|
1170
|
-
var
|
|
1539
|
+
var assertUnclaimed2 = async (runner, props) => {
|
|
1171
1540
|
const path = unitPathFor(props);
|
|
1172
1541
|
const bytes = await runner.readFile(path);
|
|
1173
|
-
const live = bytes === undefined ? undefined :
|
|
1174
|
-
if (live !== undefined && live !==
|
|
1542
|
+
const live = bytes === undefined ? undefined : digestOf(decoder4.decode(bytes));
|
|
1543
|
+
if (live !== undefined && live !== digestOf(unitText(props))) {
|
|
1175
1544
|
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.");
|
|
1176
1545
|
}
|
|
1177
1546
|
};
|
|
1178
|
-
var
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1547
|
+
var assertRenameTarget2 = async (runner, old, name) => {
|
|
1548
|
+
assertMayWrite2(runner, old.name);
|
|
1549
|
+
assertMayWrite2(runner, name);
|
|
1550
|
+
assertUsable2({ name }, await showUnit(runner, name));
|
|
1182
1551
|
};
|
|
1183
|
-
var
|
|
1184
|
-
|
|
1185
|
-
await
|
|
1186
|
-
await
|
|
1552
|
+
var assertReplaceable2 = async (runner, next, old, expect) => {
|
|
1553
|
+
assertValid2(next, expect);
|
|
1554
|
+
await assertRenameTarget2(runner, old, next.name);
|
|
1555
|
+
await assertUnclaimed2(runner, next);
|
|
1187
1556
|
await runner.checkWrite?.(unitPathFor(next), UNIT_WRITE);
|
|
1188
1557
|
};
|
|
1189
1558
|
|
|
1190
1559
|
// src/linux/unit-lifecycle.ts
|
|
1191
|
-
var
|
|
1192
|
-
var
|
|
1560
|
+
var encoder3 = new TextEncoder;
|
|
1561
|
+
var decoder5 = new TextDecoder;
|
|
1193
1562
|
var readUnit = async (runner, props) => {
|
|
1194
1563
|
const bytes = await runner.readFile(unitPathFor(props));
|
|
1195
1564
|
const status = await showUnit(runner, props.name);
|
|
1196
1565
|
if (bytes === undefined && !status.known)
|
|
1197
1566
|
return;
|
|
1198
|
-
return
|
|
1567
|
+
return attributesOf2(props, bytes === undefined ? "" : digestOf(decoder5.decode(bytes)), status);
|
|
1199
1568
|
};
|
|
1200
1569
|
var diffUnit = async (runner, news, output, expect) => {
|
|
1201
|
-
|
|
1570
|
+
assertValid2(news, expect);
|
|
1202
1571
|
if (news.name !== output.name || unitPathFor(news) !== output.unitPath) {
|
|
1203
|
-
await
|
|
1572
|
+
await assertReplaceable2(runner, news, output, expect);
|
|
1204
1573
|
return { action: "replace", deleteFirst: true };
|
|
1205
1574
|
}
|
|
1206
|
-
const desired2 =
|
|
1575
|
+
const desired2 = digestOf(unitText(news));
|
|
1207
1576
|
const update = async () => {
|
|
1208
|
-
|
|
1577
|
+
assertMayWrite2(runner, news.name);
|
|
1209
1578
|
await runner.checkWrite?.(unitPathFor(news), UNIT_WRITE);
|
|
1210
1579
|
return { action: "update" };
|
|
1211
1580
|
};
|
|
@@ -1226,18 +1595,18 @@ var diffUnit = async (runner, news, output, expect) => {
|
|
|
1226
1595
|
return status.needDaemonReload ? update() : { action: "noop" };
|
|
1227
1596
|
};
|
|
1228
1597
|
var reconcileUnit = async (runner, props, output, adopt = false, expect) => {
|
|
1229
|
-
|
|
1230
|
-
|
|
1598
|
+
assertValid2(props, expect);
|
|
1599
|
+
assertMayWrite2(runner, props.name);
|
|
1231
1600
|
const path = unitPathFor(props);
|
|
1232
1601
|
const status = await showUnit(runner, props.name);
|
|
1233
|
-
|
|
1602
|
+
assertUsable2(props, status);
|
|
1234
1603
|
const text = unitText(props);
|
|
1235
|
-
const desired2 =
|
|
1604
|
+
const desired2 = digestOf(text);
|
|
1236
1605
|
const before = await runner.readFile(path);
|
|
1237
|
-
const liveSha = before === undefined ? undefined :
|
|
1606
|
+
const liveSha = before === undefined ? undefined : digestOf(decoder5.decode(before));
|
|
1238
1607
|
const moved = output !== undefined && (output.name !== props.name || output.unitPath !== path);
|
|
1239
1608
|
if (moved && output !== undefined)
|
|
1240
|
-
await
|
|
1609
|
+
await assertReplaceable2(runner, props, output, expect);
|
|
1241
1610
|
const prior = moved ? undefined : output;
|
|
1242
1611
|
if (prior === undefined && !(adopt && output === undefined) && liveSha !== undefined && liveSha !== desired2) {
|
|
1243
1612
|
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.");
|
|
@@ -1248,14 +1617,14 @@ var reconcileUnit = async (runner, props, output, adopt = false, expect) => {
|
|
|
1248
1617
|
const stale = prior !== undefined && prior.unitSha256 !== desired2;
|
|
1249
1618
|
const configChanged = prior !== undefined && prior.configSha256 !== configDigest(props.restartOn);
|
|
1250
1619
|
if (wrote)
|
|
1251
|
-
await runner.writeFileAtomic(path,
|
|
1620
|
+
await runner.writeFileAtomic(path, encoder3.encode(text), UNIT_WRITE);
|
|
1252
1621
|
if (wrote || status.needDaemonReload)
|
|
1253
1622
|
await daemonReload(runner);
|
|
1254
1623
|
const changed = wrote || stale || status.needDaemonReload;
|
|
1255
|
-
return
|
|
1624
|
+
return settle2(runner, props, { before, changed, configChanged, desired: desired2, path, status });
|
|
1256
1625
|
};
|
|
1257
1626
|
var deleteUnit = async (runner, output) => {
|
|
1258
|
-
|
|
1627
|
+
assertMayWrite2(runner, output.name);
|
|
1259
1628
|
const status = await showUnit(runner, output.name);
|
|
1260
1629
|
if (status.activeState === "active" || status.activeState === "activating") {
|
|
1261
1630
|
await stopUnit(runner, output.name);
|
|
@@ -1273,54 +1642,54 @@ var deleteUnit = async (runner, output) => {
|
|
|
1273
1642
|
};
|
|
1274
1643
|
|
|
1275
1644
|
// src/linux/unit-handlers.ts
|
|
1276
|
-
var
|
|
1645
|
+
var readHandler2 = (runner, olds, output) => lift(async () => {
|
|
1277
1646
|
const found = await readUnit(runner, olds);
|
|
1278
1647
|
if (found === undefined)
|
|
1279
1648
|
return;
|
|
1280
|
-
return output === undefined ?
|
|
1649
|
+
return output === undefined ? Unowned5(found) : found;
|
|
1281
1650
|
});
|
|
1282
|
-
var
|
|
1651
|
+
var diffHandler2 = (runner, instanceId, news, output, expect) => {
|
|
1283
1652
|
if (output === undefined)
|
|
1284
1653
|
return noteUnfinished(instanceId);
|
|
1285
|
-
if (
|
|
1654
|
+
if (isResolved4(news))
|
|
1286
1655
|
return lift(() => diffUnit(runner, news, output, expect));
|
|
1287
1656
|
const name = resolvedString(news, "name");
|
|
1288
1657
|
if (name === undefined || name === output.name)
|
|
1289
|
-
return
|
|
1658
|
+
return Effect10.succeed(undefined);
|
|
1290
1659
|
return lift(async () => {
|
|
1291
|
-
await
|
|
1660
|
+
await assertRenameTarget2(runner, output, name);
|
|
1292
1661
|
return { action: "replace", deleteFirst: true };
|
|
1293
1662
|
});
|
|
1294
1663
|
};
|
|
1295
|
-
var
|
|
1296
|
-
var
|
|
1664
|
+
var reconcileHandler2 = (runner, args, expect) => Effect10.flatMap(adoptsAtApply(args), (adopt) => lift(() => reconcileUnit(runner, args.news, args.output, adopt, expect)));
|
|
1665
|
+
var deleteHandler2 = (runner, output) => lift(() => deleteUnit(runner, output));
|
|
1297
1666
|
|
|
1298
1667
|
// src/linux/unit.ts
|
|
1299
|
-
var SystemdUnit =
|
|
1300
|
-
var SystemdTimer =
|
|
1301
|
-
var SystemdUnitProvider = () =>
|
|
1668
|
+
var SystemdUnit = Resource4("Systemd.Unit");
|
|
1669
|
+
var SystemdTimer = Resource4("Systemd.Timer");
|
|
1670
|
+
var SystemdUnitProvider = () => Provider4.effect(SystemdUnit, Effect11.gen(function* () {
|
|
1302
1671
|
const runner = yield* HostRunnerService;
|
|
1303
1672
|
return SystemdUnit.Provider.of({
|
|
1304
|
-
list: () =>
|
|
1305
|
-
read: ({ olds, output }) =>
|
|
1306
|
-
diff: ({ instanceId, news, output }) =>
|
|
1307
|
-
reconcile: (args) =>
|
|
1308
|
-
delete: ({ output }) =>
|
|
1673
|
+
list: () => Effect11.succeed([]),
|
|
1674
|
+
read: ({ olds, output }) => readHandler2(runner, olds, output),
|
|
1675
|
+
diff: ({ instanceId, news, output }) => diffHandler2(runner, instanceId, news, output),
|
|
1676
|
+
reconcile: (args) => reconcileHandler2(runner, args),
|
|
1677
|
+
delete: ({ output }) => deleteHandler2(runner, output)
|
|
1309
1678
|
});
|
|
1310
1679
|
}));
|
|
1311
|
-
var SystemdTimerProvider = () =>
|
|
1680
|
+
var SystemdTimerProvider = () => Provider4.effect(SystemdTimer, Effect11.gen(function* () {
|
|
1312
1681
|
const runner = yield* HostRunnerService;
|
|
1313
1682
|
return SystemdTimer.Provider.of({
|
|
1314
|
-
list: () =>
|
|
1315
|
-
read: ({ olds, output }) =>
|
|
1316
|
-
diff: ({ instanceId, news, output }) =>
|
|
1317
|
-
reconcile: (args) =>
|
|
1318
|
-
delete: ({ output }) =>
|
|
1683
|
+
list: () => Effect11.succeed([]),
|
|
1684
|
+
read: ({ olds, output }) => readHandler2(runner, olds, output),
|
|
1685
|
+
diff: ({ instanceId, news, output }) => diffHandler2(runner, instanceId, news, output, "timer"),
|
|
1686
|
+
reconcile: (args) => reconcileHandler2(runner, args, "timer"),
|
|
1687
|
+
delete: ({ output }) => deleteHandler2(runner, output)
|
|
1319
1688
|
});
|
|
1320
1689
|
}));
|
|
1321
1690
|
|
|
1322
1691
|
// src/linux/providers.ts
|
|
1323
|
-
var linuxProviders = (runner) => Layer2.mergeAll(HostDirectoryProvider(), RemoteFileProvider(), SystemdTimerProvider(), SystemdUnitProvider()).pipe(Layer2.provide(hostRunnerLayer(runner)));
|
|
1692
|
+
var linuxProviders = (runner) => Layer2.mergeAll(HostDirectoryProvider(), PodmanContainerProvider(), RemoteFileProvider(), SystemdTimerProvider(), SystemdUnitProvider()).pipe(Layer2.provide(hostRunnerLayer(runner)));
|
|
1324
1693
|
// src/linux/ssh-runner.ts
|
|
1325
1694
|
import { spawn } from "node:child_process";
|
|
1326
1695
|
import { randomBytes } from "node:crypto";
|
|
@@ -2726,6 +3095,10 @@ export {
|
|
|
2726
3095
|
HostDirectory,
|
|
2727
3096
|
HostDirectoryProvider,
|
|
2728
3097
|
HostRunnerService,
|
|
3098
|
+
PodmanContainer,
|
|
3099
|
+
PodmanContainerProvider,
|
|
3100
|
+
QUADLET_DEFAULT_DIRECTORY,
|
|
3101
|
+
QUADLET_SEARCH_DIRECTORIES,
|
|
2729
3102
|
RemoteFile,
|
|
2730
3103
|
RemoteFileProvider,
|
|
2731
3104
|
SystemdTimer,
|
|
@@ -2740,5 +3113,5 @@ export {
|
|
|
2740
3113
|
sshSudoRunner
|
|
2741
3114
|
};
|
|
2742
3115
|
|
|
2743
|
-
//# debugId=
|
|
3116
|
+
//# debugId=1EC8A0E327D500A164756E2164756E21
|
|
2744
3117
|
//# sourceMappingURL=index.js.map
|