@homeflare/alchemy 0.37.7 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/caddy/admin.d.ts +42 -8
- package/dist/caddy/admin.d.ts.map +1 -1
- package/dist/caddy/config.d.ts +9 -3
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/fake-caddy.d.ts +5 -0
- package/dist/caddy/fake-caddy.d.ts.map +1 -1
- package/dist/caddy/index.js +10 -6
- package/dist/caddy/index.js.map +5 -5
- package/dist/caddy/providers.d.ts +19 -12
- package/dist/caddy/providers.d.ts.map +1 -1
- package/dist/linux/container-form.d.ts +1 -1
- package/dist/linux/container-generator.d.ts +28 -0
- package/dist/linux/container-generator.d.ts.map +1 -1
- package/dist/linux/container-lifecycle.d.ts +18 -1
- package/dist/linux/container-lifecycle.d.ts.map +1 -1
- package/dist/linux/container-preflight.d.ts +34 -0
- package/dist/linux/container-preflight.d.ts.map +1 -1
- package/dist/linux/container-settle.d.ts.map +1 -1
- package/dist/linux/fake-sudo.d.ts +14 -2
- package/dist/linux/fake-sudo.d.ts.map +1 -1
- package/dist/linux/index.js +76 -29
- package/dist/linux/index.js.map +11 -10
- package/dist/linux/sudo-exec.d.ts +16 -0
- package/dist/linux/sudo-exec.d.ts.map +1 -1
- package/dist/linux/unit-form.d.ts.map +1 -1
- package/dist/linux/unit-text.d.ts +35 -0
- package/dist/linux/unit-text.d.ts.map +1 -0
- package/docs/linux-sudo.md +18 -7
- package/package.json +1 -1
- package/src/caddy/admin-transport-scope.test.ts +127 -0
- package/src/caddy/admin.ts +45 -9
- package/src/caddy/config-adopt-provider.test.ts +2 -2
- package/src/caddy/config-format-warning.test.ts +9 -3
- package/src/caddy/config.ts +20 -4
- package/src/caddy/fake-caddy.ts +10 -1
- package/src/caddy/providers.test.ts +3 -2
- package/src/caddy/providers.ts +18 -11
- package/src/linux/container-form.ts +1 -1
- package/src/linux/container-generator.test.ts +33 -0
- package/src/linux/container-generator.ts +72 -0
- package/src/linux/container-lifecycle.test.ts +6 -3
- package/src/linux/container-lifecycle.ts +23 -2
- package/src/linux/container-preflight.ts +47 -0
- package/src/linux/container-settle.ts +12 -1
- package/src/linux/container-shadow.test.ts +131 -0
- package/src/linux/fake-sudo.ts +21 -5
- package/src/linux/sudo-exec.ts +60 -10
- package/src/linux/sudo-lifecycle.test.ts +64 -3
- package/src/linux/unit-form.ts +4 -18
- package/src/linux/unit-text.test.ts +67 -0
- package/src/linux/unit-text.ts +58 -0
|
@@ -27,6 +27,40 @@ export declare const assertUsable: (props: Pick<ContainerProps, 'name'>, status:
|
|
|
27
27
|
* this declaration's own leftover, not someone else's.
|
|
28
28
|
*/
|
|
29
29
|
export declare const assertUnclaimed: (runner: HostRunner, props: ContainerProps) => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* ⛔ A PLAIN UNIT SHARING THE SERVICE NAME CAN OUTRANK QUADLET'S GENERATOR, SILENTLY — but only
|
|
32
|
+
* when it sits in one of the directories that actually search BEFORE `${GENERATED_UNIT_DIRECTORY}`
|
|
33
|
+
* in systemd's own unit load path (systemd.unit(5) "Unit File Load Path") — `/etc/systemd/system`
|
|
34
|
+
* chief among them (`isShadowingFragment`, container-generator.ts, has the full list and the
|
|
35
|
+
* measured facts). ⛔ CORRECTED ON ADVERSARIAL RE-REVIEW: an EARLIER version of this check refused
|
|
36
|
+
* for ANY `FragmentPath` not literally under the generator directory, including the vendor
|
|
37
|
+
* directories (`/usr/local/lib/systemd/system`, `/usr/lib/systemd/system`) — those are LOWER
|
|
38
|
+
* precedence than the generator, not higher, so a plain unit there is harmlessly shadowed BY our
|
|
39
|
+
* generated unit once we write and reload; refusing for it was a false positive that blocked a
|
|
40
|
+
* create/adopt apply would have handled fine (exactly what S49 "neither looser nor stricter"
|
|
41
|
+
* forbids). With no `.container` file of ours on disk yet, a `FragmentPath` under one of the
|
|
42
|
+
* genuinely higher-precedence directories means some OTHER unit file — hand-written, or shipped by
|
|
43
|
+
* a package, and admin-controlled rather than vendor-controlled — already answers to this name:
|
|
44
|
+
* writing and reloading would generate a unit systemd never actually loads, because the
|
|
45
|
+
* higher-precedence plain one still wins.
|
|
46
|
+
* ★ TWIN OF `unit-preflight.ts`'s `assertUnclaimed`, for the one failure mode `Systemd.Unit` can't
|
|
47
|
+
* have: there is no generator standing between ITS file and the unit systemd loads, so nothing
|
|
48
|
+
* there can be shadowed the way a Quadlet generation can.
|
|
49
|
+
* ★ ONLY WHEN THE `.container` FILE ITSELF IS ABSENT (called from `readContainer` in that branch):
|
|
50
|
+
* a present file with different content is `assertUnclaimed`'s job; a present file that IS ours
|
|
51
|
+
* generating a shadowed unit is `verifyGenerated`'s job (container-generator.ts, hardened to check
|
|
52
|
+
* this same `FragmentPath` fact), at apply time, once daemon-reload has actually run — this is the
|
|
53
|
+
* "nothing declared yet" half neither of those covers, and the one this bug leaves refusing only
|
|
54
|
+
* mid-apply instead of at plan time for the directories that genuinely shadow.
|
|
55
|
+
* ⚠️ NEVER FIRES WITHOUT A CONCRETE `fragmentPath` TO NAME, AND NEVER FOR A MASKED UNIT — MEASURED
|
|
56
|
+
* against this family's own fake (`fakeQuadletHost`, unlike `sudo-lifecycle.test.ts`'s, which is
|
|
57
|
+
* a different fake and reads differently): `placeUnit(path, text, {masked: true})` still reports
|
|
58
|
+
* a real `FragmentPath`. Refusing there anyway would work, but with THIS function's generic
|
|
59
|
+
* "plain unit" wording instead of `assertUsable`'s specific, more actionable "is masked, run
|
|
60
|
+
* `systemctl unmask` deliberately" — masking is a person's decision, not an accident to explain
|
|
61
|
+
* as a name collision. `assertUsable` stays the one place that message comes from.
|
|
62
|
+
*/
|
|
63
|
+
export declare const assertUnshadowed: (props: Pick<ContainerProps, 'name'>, status: UnitStatus) => void;
|
|
30
64
|
/** The half of a rename check that needs only the NEW NAME — mirrors `assertRenameTarget`. */
|
|
31
65
|
export declare const assertRenameTarget: (runner: HostRunner, old: ContainerAttributes, name: string) => Promise<void>;
|
|
32
66
|
/** A rename, checked at plan time, before the plan may promise a delete-first replace. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-preflight.d.ts","sourceRoot":"","sources":["../../src/linux/container-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"container-preflight.d.ts","sourceRoot":"","sources":["../../src/linux/container-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,sBAAsB,CAAC;AAErE,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,gBAAgB,CAAC;AAE3D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAMpB,MAAM,qBAAqB,CAAC;AAI7B,qEAAqE;AACrE,eAAO,MAAM,WAAW,UAAW,cAAc,KAAG,IAGnD,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,cAAc,WAAY,UAAU,QAAQ,MAAM,KAAG,IAQjE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,UAAW,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,UAAU,KAAG,IAQtF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,WAAkB,UAAU,SAAS,cAAc,KAAG,OAAO,CAAC,IAAI,CAW7F,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,gBAAgB,UAAW,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,UAAU,UAAU,KAAG,IAW1F,CAAC;AAEF,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,WACrB,UAAU,OACb,mBAAmB,QAClB,MAAM,KACX,OAAO,CAAC,IAAI,CAId,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,WACpB,UAAU,QACZ,cAAc,OACf,mBAAmB,KACvB,OAAO,CAAC,IAAI,CAKd,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-settle.d.ts","sourceRoot":"","sources":["../../src/linux/container-settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAMhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,YAAY,UAChB,cAAc,gBACP,MAAM,mBACH,MAAM,iBACR,MAAM,eACR,MAAM,UACX,UAAU,KACjB,mBAWD,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,WACT,UAAU,SACX,cAAc,gBACP,MAAM,QACd,MAAM,KACX,OAAO,CAAC,mBAAmB,
|
|
1
|
+
{"version":3,"file":"container-settle.d.ts","sourceRoot":"","sources":["../../src/linux/container-settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EACL,KAAK,UAAU,EAMhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,YAAY,UAChB,cAAc,gBACP,MAAM,mBACH,MAAM,iBACR,MAAM,eACR,MAAM,UACX,UAAU,KACjB,mBAWD,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,WACT,UAAU,SACX,cAAc,gBACP,MAAM,QACd,MAAM,KACX,OAAO,CAAC,mBAAmB,CAqC7B,CAAC"}
|
|
@@ -12,19 +12,31 @@
|
|
|
12
12
|
* `mktemp -d` registers a fresh 0700 directory the operator owns.
|
|
13
13
|
* ★ SUDO REFUSES A CALL THAT WAS NOT LOGGED FIRST, so every test holds the runner to "logged
|
|
14
14
|
* before it runs", exactly as the Mac fake does.
|
|
15
|
+
* ★ `hostFactory`/`prefixes` (below) let sudo-lifecycle.test.ts drive `fakeQuadletHost` (not just
|
|
16
|
+
* the plain `fakeLinuxHost`) through this SAME elevation wiring, for the Podman.Container-through
|
|
17
|
+
* -sshSudoRunner regression tests — `fakeQuadletHost` returns the exact shape `fakeLinuxHost`
|
|
18
|
+
* does (`FakeLinuxOptions` in, the same fields out), so every handler below that reads
|
|
19
|
+
* `fake.dirs`/`fake.modes`/`fake.files` or calls `fake.runner.exec` works unchanged either way.
|
|
15
20
|
*/
|
|
16
21
|
import type { FileStat, HostRunner } from '../launchd/runner.ts';
|
|
17
22
|
import { SUDO_SAYS } from '../launchd/fake-sudo.ts';
|
|
23
|
+
import { type FakeLinuxOptions, fakeLinuxHost } from './fake-linux-host.ts';
|
|
18
24
|
import { type RootState } from './fake-sudo-root.ts';
|
|
19
25
|
export { SUDO_SAYS } from '../launchd/fake-sudo.ts';
|
|
20
26
|
export declare const OPERATOR = 1000;
|
|
21
27
|
export declare const PREFIXES: string[];
|
|
28
|
+
export type FakeSudoHostOptions = {
|
|
29
|
+
/** Swap `fakeLinuxHost` for a variant that models more than plain units — `fakeQuadletHost`. */
|
|
30
|
+
readonly hostFactory?: (options: FakeLinuxOptions) => ReturnType<typeof fakeLinuxHost>;
|
|
31
|
+
/** The runner's own declared prefixes. @default PREFIXES */
|
|
32
|
+
readonly prefixes?: readonly string[];
|
|
33
|
+
};
|
|
22
34
|
/**
|
|
23
35
|
* `extraDirs` adds more directories to the fake filesystem (a pre-existing tree outside every
|
|
24
|
-
* prefix, say); it never changes the runner's OWN prefixes, which
|
|
36
|
+
* prefix, say); it never changes the runner's OWN prefixes, which default to `PREFIXES`.
|
|
25
37
|
* `acl` is the set of directory paths a test wants to carry the `+` (POSIX ACL) flag.
|
|
26
38
|
*/
|
|
27
|
-
export declare const fakeSudoHost: (extraDirs?: Record<string, number>, acl?: ReadonlySet<string
|
|
39
|
+
export declare const fakeSudoHost: (extraDirs?: Record<string, number>, acl?: ReadonlySet<string>, { hostFactory, prefixes }?: FakeSudoHostOptions) => {
|
|
28
40
|
base: HostRunner;
|
|
29
41
|
fake: {
|
|
30
42
|
calls: string[][];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fake-sudo.d.ts","sourceRoot":"","sources":["../../src/linux/fake-sudo.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fake-sudo.d.ts","sourceRoot":"","sources":["../../src/linux/fake-sudo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAc,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,qBAAqB,CAAC;AAK7D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,eAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,eAAO,MAAM,QAAQ,UAA4C,CAAC;AAyBlE,MAAM,MAAM,mBAAmB,GAAG;IAChC,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACvF,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,eACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,WAAW,CAAC,MAAM,CAAC,8BAC8B,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;cAaxC,IAAI,GAAG,MAAM,OAAO,SAAS;;CAyE/D,CAAC"}
|
package/dist/linux/index.js
CHANGED
|
@@ -635,6 +635,29 @@ var renderJob = (props) => {
|
|
|
635
635
|
return { bytes, sha256: sha256Hex(bytes), text };
|
|
636
636
|
};
|
|
637
637
|
|
|
638
|
+
// src/linux/unit-text.ts
|
|
639
|
+
var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
|
|
640
|
+
var hasInstallSection = (text) => {
|
|
641
|
+
if (!text.includes("[Install]"))
|
|
642
|
+
return false;
|
|
643
|
+
const after = text.slice(text.indexOf("[Install]"));
|
|
644
|
+
return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
|
|
645
|
+
};
|
|
646
|
+
var firstSubstantiveLine = (text) => text.split(`
|
|
647
|
+
`).find((line) => !(line.trim() === "" || line.startsWith("#") || line.startsWith(";")));
|
|
648
|
+
var textProblems = (text) => {
|
|
649
|
+
const found = [];
|
|
650
|
+
if (text.trim() === "")
|
|
651
|
+
found.push("the unit file is empty");
|
|
652
|
+
if (text.includes("\x00"))
|
|
653
|
+
found.push("the unit file contains NUL");
|
|
654
|
+
const first = firstSubstantiveLine(text);
|
|
655
|
+
if (first === undefined || !/^\s*\[[A-Za-z]+]/.test(first)) {
|
|
656
|
+
found.push("a unit file must begin with a [Section] (blank lines and #/; comments before it are fine)");
|
|
657
|
+
}
|
|
658
|
+
return found;
|
|
659
|
+
};
|
|
660
|
+
|
|
638
661
|
// src/linux/unit-form.ts
|
|
639
662
|
var DEFAULT_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
640
663
|
var UNIT_SEARCH_DIRECTORIES = [
|
|
@@ -678,7 +701,6 @@ var TYPES = new Set([
|
|
|
678
701
|
"target",
|
|
679
702
|
"timer"
|
|
680
703
|
]);
|
|
681
|
-
var INSTALL_KEYS = new Set(["WantedBy", "RequiredBy", "UpsertedBy", "Also", "Alias"]);
|
|
682
704
|
var unitProblems = (props, expect) => {
|
|
683
705
|
const found = [];
|
|
684
706
|
const type = NAME.exec(props.name)?.[1];
|
|
@@ -698,7 +720,7 @@ var unitProblems = (props, expect) => {
|
|
|
698
720
|
if (props.enabled !== false && !UNIT_SEARCH_DIRECTORIES.includes(directory)) {
|
|
699
721
|
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.`);
|
|
700
722
|
}
|
|
701
|
-
if (props.enabled !== false && !
|
|
723
|
+
if (props.enabled !== false && !hasInstallSection(unitText(props))) {
|
|
702
724
|
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.");
|
|
703
725
|
}
|
|
704
726
|
if (type === "timer" && !unitText(props).includes("[Timer]")) {
|
|
@@ -706,23 +728,6 @@ var unitProblems = (props, expect) => {
|
|
|
706
728
|
}
|
|
707
729
|
return found;
|
|
708
730
|
};
|
|
709
|
-
var hasInstall = (props) => {
|
|
710
|
-
const text = unitText(props);
|
|
711
|
-
if (!text.includes("[Install]"))
|
|
712
|
-
return false;
|
|
713
|
-
const after = text.slice(text.indexOf("[Install]"));
|
|
714
|
-
return [...INSTALL_KEYS].some((key) => new RegExp(`^${key}=`, "m").test(after));
|
|
715
|
-
};
|
|
716
|
-
var textProblems = (text) => {
|
|
717
|
-
const found = [];
|
|
718
|
-
if (text.trim() === "")
|
|
719
|
-
found.push("the unit file is empty");
|
|
720
|
-
if (text.includes("\x00"))
|
|
721
|
-
found.push("the unit file contains NUL");
|
|
722
|
-
if (!/^\s*\[[A-Za-z]+]/.test(text))
|
|
723
|
-
found.push("a unit file must begin with a [Section]");
|
|
724
|
-
return found;
|
|
725
|
-
};
|
|
726
731
|
|
|
727
732
|
// src/linux/systemctl.ts
|
|
728
733
|
var SYSTEMCTL = "systemctl";
|
|
@@ -802,6 +807,19 @@ var stopUnit = (runner, name) => run(runner, ["stop", "--", name]);
|
|
|
802
807
|
var restartUnit = (runner, name) => run(runner, ["restart", "--", name]);
|
|
803
808
|
|
|
804
809
|
// src/linux/container-generator.ts
|
|
810
|
+
var GENERATED_UNIT_DIRECTORY = "/run/systemd/generator";
|
|
811
|
+
var isGeneratorFragment = (fragmentPath) => fragmentPath !== undefined && fragmentPath.startsWith(`${GENERATED_UNIT_DIRECTORY}/`);
|
|
812
|
+
var SHADOWING_UNIT_DIRECTORIES = [
|
|
813
|
+
"/etc/systemd/system.control",
|
|
814
|
+
"/run/systemd/system.control",
|
|
815
|
+
"/run/systemd/transient",
|
|
816
|
+
"/etc/systemd/system",
|
|
817
|
+
"/etc/systemd/system.attached",
|
|
818
|
+
"/run/systemd/system",
|
|
819
|
+
"/run/systemd/system.attached"
|
|
820
|
+
];
|
|
821
|
+
var isShadowingFragment = (fragmentPath) => fragmentPath !== undefined && SHADOWING_UNIT_DIRECTORIES.some((dir) => fragmentPath.startsWith(`${dir}/`));
|
|
822
|
+
|
|
805
823
|
class QuadletGeneratorError extends Error {
|
|
806
824
|
constructor(name, detail) {
|
|
807
825
|
super(`Podman.Container ${name}: ${detail}`);
|
|
@@ -814,6 +832,9 @@ var verifyGenerated = (name, containerPath, serviceName, status) => {
|
|
|
814
832
|
if (!status.known || status.loadState !== "loaded") {
|
|
815
833
|
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).`);
|
|
816
834
|
}
|
|
835
|
+
if (status.fragmentPath !== undefined && !isGeneratorFragment(status.fragmentPath)) {
|
|
836
|
+
throw new QuadletGeneratorError(name, `${serviceName} is loaded from ${status.fragmentPath}, not from Quadlet’s generator ` + `(${GENERATED_UNIT_DIRECTORY}/…) — daemon-reload ran, but a plain unit at that path still ` + "answers to this name, and systemd loaded IT, not ours. Move the plain unit aside — a " + "cutover — and redeploy.");
|
|
837
|
+
}
|
|
817
838
|
if (status.sourcePath !== undefined && status.sourcePath !== containerPath) {
|
|
818
839
|
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).");
|
|
819
840
|
}
|
|
@@ -954,6 +975,15 @@ var assertUnclaimed = async (runner, props) => {
|
|
|
954
975
|
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.");
|
|
955
976
|
}
|
|
956
977
|
};
|
|
978
|
+
var assertUnshadowed = (props, status) => {
|
|
979
|
+
if (!status.known || status.fragmentPath === undefined)
|
|
980
|
+
return;
|
|
981
|
+
if (status.loadState === "masked" || status.unitFileState === "masked")
|
|
982
|
+
return;
|
|
983
|
+
if (!isShadowingFragment(status.fragmentPath))
|
|
984
|
+
return;
|
|
985
|
+
throw refuseContainer(props.name, `${serviceNameFor(props)} already exists as a plain unit at ${status.fragmentPath}, not a ` + `Quadlet generation — Quadlet's own output (${GENERATED_UNIT_DIRECTORY}/…) would be ` + "shadowed by it and never actually run: systemd loads the higher-precedence file first. " + "Move the plain unit aside — a cutover — before declaring this container.");
|
|
986
|
+
};
|
|
957
987
|
var assertRenameTarget = async (runner, old, name) => {
|
|
958
988
|
assertMayWrite(runner, old.name);
|
|
959
989
|
assertMayWrite(runner, name);
|
|
@@ -991,7 +1021,11 @@ var settle = async (runner, props, configSha256, step) => {
|
|
|
991
1021
|
return;
|
|
992
1022
|
});
|
|
993
1023
|
}
|
|
994
|
-
|
|
1024
|
+
const after2 = await showUnit(runner, step.serviceName).catch(() => {
|
|
1025
|
+
return;
|
|
1026
|
+
});
|
|
1027
|
+
const stateNote = after2 === undefined ? "Its running state was not re-checked." : `Its unit is now ${after2.activeState}.`;
|
|
1028
|
+
throw refuseContainer(props.name, `${detail} ${stateNote}`);
|
|
995
1029
|
};
|
|
996
1030
|
try {
|
|
997
1031
|
if (!wantStarted) {
|
|
@@ -1018,8 +1052,11 @@ var readContainer = async (runner, props) => {
|
|
|
1018
1052
|
const path = containerPathFor(props);
|
|
1019
1053
|
const bytes = await runner.readFile(path);
|
|
1020
1054
|
const status = await showUnit(runner, serviceNameFor(props));
|
|
1021
|
-
if (bytes === undefined
|
|
1022
|
-
|
|
1055
|
+
if (bytes === undefined) {
|
|
1056
|
+
if (!status.known)
|
|
1057
|
+
return;
|
|
1058
|
+
assertUnshadowed(props, status);
|
|
1059
|
+
}
|
|
1023
1060
|
const sha = bytes === undefined ? "" : digestOf(decoder2.decode(bytes));
|
|
1024
1061
|
return attributesOf(props, configDigest(props.restartOn), sha, path, serviceNameFor(props), status);
|
|
1025
1062
|
};
|
|
@@ -2870,6 +2907,11 @@ var assertInstallable2 = async (base, prefix, path, options, groups) => {
|
|
|
2870
2907
|
};
|
|
2871
2908
|
|
|
2872
2909
|
// src/linux/sudo-exec.ts
|
|
2910
|
+
var GENERATOR_DIRECTORIES = [
|
|
2911
|
+
GENERATED_UNIT_DIRECTORY,
|
|
2912
|
+
`${GENERATED_UNIT_DIRECTORY}.late`
|
|
2913
|
+
];
|
|
2914
|
+
var isUnderGeneratorDirectory = (fragmentPath) => GENERATOR_DIRECTORIES.some((dir) => fragmentPath.startsWith(`${dir}/`));
|
|
2873
2915
|
var DIR_EXPECT = {
|
|
2874
2916
|
chmod: "directory",
|
|
2875
2917
|
chown: "directory",
|
|
@@ -2883,17 +2925,18 @@ var unitStatusOf = async (base, unit) => {
|
|
|
2883
2925
|
"show",
|
|
2884
2926
|
"--no-pager",
|
|
2885
2927
|
"-p",
|
|
2886
|
-
"FragmentPath,LoadState",
|
|
2928
|
+
"FragmentPath,LoadState,SourcePath",
|
|
2887
2929
|
"--",
|
|
2888
2930
|
unit
|
|
2889
2931
|
]);
|
|
2890
2932
|
if (result.exitCode !== 0) {
|
|
2891
|
-
throw new SudoRefusedError(`sshSudoRunner ${unit}: could not read FragmentPath/LoadState (systemctl show
|
|
2933
|
+
throw new SudoRefusedError(`sshSudoRunner ${unit}: could not read FragmentPath/LoadState/SourcePath (systemctl show ` + `exit ${String(result.exitCode)}). Nothing ran as root.`);
|
|
2892
2934
|
}
|
|
2893
2935
|
const fields = parseShow(result.stdout);
|
|
2894
2936
|
return {
|
|
2895
2937
|
fragmentPath: fields.get("FragmentPath") ?? "",
|
|
2896
|
-
loadState: fields.get("LoadState") ?? ""
|
|
2938
|
+
loadState: fields.get("LoadState") ?? "",
|
|
2939
|
+
sourcePath: fields.get("SourcePath") ?? ""
|
|
2897
2940
|
};
|
|
2898
2941
|
};
|
|
2899
2942
|
var elevatedExec = (base, elevate, prefixes, argv) => {
|
|
@@ -2914,7 +2957,7 @@ var elevatedExec = (base, elevate, prefixes, argv) => {
|
|
|
2914
2957
|
const verb = canonical[1] ?? "";
|
|
2915
2958
|
const unit = canonical[3] ?? "";
|
|
2916
2959
|
return elevate(canonical, {}, async () => {
|
|
2917
|
-
const { fragmentPath, loadState } = await unitStatusOf(base, unit);
|
|
2960
|
+
const { fragmentPath, loadState, sourcePath } = await unitStatusOf(base, unit);
|
|
2918
2961
|
if (loadState === "masked") {
|
|
2919
2962
|
throw new SudoRefusedError(`sshSudoRunner ${unit}: is masked. Masking is someone's decision, not drift; unmask it ` + "deliberately, then redeploy. Nothing ran as root.");
|
|
2920
2963
|
}
|
|
@@ -2923,9 +2966,13 @@ var elevatedExec = (base, elevate, prefixes, argv) => {
|
|
|
2923
2966
|
return;
|
|
2924
2967
|
throw new SudoRefusedError(`sshSudoRunner ${unit}: has no unit file (FragmentPath empty), and ${verb} is not ` + "stop/disable — refusing rather than run it against whatever systemd currently thinks " + `${unit} is (a kernel-generated unit, say). Nothing ran as root.`);
|
|
2925
2968
|
}
|
|
2926
|
-
if (prefixOf(fragmentPath, prefixes)
|
|
2927
|
-
|
|
2969
|
+
if (prefixOf(fragmentPath, prefixes) !== undefined)
|
|
2970
|
+
return;
|
|
2971
|
+
if (isUnderGeneratorDirectory(fragmentPath) && sourcePath !== "" && prefixOf(sourcePath, prefixes) !== undefined) {
|
|
2972
|
+
return;
|
|
2928
2973
|
}
|
|
2974
|
+
const sourceNote = sourcePath === "" ? "" : `, and its SourcePath ${sourcePath} is not under one either`;
|
|
2975
|
+
throw new SudoRefusedError(`sshSudoRunner ${unit}: FragmentPath ${fragmentPath} is outside every declared prefix ` + `(${prefixes.join(", ")})${sourceNote}; refusing to touch a unit this runner does not ` + "own. Nothing ran as root.");
|
|
2929
2976
|
});
|
|
2930
2977
|
};
|
|
2931
2978
|
|
|
@@ -3132,5 +3179,5 @@ export {
|
|
|
3132
3179
|
sshSudoRunner
|
|
3133
3180
|
};
|
|
3134
3181
|
|
|
3135
|
-
//# debugId=
|
|
3182
|
+
//# debugId=47589007F27006E864756E2164756E21
|
|
3136
3183
|
//# sourceMappingURL=index.js.map
|