paratix 0.4.0 → 0.6.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 +17 -1
- package/dist/{chunk-C45YPXCX.js → chunk-ENWMSERJ.js} +814 -63
- package/dist/chunk-ENWMSERJ.js.map +1 -0
- package/dist/{chunk-EGP3QRLV.js → chunk-IUY5BJHA.js} +69 -16
- package/dist/chunk-IUY5BJHA.js.map +1 -0
- package/dist/{chunk-G3BMCQKU.js → chunk-JJRF37BP.js} +188 -31
- package/dist/chunk-JJRF37BP.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +23 -1319
- package/dist/modules/index.js +6 -2
- package/dist/user-CiAMlpWO.d.ts +1443 -0
- package/llm-guide.md +48 -0
- package/package.json +1 -1
- package/dist/chunk-C45YPXCX.js.map +0 -1
- package/dist/chunk-EGP3QRLV.js.map +0 -1
- package/dist/chunk-G3BMCQKU.js.map +0 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
shellQuote,
|
|
10
10
|
sshdPortMeta,
|
|
11
11
|
validateMode
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JJRF37BP.js";
|
|
13
13
|
|
|
14
14
|
// src/moduleFailure.ts
|
|
15
15
|
function firstNonEmptyLine(text) {
|
|
@@ -676,11 +676,111 @@ function parseContainerStates(stdout) {
|
|
|
676
676
|
function sanitizeUnitValue(value) {
|
|
677
677
|
return value.replaceAll(new RegExp("[\\n\\r]", "gv"), "");
|
|
678
678
|
}
|
|
679
|
-
function
|
|
679
|
+
function validateGeneratedSystemdUnitContent(content, unitFileName) {
|
|
680
|
+
if (content.trim() === "") {
|
|
681
|
+
return failed(`[compose.systemd] generated empty unit content for ${unitFileName}`);
|
|
682
|
+
}
|
|
683
|
+
if (!content.includes("[Unit]") || !content.includes("[Service]")) {
|
|
684
|
+
return failed(`[compose.systemd] generated invalid unit content for ${unitFileName}`);
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
async function verifyNonEmptySystemdUnit(parameters) {
|
|
689
|
+
const writtenContent = await parameters.connection.readFile(parameters.filePath);
|
|
690
|
+
if (writtenContent.trim() === "") return "empty";
|
|
691
|
+
return writtenContent.trim() === parameters.content.trim() ? "matches" : "unexpected";
|
|
692
|
+
}
|
|
693
|
+
async function cleanupComposeSystemdTarget(parameters) {
|
|
694
|
+
await parameters.connection.exec(`rm -f ${shellQuote(parameters.filePath)}`, {
|
|
695
|
+
ignoreExitCode: true,
|
|
696
|
+
silent: true
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function rewriteComposeSystemdUnitViaShell(parameters) {
|
|
700
|
+
const encodedContent = Buffer.from(parameters.content, "utf8").toString("base64");
|
|
701
|
+
await cleanupComposeSystemdTarget(parameters);
|
|
702
|
+
const result = await parameters.connection.exec(
|
|
703
|
+
`printf '%s' ${shellQuote(encodedContent)} | base64 -d > ${shellQuote(parameters.filePath)} && chmod ${shellQuote(SYSTEMD_UNIT_MODE)} ${shellQuote(parameters.filePath)} && chown ${shellQuote("root:root")} ${shellQuote(parameters.filePath)}`,
|
|
704
|
+
EXEC_OPTS2
|
|
705
|
+
);
|
|
706
|
+
if (result.code !== 0) {
|
|
707
|
+
return failedCommand(
|
|
708
|
+
`[compose.systemd] shell fallback write failed for ${parameters.unitFileName}`,
|
|
709
|
+
result
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const fallbackVerification = await verifyNonEmptySystemdUnit(parameters);
|
|
713
|
+
if (fallbackVerification === "matches") return null;
|
|
714
|
+
await cleanupComposeSystemdTarget(parameters);
|
|
715
|
+
if (fallbackVerification === "empty") {
|
|
716
|
+
return failed(
|
|
717
|
+
`[compose.systemd] wrote empty unit file for ${parameters.unitFileName} even after shell fallback`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return failed(
|
|
721
|
+
`[compose.systemd] wrote unexpected unit content for ${parameters.unitFileName} even after shell fallback`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
async function applyComposeSystemdUnit(parameters) {
|
|
725
|
+
await prepareComposeSystemdTarget(parameters);
|
|
726
|
+
try {
|
|
727
|
+
await parameters.connection.writeFile(parameters.filePath, parameters.content, {
|
|
728
|
+
mode: SYSTEMD_UNIT_MODE
|
|
729
|
+
});
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
732
|
+
return failed(`[compose.systemd] atomic write failed for ${parameters.unitFileName}: ${reason}`);
|
|
733
|
+
}
|
|
734
|
+
const writeVerification = await verifyNonEmptySystemdUnit(parameters);
|
|
735
|
+
if (writeVerification !== "matches") {
|
|
736
|
+
const fallbackFailure = await rewriteComposeSystemdUnitViaShell(parameters);
|
|
737
|
+
if (fallbackFailure != null) return fallbackFailure;
|
|
738
|
+
}
|
|
739
|
+
const result = await parameters.connection.exec("systemctl daemon-reload", EXEC_OPTS2);
|
|
740
|
+
return result.code === 0 ? { status: "changed" } : failedCommand(`[compose.systemd] daemon-reload failed for ${parameters.unitFileName}`, result);
|
|
741
|
+
}
|
|
742
|
+
async function checkComposeSystemdUnit(parameters) {
|
|
743
|
+
const runtime = await getRuntime(parameters.ssh, parameters.explicitRuntime);
|
|
744
|
+
if (!runtime) return NEEDS_APPLY;
|
|
745
|
+
const exists = await parameters.ssh.exists(parameters.filePath);
|
|
746
|
+
if (!exists) return NEEDS_APPLY;
|
|
747
|
+
const content = generateSystemdUnit(parameters.projectDirectory, parameters.serviceName, {
|
|
748
|
+
detached: parameters.detached,
|
|
749
|
+
runtime
|
|
750
|
+
});
|
|
751
|
+
const remoteContent = await parameters.ssh.readFile(parameters.filePath);
|
|
752
|
+
return remoteContent.trim() === content.trim() ? "ok" : NEEDS_APPLY;
|
|
753
|
+
}
|
|
754
|
+
function resolveComposeSystemdIdentity(options) {
|
|
755
|
+
const serviceName = options.name ?? `compose-${basename(options.projectDirectory)}`;
|
|
756
|
+
if (!UNIT_NAME_PATTERN.test(serviceName)) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`compose.systemd: name must match ${String(UNIT_NAME_PATTERN)}, got: ${serviceName}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const unitFileName = `${serviceName}.service`;
|
|
762
|
+
return {
|
|
763
|
+
filePath: `/etc/systemd/system/${unitFileName}`,
|
|
764
|
+
serviceName,
|
|
765
|
+
unitFileName
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function prepareComposeSystemdTarget(parameters) {
|
|
769
|
+
await parameters.connection.exec(`systemctl unmask ${shellQuote(parameters.unitFileName)}`, {
|
|
770
|
+
ignoreExitCode: true,
|
|
771
|
+
silent: true
|
|
772
|
+
});
|
|
773
|
+
await parameters.connection.exec(`rm -f ${shellQuote(parameters.filePath)}`, {
|
|
774
|
+
ignoreExitCode: true,
|
|
775
|
+
silent: true
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function generateSystemdUnit(projectDirectory, name, options) {
|
|
680
779
|
const safeName = sanitizeUnitValue(name);
|
|
681
780
|
const safeDirectory = sanitizeUnitValue(projectDirectory);
|
|
682
|
-
const
|
|
683
|
-
|
|
781
|
+
const composeUpCommand = options.detached ? `/usr/bin/env ${options.runtime} compose up -d --remove-orphans` : `/usr/bin/env ${options.runtime} compose up --remove-orphans`;
|
|
782
|
+
const lines = ["[Unit]", `Description=Compose stack: ${safeName}`, "Wants=network-online.target"];
|
|
783
|
+
if (options.runtime === "docker") {
|
|
684
784
|
lines.push("After=network-online.target docker.service");
|
|
685
785
|
lines.push("Requires=docker.service");
|
|
686
786
|
} else {
|
|
@@ -692,8 +792,11 @@ function generateSystemdUnit(projectDirectory, name, runtime) {
|
|
|
692
792
|
"Type=oneshot",
|
|
693
793
|
"RemainAfterExit=yes",
|
|
694
794
|
`WorkingDirectory=${safeDirectory}`,
|
|
695
|
-
`ExecStart
|
|
696
|
-
`ExecStop=/usr/bin/env ${runtime} compose down`,
|
|
795
|
+
`ExecStart=${composeUpCommand}`,
|
|
796
|
+
`ExecStop=/usr/bin/env ${options.runtime} compose down`,
|
|
797
|
+
"TimeoutStartSec=0",
|
|
798
|
+
"StandardOutput=journal",
|
|
799
|
+
"StandardError=journal",
|
|
697
800
|
"",
|
|
698
801
|
"[Install]",
|
|
699
802
|
"WantedBy=multi-user.target",
|
|
@@ -888,6 +991,7 @@ var compose = {
|
|
|
888
991
|
* explicitly provided.
|
|
889
992
|
*
|
|
890
993
|
* @param options - Configuration for the systemd unit.
|
|
994
|
+
* @param options.detached - When true, use `compose up -d`; otherwise start attached.
|
|
891
995
|
* @param options.projectDirectory - The project directory on the remote host.
|
|
892
996
|
* @param options.name - Optional service name (without `.service` suffix).
|
|
893
997
|
* @param options.runtime - Explicit container runtime override.
|
|
@@ -895,14 +999,11 @@ var compose = {
|
|
|
895
999
|
*/
|
|
896
1000
|
systemd(options) {
|
|
897
1001
|
const { projectDirectory, runtime: explicitRuntime } = options;
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
const unitFileName = `${serviceName}.service`;
|
|
905
|
-
const filePath = `/etc/systemd/system/${unitFileName}`;
|
|
1002
|
+
const detached = options.detached ?? false;
|
|
1003
|
+
const { filePath, serviceName, unitFileName } = resolveComposeSystemdIdentity({
|
|
1004
|
+
name: options.name,
|
|
1005
|
+
projectDirectory
|
|
1006
|
+
});
|
|
906
1007
|
return {
|
|
907
1008
|
async apply(ssh2) {
|
|
908
1009
|
const connection = requireComposeSsh(ssh2, "systemd", projectDirectory);
|
|
@@ -914,20 +1015,26 @@ var compose = {
|
|
|
914
1015
|
ssh: connection
|
|
915
1016
|
});
|
|
916
1017
|
if (typeof runtime !== "string") return runtime;
|
|
917
|
-
const content = generateSystemdUnit(projectDirectory, serviceName, runtime);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
return
|
|
1018
|
+
const content = generateSystemdUnit(projectDirectory, serviceName, { detached, runtime });
|
|
1019
|
+
const validationFailure = validateGeneratedSystemdUnitContent(content, unitFileName);
|
|
1020
|
+
if (validationFailure != null) return validationFailure;
|
|
1021
|
+
return applyComposeSystemdUnit({
|
|
1022
|
+
connection,
|
|
1023
|
+
content,
|
|
1024
|
+
filePath,
|
|
1025
|
+
unitFileName
|
|
1026
|
+
});
|
|
921
1027
|
},
|
|
922
1028
|
async check(ssh2) {
|
|
923
1029
|
if (!ssh2) return NEEDS_APPLY;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1030
|
+
return checkComposeSystemdUnit({
|
|
1031
|
+
detached,
|
|
1032
|
+
explicitRuntime,
|
|
1033
|
+
filePath,
|
|
1034
|
+
projectDirectory,
|
|
1035
|
+
serviceName,
|
|
1036
|
+
ssh: ssh2
|
|
1037
|
+
});
|
|
931
1038
|
},
|
|
932
1039
|
name: `compose.systemd: ${unitFileName}`
|
|
933
1040
|
};
|
|
@@ -1755,7 +1862,7 @@ function stat(remotePath) {
|
|
|
1755
1862
|
};
|
|
1756
1863
|
}
|
|
1757
1864
|
|
|
1758
|
-
// src/modules/
|
|
1865
|
+
// src/modules/fileMetadataHelpers.ts
|
|
1759
1866
|
var DEFAULT_FILE_WRITE_MODE2 = "0644";
|
|
1760
1867
|
async function readOwnership(ssh2, remotePath) {
|
|
1761
1868
|
const raw = await ssh2.output(`stat -c '%a %U %G' ${shellQuote(remotePath)}`);
|
|
@@ -1794,9 +1901,39 @@ function ownershipMatches(current, options) {
|
|
|
1794
1901
|
if (expectsGroup && current.group !== expectedGroup) return false;
|
|
1795
1902
|
return true;
|
|
1796
1903
|
}
|
|
1904
|
+
function createMetadataModule(kind, remotePath, value) {
|
|
1905
|
+
const name = `file.${kind}: ${remotePath}`;
|
|
1906
|
+
return {
|
|
1907
|
+
async apply(ssh2) {
|
|
1908
|
+
if (!ssh2) return failed(`[${name}] SSH connection is required`);
|
|
1909
|
+
if (kind === "chmod") validateMode(value);
|
|
1910
|
+
await ssh2.exec(`${kind} ${shellQuote(value)} ${shellQuote(remotePath)}`, { silent: true });
|
|
1911
|
+
return { status: "changed" };
|
|
1912
|
+
},
|
|
1913
|
+
async check(ssh2) {
|
|
1914
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
1915
|
+
if (!await ssh2.exists(remotePath)) return NEEDS_APPLY;
|
|
1916
|
+
const ownership = await readOwnership(ssh2, remotePath);
|
|
1917
|
+
const matches = kind === "chmod" ? { mode: value } : { owner: value };
|
|
1918
|
+
return ownershipMatches(ownership, matches) ? "ok" : NEEDS_APPLY;
|
|
1919
|
+
},
|
|
1920
|
+
name
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/modules/file.ts
|
|
1797
1925
|
function splitLines(content) {
|
|
1798
1926
|
return content.split(new RegExp("\\r?\\n", "v"));
|
|
1799
1927
|
}
|
|
1928
|
+
function findFirstMatchingLineIndex(lines, pattern) {
|
|
1929
|
+
return lines.findIndex((candidateLine) => pattern.test(candidateLine));
|
|
1930
|
+
}
|
|
1931
|
+
function splitLinesPreservingTrailingNewline(content) {
|
|
1932
|
+
const hasTrailingNewline = new RegExp("\\r?\\n$", "v").test(content);
|
|
1933
|
+
const lines = splitLines(content);
|
|
1934
|
+
if (hasTrailingNewline && lines.at(-1) === "") lines.pop();
|
|
1935
|
+
return { hasTrailingNewline, lines };
|
|
1936
|
+
}
|
|
1800
1937
|
function validateAbsentPath(remotePath) {
|
|
1801
1938
|
const trimmedPath = remotePath.trim();
|
|
1802
1939
|
if (trimmedPath.length === 0) {
|
|
@@ -1836,6 +1973,12 @@ var file = {
|
|
|
1836
1973
|
},
|
|
1837
1974
|
assemble,
|
|
1838
1975
|
block,
|
|
1976
|
+
chmod(remotePath, mode) {
|
|
1977
|
+
return createMetadataModule("chmod", remotePath, mode);
|
|
1978
|
+
},
|
|
1979
|
+
chown(remotePath, owner) {
|
|
1980
|
+
return createMetadataModule("chown", remotePath, owner);
|
|
1981
|
+
},
|
|
1839
1982
|
/**
|
|
1840
1983
|
* Upload a local file to the remote host.
|
|
1841
1984
|
* The file is only transferred when the remote SHA-256 differs from the local one.
|
|
@@ -1936,13 +2079,17 @@ var file = {
|
|
|
1936
2079
|
});
|
|
1937
2080
|
} else {
|
|
1938
2081
|
const content = await ssh2.readFile(remotePath);
|
|
2082
|
+
const { hasTrailingNewline, lines } = splitLinesPreservingTrailingNewline(content);
|
|
1939
2083
|
const pattern = new RegExp(options.match, "mu");
|
|
1940
|
-
|
|
2084
|
+
const matchingLineIndex = findFirstMatchingLineIndex(lines, pattern);
|
|
2085
|
+
if (matchingLineIndex === -1) {
|
|
1941
2086
|
return failed(
|
|
1942
2087
|
`[file.line: ${remotePath}] No line matching ${options.match} found for replacement`
|
|
1943
2088
|
);
|
|
1944
2089
|
}
|
|
1945
|
-
|
|
2090
|
+
lines[matchingLineIndex] = line;
|
|
2091
|
+
let newContent = lines.join("\n");
|
|
2092
|
+
if (hasTrailingNewline) newContent += "\n";
|
|
1946
2093
|
const ownership = await readOwnership(ssh2, remotePath);
|
|
1947
2094
|
await guardedWriteFile(ssh2, {
|
|
1948
2095
|
mode: normalizeMode2(ownership.mode),
|
|
@@ -1961,7 +2108,8 @@ var file = {
|
|
|
1961
2108
|
const lines = splitLines(content);
|
|
1962
2109
|
if (options?.match != null) {
|
|
1963
2110
|
const matchPattern = new RegExp(options.match, "mu");
|
|
1964
|
-
const
|
|
2111
|
+
const matchedLineIndex = findFirstMatchingLineIndex(lines, matchPattern);
|
|
2112
|
+
const matchedLine = matchedLineIndex === -1 ? void 0 : lines[matchedLineIndex];
|
|
1965
2113
|
return matchedLine === line ? "ok" : NEEDS_APPLY;
|
|
1966
2114
|
}
|
|
1967
2115
|
return lines.includes(line) ? "ok" : NEEDS_APPLY;
|
|
@@ -3212,6 +3360,309 @@ var pkg = {
|
|
|
3212
3360
|
}
|
|
3213
3361
|
};
|
|
3214
3362
|
|
|
3363
|
+
// src/modules/quadletHelpers.ts
|
|
3364
|
+
var CONTAINERS_SYSTEMD_DIRECTORY = "/etc/containers/systemd";
|
|
3365
|
+
var QUADLET_PULL_CHANGED_OUTPUT_PATTERNS = [
|
|
3366
|
+
"Copying blob",
|
|
3367
|
+
"Copying config",
|
|
3368
|
+
"Downloaded newer image",
|
|
3369
|
+
"Pulling fs layer",
|
|
3370
|
+
"Storing signatures",
|
|
3371
|
+
"Writing manifest"
|
|
3372
|
+
];
|
|
3373
|
+
function sanitizeQuadletValue(value) {
|
|
3374
|
+
return value.replaceAll(new RegExp("[\\n\\r]", "gv"), "");
|
|
3375
|
+
}
|
|
3376
|
+
function renderQuadletLine(key, value) {
|
|
3377
|
+
return `${key}=${sanitizeQuadletValue(value)}`;
|
|
3378
|
+
}
|
|
3379
|
+
function renderQuadletRepeated(key, values) {
|
|
3380
|
+
return values.map((value) => renderQuadletLine(key, value));
|
|
3381
|
+
}
|
|
3382
|
+
function renderQuadletKeyValue(key, record) {
|
|
3383
|
+
return Object.entries(record).sort(([left], [right]) => left.localeCompare(right)).map(([k, v]) => renderQuadletLine(key, `${k}=${v}`));
|
|
3384
|
+
}
|
|
3385
|
+
function maybeRenderQuadletLine(key, value) {
|
|
3386
|
+
if (value == null || value === "") return null;
|
|
3387
|
+
return renderQuadletLine(key, value);
|
|
3388
|
+
}
|
|
3389
|
+
function maybeRenderQuadletBool(key, value) {
|
|
3390
|
+
if (value == null) return null;
|
|
3391
|
+
return renderQuadletLine(key, String(value));
|
|
3392
|
+
}
|
|
3393
|
+
function maybeRenderQuadletNumber(key, value) {
|
|
3394
|
+
if (value == null) return null;
|
|
3395
|
+
return renderQuadletLine(key, String(value));
|
|
3396
|
+
}
|
|
3397
|
+
function compactQuadletLines(lines) {
|
|
3398
|
+
return lines.filter((line) => line != null);
|
|
3399
|
+
}
|
|
3400
|
+
function renderQuadletSection(name, lines) {
|
|
3401
|
+
return [`[${name}]`, ...lines, ""].join("\n");
|
|
3402
|
+
}
|
|
3403
|
+
function buildQuadletIdentityLines(options) {
|
|
3404
|
+
return [
|
|
3405
|
+
renderQuadletLine("Image", options.image),
|
|
3406
|
+
maybeRenderQuadletLine("ContainerName", options.containerName),
|
|
3407
|
+
maybeRenderQuadletLine("AutoUpdate", options.autoUpdate),
|
|
3408
|
+
maybeRenderQuadletLine("Pull", options.pull),
|
|
3409
|
+
maybeRenderQuadletLine("Entrypoint", options.entrypoint?.join(" ")),
|
|
3410
|
+
maybeRenderQuadletLine("Exec", options.exec?.join(" ")),
|
|
3411
|
+
maybeRenderQuadletLine("WorkingDir", options.workingDir),
|
|
3412
|
+
maybeRenderQuadletLine("User", options.user),
|
|
3413
|
+
maybeRenderQuadletLine("UserNS", options.userNs)
|
|
3414
|
+
];
|
|
3415
|
+
}
|
|
3416
|
+
function buildQuadletNetworkLines(options) {
|
|
3417
|
+
return [
|
|
3418
|
+
maybeRenderQuadletLine("HostName", options.hostName),
|
|
3419
|
+
...renderQuadletRepeated("Network", options.networks ?? []),
|
|
3420
|
+
...renderQuadletRepeated("DNS", options.dns ?? []),
|
|
3421
|
+
...renderQuadletRepeated("DNSOption", options.dnsOption ?? []),
|
|
3422
|
+
...renderQuadletRepeated("DNSSearch", options.dnsSearch ?? []),
|
|
3423
|
+
maybeRenderQuadletLine("IP", options.ip),
|
|
3424
|
+
maybeRenderQuadletLine("IP6", options.ip6)
|
|
3425
|
+
];
|
|
3426
|
+
}
|
|
3427
|
+
function buildQuadletSecurityLines(options) {
|
|
3428
|
+
return [
|
|
3429
|
+
...renderQuadletRepeated("AddCapability", options.addCapability ?? []),
|
|
3430
|
+
...renderQuadletRepeated("DropCapability", options.dropCapability ?? []),
|
|
3431
|
+
maybeRenderQuadletBool("SecurityLabelDisable", options.securityLabelDisable),
|
|
3432
|
+
maybeRenderQuadletLine("SecurityLabelType", options.securityLabelType),
|
|
3433
|
+
maybeRenderQuadletLine("SeccompProfile", options.seccompProfile),
|
|
3434
|
+
maybeRenderQuadletBool("NoNewPrivileges", options.noNewPrivileges),
|
|
3435
|
+
maybeRenderQuadletBool("ReadOnly", options.readOnly)
|
|
3436
|
+
];
|
|
3437
|
+
}
|
|
3438
|
+
function buildQuadletRuntimeLines(options) {
|
|
3439
|
+
return [
|
|
3440
|
+
maybeRenderQuadletBool("Notify", options.notify),
|
|
3441
|
+
maybeRenderQuadletBool("RunInit", options.runInit),
|
|
3442
|
+
maybeRenderQuadletLine("LogDriver", options.logDriver),
|
|
3443
|
+
maybeRenderQuadletLine("Timezone", options.timezone),
|
|
3444
|
+
maybeRenderQuadletNumber("StopTimeout", options.stopTimeout)
|
|
3445
|
+
];
|
|
3446
|
+
}
|
|
3447
|
+
function buildQuadletStorageLines(options) {
|
|
3448
|
+
return [
|
|
3449
|
+
...renderQuadletRepeated("PublishPort", options.publishPorts ?? []),
|
|
3450
|
+
...renderQuadletRepeated("ExposeHostPort", options.exposeHostPort ?? []),
|
|
3451
|
+
...renderQuadletRepeated("Volume", options.volumes ?? []),
|
|
3452
|
+
...renderQuadletRepeated("Mount", options.mount ?? []),
|
|
3453
|
+
...renderQuadletRepeated("Tmpfs", options.tmpfs ?? []),
|
|
3454
|
+
...renderQuadletRepeated("AddDevice", options.addDevice ?? [])
|
|
3455
|
+
];
|
|
3456
|
+
}
|
|
3457
|
+
function buildQuadletMetadataLines(options) {
|
|
3458
|
+
return [
|
|
3459
|
+
...renderQuadletRepeated("Secret", options.secret ?? []),
|
|
3460
|
+
...renderQuadletEnvironment(options.environment ?? {}),
|
|
3461
|
+
...renderQuadletRepeated("EnvironmentFile", options.environmentFiles ?? []),
|
|
3462
|
+
...renderQuadletKeyValue("Label", options.label ?? {}),
|
|
3463
|
+
...renderQuadletKeyValue("Annotation", options.annotation ?? {})
|
|
3464
|
+
];
|
|
3465
|
+
}
|
|
3466
|
+
function renderQuadletEnvironment(environment) {
|
|
3467
|
+
return Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => renderQuadletLine("Environment", `${key}=${value}`));
|
|
3468
|
+
}
|
|
3469
|
+
function buildQuadletTuningLines(options) {
|
|
3470
|
+
return [
|
|
3471
|
+
...renderQuadletKeyValue("Sysctl", options.sysctl ?? {}),
|
|
3472
|
+
...renderQuadletRepeated("Ulimit", options.ulimit ?? []),
|
|
3473
|
+
...renderQuadletRepeated("GroupAdd", options.groupAdd ?? []),
|
|
3474
|
+
...renderQuadletRepeated("Mask", options.mask ?? []),
|
|
3475
|
+
...renderQuadletRepeated("Unmask", options.unmask ?? [])
|
|
3476
|
+
];
|
|
3477
|
+
}
|
|
3478
|
+
function buildQuadletHealthcheckLines(options) {
|
|
3479
|
+
if (options.healthCmd == null) return [];
|
|
3480
|
+
return compactQuadletLines([
|
|
3481
|
+
renderQuadletLine("HealthCmd", options.healthCmd),
|
|
3482
|
+
maybeRenderQuadletLine("HealthInterval", options.healthInterval),
|
|
3483
|
+
maybeRenderQuadletLine("HealthTimeout", options.healthTimeout),
|
|
3484
|
+
maybeRenderQuadletNumber("HealthRetries", options.healthRetries),
|
|
3485
|
+
maybeRenderQuadletLine("HealthStartPeriod", options.healthStartPeriod),
|
|
3486
|
+
maybeRenderQuadletLine("HealthOnFailure", options.healthOnFailure)
|
|
3487
|
+
]);
|
|
3488
|
+
}
|
|
3489
|
+
function buildQuadletContainerLines(options) {
|
|
3490
|
+
return compactQuadletLines([
|
|
3491
|
+
...buildQuadletIdentityLines(options),
|
|
3492
|
+
...buildQuadletNetworkLines(options),
|
|
3493
|
+
...buildQuadletSecurityLines(options),
|
|
3494
|
+
...buildQuadletRuntimeLines(options),
|
|
3495
|
+
...renderQuadletRepeated("PodmanArgs", options.podmanArgs ?? []),
|
|
3496
|
+
...buildQuadletStorageLines(options),
|
|
3497
|
+
...buildQuadletMetadataLines(options),
|
|
3498
|
+
...buildQuadletTuningLines(options),
|
|
3499
|
+
...buildQuadletHealthcheckLines(options)
|
|
3500
|
+
]);
|
|
3501
|
+
}
|
|
3502
|
+
function buildQuadletServiceLines(options) {
|
|
3503
|
+
return compactQuadletLines([
|
|
3504
|
+
maybeRenderQuadletLine("Restart", options.restart),
|
|
3505
|
+
maybeRenderQuadletNumber("TimeoutStartSec", options.timeoutStartSec),
|
|
3506
|
+
maybeRenderQuadletNumber("TimeoutStopSec", options.timeoutStopSec)
|
|
3507
|
+
]);
|
|
3508
|
+
}
|
|
3509
|
+
function buildQuadletUnitSection(options) {
|
|
3510
|
+
return renderQuadletSection("Unit", [
|
|
3511
|
+
renderQuadletLine("Description", options.description ?? `Podman container: ${options.name}`),
|
|
3512
|
+
"Wants=network-online.target",
|
|
3513
|
+
"After=network-online.target"
|
|
3514
|
+
]);
|
|
3515
|
+
}
|
|
3516
|
+
function buildQuadletInstallSection(options) {
|
|
3517
|
+
return renderQuadletSection("Install", [
|
|
3518
|
+
renderQuadletLine("WantedBy", options.wantedBy ?? "multi-user.target")
|
|
3519
|
+
]);
|
|
3520
|
+
}
|
|
3521
|
+
function buildQuadletImagePullCommand(options) {
|
|
3522
|
+
const authFileFlag = options.authFile == null ? "" : ` --authfile ${shellQuoteForQuadlet(options.authFile)}`;
|
|
3523
|
+
return `podman pull${authFileFlag} ${shellQuoteForQuadlet(options.image)} 2>&1`;
|
|
3524
|
+
}
|
|
3525
|
+
function getQuadletContainerFilePath(name) {
|
|
3526
|
+
return `${CONTAINERS_SYSTEMD_DIRECTORY}/${name}.container`;
|
|
3527
|
+
}
|
|
3528
|
+
function getQuadletContainerServiceName(options) {
|
|
3529
|
+
return options.serviceName ?? options.name;
|
|
3530
|
+
}
|
|
3531
|
+
function quadletPullOutputIndicatesChange(output) {
|
|
3532
|
+
return QUADLET_PULL_CHANGED_OUTPUT_PATTERNS.some((pattern) => output.includes(pattern));
|
|
3533
|
+
}
|
|
3534
|
+
var UNIT_NAME_PATTERN2 = new RegExp("^[\\w@.\\-]+$", "v");
|
|
3535
|
+
function validateQuadletName(name) {
|
|
3536
|
+
if (!UNIT_NAME_PATTERN2.test(name)) {
|
|
3537
|
+
throw new Error(`quadlet: name must match ${String(UNIT_NAME_PATTERN2)}, got: ${name}`);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function shellQuoteForQuadlet(value) {
|
|
3541
|
+
const escapedQuote = "'\\''";
|
|
3542
|
+
return `'${value.replaceAll("'", escapedQuote)}'`;
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
// src/modules/quadlet.ts
|
|
3546
|
+
var CONTAINERS_SYSTEMD_DIRECTORY_COMMAND = "mkdir -p '/etc/containers/systemd'";
|
|
3547
|
+
var QUADLET_FILE_MODE = "0644";
|
|
3548
|
+
var SYSTEMCTL = "systemctl";
|
|
3549
|
+
function generateContainerQuadlet(options) {
|
|
3550
|
+
const serviceLines = buildQuadletServiceLines(options);
|
|
3551
|
+
const sections = [
|
|
3552
|
+
buildQuadletUnitSection(options),
|
|
3553
|
+
renderQuadletSection("Container", buildQuadletContainerLines(options)),
|
|
3554
|
+
...serviceLines.length > 0 ? [renderQuadletSection("Service", serviceLines)] : [],
|
|
3555
|
+
buildQuadletInstallSection(options)
|
|
3556
|
+
];
|
|
3557
|
+
return sections.join("\n").trimEnd();
|
|
3558
|
+
}
|
|
3559
|
+
async function createQuadletDirectory(ssh2) {
|
|
3560
|
+
return ssh2.exec(CONTAINERS_SYSTEMD_DIRECTORY_COMMAND, {
|
|
3561
|
+
ignoreExitCode: true,
|
|
3562
|
+
silent: true
|
|
3563
|
+
});
|
|
3564
|
+
}
|
|
3565
|
+
async function applyQuadletFile(parameters) {
|
|
3566
|
+
const mkdirResult = await createQuadletDirectory(parameters.ssh);
|
|
3567
|
+
if (mkdirResult.code !== 0) {
|
|
3568
|
+
return failedCommand(
|
|
3569
|
+
`[quadlet.container: ${parameters.name}] failed to create quadlet directory`,
|
|
3570
|
+
mkdirResult
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
await parameters.ssh.writeFile(parameters.filePath, parameters.content, {
|
|
3574
|
+
mode: QUADLET_FILE_MODE
|
|
3575
|
+
});
|
|
3576
|
+
const daemonReload = await parameters.ssh.exec(`${SYSTEMCTL} daemon-reload`, {
|
|
3577
|
+
ignoreExitCode: true,
|
|
3578
|
+
silent: true
|
|
3579
|
+
});
|
|
3580
|
+
return daemonReload.code === 0 ? { status: "changed" } : failedCommand(
|
|
3581
|
+
`[quadlet.container: ${parameters.name}] systemctl daemon-reload failed`,
|
|
3582
|
+
daemonReload
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
async function checkQuadletFile(parameters) {
|
|
3586
|
+
const exists = await parameters.ssh.exists(parameters.filePath);
|
|
3587
|
+
if (!exists) return NEEDS_APPLY;
|
|
3588
|
+
const remoteContent = await parameters.ssh.readFile(parameters.filePath);
|
|
3589
|
+
return remoteContent.trim() === parameters.content.trim() ? "ok" : NEEDS_APPLY;
|
|
3590
|
+
}
|
|
3591
|
+
var quadlet = {
|
|
3592
|
+
/**
|
|
3593
|
+
* Write a Podman Quadlet `.container` definition and reload systemd when it changes.
|
|
3594
|
+
*
|
|
3595
|
+
* The resulting generated service can be controlled with `service.enabled(name)`
|
|
3596
|
+
* and `service.running(name)`.
|
|
3597
|
+
*
|
|
3598
|
+
* @param options - Configuration for the Quadlet container definition.
|
|
3599
|
+
* @returns A Module that ensures the Quadlet file is present and up to date.
|
|
3600
|
+
*/
|
|
3601
|
+
container(options) {
|
|
3602
|
+
validateQuadletName(options.name);
|
|
3603
|
+
const filePath = getQuadletContainerFilePath(options.name);
|
|
3604
|
+
const content = generateContainerQuadlet(options);
|
|
3605
|
+
return {
|
|
3606
|
+
async apply(ssh2) {
|
|
3607
|
+
if (!ssh2) return failed(`[quadlet.container: ${options.name}] SSH connection is required`);
|
|
3608
|
+
return applyQuadletFile({ content, filePath, name: options.name, ssh: ssh2 });
|
|
3609
|
+
},
|
|
3610
|
+
async check(ssh2) {
|
|
3611
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
3612
|
+
return checkQuadletFile({ content, filePath, ssh: ssh2 });
|
|
3613
|
+
},
|
|
3614
|
+
name: `quadlet.container: ${options.name}`
|
|
3615
|
+
};
|
|
3616
|
+
},
|
|
3617
|
+
/**
|
|
3618
|
+
* Pull the latest image for a Quadlet-managed container and restart the service
|
|
3619
|
+
* only when the image changed.
|
|
3620
|
+
*
|
|
3621
|
+
* Accepts the same `name` and `image` fields as `quadlet.container(...)`, so a
|
|
3622
|
+
* shared config object can drive both deployment and targeted image refreshes.
|
|
3623
|
+
*
|
|
3624
|
+
* @param options - Image pull and restart configuration for the Quadlet service.
|
|
3625
|
+
* @returns A Module that updates the image and conditionally restarts the service.
|
|
3626
|
+
*/
|
|
3627
|
+
updateImage(options) {
|
|
3628
|
+
validateQuadletName(options.name);
|
|
3629
|
+
if (options.serviceName != null) validateQuadletName(options.serviceName);
|
|
3630
|
+
const pullCommand = buildQuadletImagePullCommand(options);
|
|
3631
|
+
const serviceName = getQuadletContainerServiceName(options);
|
|
3632
|
+
return {
|
|
3633
|
+
async apply(ssh2) {
|
|
3634
|
+
if (!ssh2) return failed(`[quadlet.updateImage: ${options.name}] SSH connection is required`);
|
|
3635
|
+
const pullResult = await ssh2.exec(pullCommand, {
|
|
3636
|
+
ignoreExitCode: true,
|
|
3637
|
+
silent: true
|
|
3638
|
+
});
|
|
3639
|
+
if (pullResult.code !== 0) {
|
|
3640
|
+
return failedCommand(
|
|
3641
|
+
`[quadlet.updateImage: ${options.name}] podman pull failed`,
|
|
3642
|
+
pullResult
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
if (!quadletPullOutputIndicatesChange(pullResult.stdout)) {
|
|
3646
|
+
return { status: "ok" };
|
|
3647
|
+
}
|
|
3648
|
+
const restartResult = await ssh2.exec(`${SYSTEMCTL} restart ${shellQuote(serviceName)}`, {
|
|
3649
|
+
ignoreExitCode: true,
|
|
3650
|
+
silent: true
|
|
3651
|
+
});
|
|
3652
|
+
return restartResult.code === 0 ? { status: "changed" } : failedCommand(
|
|
3653
|
+
`[quadlet.updateImage: ${options.name}] systemctl restart failed`,
|
|
3654
|
+
restartResult
|
|
3655
|
+
);
|
|
3656
|
+
},
|
|
3657
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- Signal-style module
|
|
3658
|
+
async check() {
|
|
3659
|
+
return NEEDS_APPLY;
|
|
3660
|
+
},
|
|
3661
|
+
name: `quadlet.updateImage: ${options.name}`
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3215
3666
|
// src/modules/releaseUpgrade.ts
|
|
3216
3667
|
var NONINTERACTIVE2 = "DEBIAN_FRONTEND=noninteractive";
|
|
3217
3668
|
var CODENAME_RE = new RegExp("^[a-z]{3,20}$", "v");
|
|
@@ -3666,7 +4117,7 @@ var script = {
|
|
|
3666
4117
|
};
|
|
3667
4118
|
|
|
3668
4119
|
// src/modules/service.ts
|
|
3669
|
-
var
|
|
4120
|
+
var SYSTEMCTL2 = "systemctl";
|
|
3670
4121
|
var service = {
|
|
3671
4122
|
/**
|
|
3672
4123
|
* Ensure a systemd service is disabled and will not start on boot.
|
|
@@ -3677,7 +4128,7 @@ var service = {
|
|
|
3677
4128
|
return {
|
|
3678
4129
|
async apply(ssh2) {
|
|
3679
4130
|
if (!ssh2) return failed(`[service.disabled: ${name}] SSH connection is required`);
|
|
3680
|
-
const result = await ssh2.exec(`${
|
|
4131
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} disable ${shellQuote(name)}`, {
|
|
3681
4132
|
ignoreExitCode: true,
|
|
3682
4133
|
silent: true
|
|
3683
4134
|
});
|
|
@@ -3685,7 +4136,7 @@ var service = {
|
|
|
3685
4136
|
},
|
|
3686
4137
|
async check(ssh2) {
|
|
3687
4138
|
if (!ssh2) return NEEDS_APPLY;
|
|
3688
|
-
const enabled = await ssh2.test(`${
|
|
4139
|
+
const enabled = await ssh2.test(`${SYSTEMCTL2} is-enabled --quiet ${shellQuote(name)}`);
|
|
3689
4140
|
return enabled ? "needs-apply" : "ok";
|
|
3690
4141
|
},
|
|
3691
4142
|
name: `service.disabled: ${name}`
|
|
@@ -3700,7 +4151,7 @@ var service = {
|
|
|
3700
4151
|
return {
|
|
3701
4152
|
async apply(ssh2) {
|
|
3702
4153
|
if (!ssh2) return failed(`[service.enabled: ${name}] SSH connection is required`);
|
|
3703
|
-
const result = await ssh2.exec(`${
|
|
4154
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} enable ${shellQuote(name)}`, {
|
|
3704
4155
|
ignoreExitCode: true,
|
|
3705
4156
|
silent: true
|
|
3706
4157
|
});
|
|
@@ -3708,7 +4159,7 @@ var service = {
|
|
|
3708
4159
|
},
|
|
3709
4160
|
async check(ssh2) {
|
|
3710
4161
|
if (!ssh2) return NEEDS_APPLY;
|
|
3711
|
-
return await ssh2.test(`${
|
|
4162
|
+
return await ssh2.test(`${SYSTEMCTL2} is-enabled --quiet ${shellQuote(name)}`) ? "ok" : NEEDS_APPLY;
|
|
3712
4163
|
},
|
|
3713
4164
|
name: `service.enabled: ${name}`
|
|
3714
4165
|
};
|
|
@@ -3724,7 +4175,7 @@ var service = {
|
|
|
3724
4175
|
async apply(ssh2) {
|
|
3725
4176
|
if (!ssh2) return failed("[service.facts] SSH connection is required");
|
|
3726
4177
|
const result = await ssh2.exec(
|
|
3727
|
-
`${
|
|
4178
|
+
`${SYSTEMCTL2} list-units --type=service --all --no-pager --no-legend`,
|
|
3728
4179
|
{ ignoreExitCode: true, silent: true }
|
|
3729
4180
|
);
|
|
3730
4181
|
if (result.code !== 0) {
|
|
@@ -3759,7 +4210,7 @@ var service = {
|
|
|
3759
4210
|
return {
|
|
3760
4211
|
async apply(ssh2) {
|
|
3761
4212
|
if (!ssh2) return failed(`[service.reload: ${name}] SSH connection is required`);
|
|
3762
|
-
const result = await ssh2.exec(`${
|
|
4213
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} reload ${shellQuote(name)}`, {
|
|
3763
4214
|
ignoreExitCode: true,
|
|
3764
4215
|
silent: true
|
|
3765
4216
|
});
|
|
@@ -3781,7 +4232,7 @@ var service = {
|
|
|
3781
4232
|
return {
|
|
3782
4233
|
async apply(ssh2) {
|
|
3783
4234
|
if (!ssh2) return failed(`[service.restart: ${name}] SSH connection is required`);
|
|
3784
|
-
const result = await ssh2.exec(`${
|
|
4235
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} restart ${shellQuote(name)}`, {
|
|
3785
4236
|
ignoreExitCode: true,
|
|
3786
4237
|
silent: true
|
|
3787
4238
|
});
|
|
@@ -3803,7 +4254,7 @@ var service = {
|
|
|
3803
4254
|
return {
|
|
3804
4255
|
async apply(ssh2) {
|
|
3805
4256
|
if (!ssh2) return failed(`[service.running: ${name}] SSH connection is required`);
|
|
3806
|
-
const result = await ssh2.exec(`${
|
|
4257
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} start ${shellQuote(name)}`, {
|
|
3807
4258
|
ignoreExitCode: true,
|
|
3808
4259
|
silent: true
|
|
3809
4260
|
});
|
|
@@ -3811,7 +4262,7 @@ var service = {
|
|
|
3811
4262
|
},
|
|
3812
4263
|
async check(ssh2) {
|
|
3813
4264
|
if (!ssh2) return NEEDS_APPLY;
|
|
3814
|
-
return await ssh2.test(`${
|
|
4265
|
+
return await ssh2.test(`${SYSTEMCTL2} is-active --quiet ${shellQuote(name)}`) ? "ok" : NEEDS_APPLY;
|
|
3815
4266
|
},
|
|
3816
4267
|
name: `service.running: ${name}`
|
|
3817
4268
|
};
|
|
@@ -3825,7 +4276,7 @@ var service = {
|
|
|
3825
4276
|
return {
|
|
3826
4277
|
async apply(ssh2) {
|
|
3827
4278
|
if (!ssh2) return failed(`[service.stopped: ${name}] SSH connection is required`);
|
|
3828
|
-
const result = await ssh2.exec(`${
|
|
4279
|
+
const result = await ssh2.exec(`${SYSTEMCTL2} stop ${shellQuote(name)}`, {
|
|
3829
4280
|
ignoreExitCode: true,
|
|
3830
4281
|
silent: true
|
|
3831
4282
|
});
|
|
@@ -3833,7 +4284,7 @@ var service = {
|
|
|
3833
4284
|
},
|
|
3834
4285
|
async check(ssh2) {
|
|
3835
4286
|
if (!ssh2) return NEEDS_APPLY;
|
|
3836
|
-
const active = await ssh2.test(`${
|
|
4287
|
+
const active = await ssh2.test(`${SYSTEMCTL2} is-active --quiet ${shellQuote(name)}`);
|
|
3837
4288
|
return active ? "needs-apply" : "ok";
|
|
3838
4289
|
},
|
|
3839
4290
|
name: `service.stopped: ${name}`
|
|
@@ -4124,7 +4575,7 @@ var DEFAULT_SSH_PORT3 = 22;
|
|
|
4124
4575
|
var PRIVILEGE_SEPARATION_DIRECTORY = "/run/sshd";
|
|
4125
4576
|
var SSHD_CONFIG_PATH = "/etc/ssh/sshd_config";
|
|
4126
4577
|
var SSHD_CONFIG_MODE = "0644";
|
|
4127
|
-
var
|
|
4578
|
+
var SYSTEMCTL3 = "systemctl";
|
|
4128
4579
|
var REGEXP_SPECIAL = /* @__PURE__ */ new Set(["?", ".", "(", ")", "[", "]", "{", "}", "*", "\\", "^", "+", "|", "$"]);
|
|
4129
4580
|
function escapeRegExp(s) {
|
|
4130
4581
|
let result = "";
|
|
@@ -4162,14 +4613,14 @@ async function disableSocketActivatedSsh(ssh2) {
|
|
|
4162
4613
|
});
|
|
4163
4614
|
}
|
|
4164
4615
|
async function resolveSshServiceUnit(ssh2) {
|
|
4165
|
-
const sshdExists = await ssh2.exec(`${
|
|
4616
|
+
const sshdExists = await ssh2.exec(`${SYSTEMCTL3} cat sshd.service >/dev/null 2>&1`, {
|
|
4166
4617
|
ignoreExitCode: true,
|
|
4167
4618
|
silent: true
|
|
4168
4619
|
});
|
|
4169
4620
|
if (sshdExists.code === 0) {
|
|
4170
4621
|
return "sshd";
|
|
4171
4622
|
}
|
|
4172
|
-
const sshExists = await ssh2.exec(`${
|
|
4623
|
+
const sshExists = await ssh2.exec(`${SYSTEMCTL3} cat ssh.service >/dev/null 2>&1`, {
|
|
4173
4624
|
ignoreExitCode: true,
|
|
4174
4625
|
silent: true
|
|
4175
4626
|
});
|
|
@@ -4182,7 +4633,7 @@ async function resolveSshServiceUnit(ssh2) {
|
|
|
4182
4633
|
}
|
|
4183
4634
|
async function reloadSshd(ssh2) {
|
|
4184
4635
|
const serviceUnit = await resolveSshServiceUnit(ssh2);
|
|
4185
|
-
const result = await ssh2.exec(`${
|
|
4636
|
+
const result = await ssh2.exec(`${SYSTEMCTL3} reload ${serviceUnit}`, {
|
|
4186
4637
|
ignoreExitCode: true,
|
|
4187
4638
|
silent: true
|
|
4188
4639
|
});
|
|
@@ -4263,7 +4714,7 @@ async function applySshdPort(ssh2, targetPort) {
|
|
|
4263
4714
|
try {
|
|
4264
4715
|
await disableSocketActivatedSsh(ssh2);
|
|
4265
4716
|
const serviceUnit = await resolveSshServiceUnit(ssh2);
|
|
4266
|
-
await ssh2.exec(`${
|
|
4717
|
+
await ssh2.exec(`${SYSTEMCTL3} restart ${serviceUnit}`, { silent: true });
|
|
4267
4718
|
} catch (error) {
|
|
4268
4719
|
if (!isRestartDisconnect(error)) {
|
|
4269
4720
|
ssh2.removePort(targetPort);
|
|
@@ -4374,8 +4825,259 @@ var sshd = {
|
|
|
4374
4825
|
}
|
|
4375
4826
|
};
|
|
4376
4827
|
|
|
4377
|
-
// src/modules/
|
|
4828
|
+
// src/modules/swapFileHelpers.ts
|
|
4829
|
+
import { dirname } from "path";
|
|
4378
4830
|
var EXEC_OPTS8 = { ignoreExitCode: true, silent: true };
|
|
4831
|
+
var FSTAB_PATH2 = "/etc/fstab";
|
|
4832
|
+
var FSTAB_MODE2 = "0644";
|
|
4833
|
+
var KIBI = 1024;
|
|
4834
|
+
var POWER_0 = 0;
|
|
4835
|
+
var POWER_1 = 1;
|
|
4836
|
+
var POWER_2 = 2;
|
|
4837
|
+
var POWER_3 = 3;
|
|
4838
|
+
var POWER_4 = 4;
|
|
4839
|
+
var POWER_5 = 5;
|
|
4840
|
+
var SIZE_POWERS = {
|
|
4841
|
+
"": POWER_0,
|
|
4842
|
+
G: POWER_3,
|
|
4843
|
+
K: POWER_1,
|
|
4844
|
+
M: POWER_2,
|
|
4845
|
+
P: POWER_5,
|
|
4846
|
+
T: POWER_4
|
|
4847
|
+
};
|
|
4848
|
+
function isSizeUnit(unit) {
|
|
4849
|
+
return Object.hasOwn(SIZE_POWERS, unit);
|
|
4850
|
+
}
|
|
4851
|
+
function normalizeSizeToBytes(size) {
|
|
4852
|
+
if (typeof size === "number") {
|
|
4853
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
4854
|
+
throw new Error("swap.file: numeric size must be a positive integer byte count");
|
|
4855
|
+
}
|
|
4856
|
+
return size;
|
|
4857
|
+
}
|
|
4858
|
+
const trimmed = size.trim();
|
|
4859
|
+
const match = new RegExp("^(?<value>\\d+)(?<unit>[KMGTP]?)$", "iv").exec(trimmed);
|
|
4860
|
+
if (match?.groups == null) {
|
|
4861
|
+
throw new Error(`swap.file: unsupported size format "${size}"`);
|
|
4862
|
+
}
|
|
4863
|
+
const value = Number.parseInt(match.groups.value, 10);
|
|
4864
|
+
const unit = match.groups.unit.toUpperCase();
|
|
4865
|
+
if (!isSizeUnit(unit)) {
|
|
4866
|
+
throw new Error(`swap.file: unsupported size unit "${unit}"`);
|
|
4867
|
+
}
|
|
4868
|
+
return value * KIBI ** SIZE_POWERS[unit];
|
|
4869
|
+
}
|
|
4870
|
+
function normalizeSizeForCommand(size) {
|
|
4871
|
+
return typeof size === "number" ? String(size) : size.trim();
|
|
4872
|
+
}
|
|
4873
|
+
function buildSwapFstabLine(path, priority) {
|
|
4874
|
+
const options = priority == null ? "sw" : `sw,pri=${String(priority)}`;
|
|
4875
|
+
return `${path} none swap ${options} 0 0`;
|
|
4876
|
+
}
|
|
4877
|
+
function findFstabEntry2(fstabContent, path) {
|
|
4878
|
+
for (const line of fstabContent.split("\n")) {
|
|
4879
|
+
const trimmed = line.trim();
|
|
4880
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
4881
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4882
|
+
if (fields[0] === path) return trimmed;
|
|
4883
|
+
}
|
|
4884
|
+
return null;
|
|
4885
|
+
}
|
|
4886
|
+
function upsertFstabEntry2(fstabContent, path, newLine) {
|
|
4887
|
+
const lines = fstabContent.split("\n");
|
|
4888
|
+
const index = lines.findIndex((line) => {
|
|
4889
|
+
const trimmed = line.trim();
|
|
4890
|
+
if (trimmed === "" || trimmed.startsWith("#")) return false;
|
|
4891
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4892
|
+
return fields[0] === path;
|
|
4893
|
+
});
|
|
4894
|
+
if (index === -1) {
|
|
4895
|
+
while (lines.length > 0 && lines.at(-1)?.trim() === "") {
|
|
4896
|
+
lines.pop();
|
|
4897
|
+
}
|
|
4898
|
+
lines.push(newLine);
|
|
4899
|
+
} else {
|
|
4900
|
+
lines[index] = newLine;
|
|
4901
|
+
}
|
|
4902
|
+
return `${lines.join("\n")}
|
|
4903
|
+
`;
|
|
4904
|
+
}
|
|
4905
|
+
function removeFstabEntry2(fstabContent, path) {
|
|
4906
|
+
const lines = fstabContent.split("\n");
|
|
4907
|
+
const result = lines.filter((line) => {
|
|
4908
|
+
const trimmed = line.trim();
|
|
4909
|
+
if (trimmed === "" || trimmed.startsWith("#")) return true;
|
|
4910
|
+
const fields = trimmed.split(new RegExp("\\s+", "v"));
|
|
4911
|
+
return fields[0] !== path;
|
|
4912
|
+
});
|
|
4913
|
+
return `${result.join("\n")}
|
|
4914
|
+
`;
|
|
4915
|
+
}
|
|
4916
|
+
async function isSwapActive(ssh2, path) {
|
|
4917
|
+
const activeSwaps = await ssh2.lines("swapon --show=NAME --noheadings");
|
|
4918
|
+
return activeSwaps.some((line) => line.trim() === path);
|
|
4919
|
+
}
|
|
4920
|
+
async function hasSwapSignature(ssh2, path) {
|
|
4921
|
+
return ssh2.test(`swaplabel ${shellQuote(path)} >/dev/null 2>&1`);
|
|
4922
|
+
}
|
|
4923
|
+
async function readFileSizeInBytes(ssh2, path) {
|
|
4924
|
+
const output = await ssh2.output(`stat -c %s ${shellQuote(path)}`);
|
|
4925
|
+
return Number.parseInt(output.trim(), 10);
|
|
4926
|
+
}
|
|
4927
|
+
async function ensureSwapFstabState(parameters) {
|
|
4928
|
+
const fstabContent = await parameters.ssh.readFile(FSTAB_PATH2);
|
|
4929
|
+
const currentEntry = findFstabEntry2(fstabContent, parameters.path);
|
|
4930
|
+
if (parameters.desiredLine == null) {
|
|
4931
|
+
if (currentEntry == null) return false;
|
|
4932
|
+
const removedContent = removeFstabEntry2(fstabContent, parameters.path);
|
|
4933
|
+
await guardedWriteFile(parameters.ssh, {
|
|
4934
|
+
mode: FSTAB_MODE2,
|
|
4935
|
+
newContent: removedContent,
|
|
4936
|
+
originalContent: fstabContent,
|
|
4937
|
+
remotePath: FSTAB_PATH2
|
|
4938
|
+
});
|
|
4939
|
+
return true;
|
|
4940
|
+
}
|
|
4941
|
+
if (currentEntry === parameters.desiredLine) return false;
|
|
4942
|
+
const updatedContent = upsertFstabEntry2(fstabContent, parameters.path, parameters.desiredLine);
|
|
4943
|
+
await guardedWriteFile(parameters.ssh, {
|
|
4944
|
+
mode: FSTAB_MODE2,
|
|
4945
|
+
newContent: updatedContent,
|
|
4946
|
+
originalContent: fstabContent,
|
|
4947
|
+
remotePath: FSTAB_PATH2
|
|
4948
|
+
});
|
|
4949
|
+
return true;
|
|
4950
|
+
}
|
|
4951
|
+
async function ensureSwapFilePresent(parameters) {
|
|
4952
|
+
const createDirectoryResult = await parameters.ssh.exec(
|
|
4953
|
+
`mkdir -p ${shellQuote(dirname(parameters.path))}`,
|
|
4954
|
+
EXEC_OPTS8
|
|
4955
|
+
);
|
|
4956
|
+
if (createDirectoryResult.code !== 0) {
|
|
4957
|
+
return failedCommand(`[swap.file: ${parameters.path}] mkdir failed`, createDirectoryResult);
|
|
4958
|
+
}
|
|
4959
|
+
const createFileResult = await parameters.ssh.exec(
|
|
4960
|
+
`fallocate -l ${shellQuote(parameters.size)} ${shellQuote(parameters.path)} || dd if=/dev/zero of=${shellQuote(parameters.path)} bs=${shellQuote(parameters.size)} count=1 status=none`,
|
|
4961
|
+
EXEC_OPTS8
|
|
4962
|
+
);
|
|
4963
|
+
if (createFileResult.code !== 0) {
|
|
4964
|
+
return failedCommand(
|
|
4965
|
+
`[swap.file: ${parameters.path}] swap file creation failed`,
|
|
4966
|
+
createFileResult
|
|
4967
|
+
);
|
|
4968
|
+
}
|
|
4969
|
+
const chmodResult = await parameters.ssh.exec(
|
|
4970
|
+
`chmod ${shellQuote(parameters.mode)} ${shellQuote(parameters.path)}`,
|
|
4971
|
+
EXEC_OPTS8
|
|
4972
|
+
);
|
|
4973
|
+
if (chmodResult.code !== 0) {
|
|
4974
|
+
return failedCommand(`[swap.file: ${parameters.path}] chmod failed`, chmodResult);
|
|
4975
|
+
}
|
|
4976
|
+
const makeSwapResult = await parameters.ssh.exec(
|
|
4977
|
+
`mkswap ${shellQuote(parameters.path)}`,
|
|
4978
|
+
EXEC_OPTS8
|
|
4979
|
+
);
|
|
4980
|
+
return makeSwapResult.code === 0 ? true : failedCommand(`[swap.file: ${parameters.path}] mkswap failed`, makeSwapResult);
|
|
4981
|
+
}
|
|
4982
|
+
function normalizeSwapFileOptions(options) {
|
|
4983
|
+
const state = options.state ?? "present";
|
|
4984
|
+
return {
|
|
4985
|
+
expectedFstabLine: state === "present" ? buildSwapFstabLine(options.path, options.priority) : null,
|
|
4986
|
+
mode: options.mode ?? "0600",
|
|
4987
|
+
path: options.path,
|
|
4988
|
+
sizeBytes: normalizeSizeToBytes(options.size),
|
|
4989
|
+
sizeForCommand: normalizeSizeForCommand(options.size),
|
|
4990
|
+
state
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
async function needsSwapRecreation(ssh2, options) {
|
|
4994
|
+
if (!await ssh2.exists(options.path)) return true;
|
|
4995
|
+
if (await readFileSizeInBytes(ssh2, options.path) !== options.sizeBytes) return true;
|
|
4996
|
+
return !await hasSwapSignature(ssh2, options.path);
|
|
4997
|
+
}
|
|
4998
|
+
async function hasSwapFstabEntry(ssh2, options) {
|
|
4999
|
+
const currentFstabContent = await ssh2.readFile(FSTAB_PATH2);
|
|
5000
|
+
return findFstabEntry2(currentFstabContent, options.path) === options.expectedFstabLine;
|
|
5001
|
+
}
|
|
5002
|
+
async function hasNoSwapFstabEntry(ssh2, path) {
|
|
5003
|
+
const currentFstabContent = await ssh2.readFile(FSTAB_PATH2);
|
|
5004
|
+
return findFstabEntry2(currentFstabContent, path) == null;
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
// src/modules/swapHelpers.ts
|
|
5008
|
+
var EXEC_OPTS9 = { ignoreExitCode: true, silent: true };
|
|
5009
|
+
async function disableSwap(ssh2, path) {
|
|
5010
|
+
if (!await isSwapActive(ssh2, path)) return false;
|
|
5011
|
+
const result = await ssh2.exec(`swapoff ${shellQuote(path)}`, EXEC_OPTS9);
|
|
5012
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] swapoff failed`, result);
|
|
5013
|
+
}
|
|
5014
|
+
async function removeSwapFile(ssh2, path) {
|
|
5015
|
+
if (!await ssh2.exists(path)) return false;
|
|
5016
|
+
const result = await ssh2.exec(`rm -f ${shellQuote(path)}`, EXEC_OPTS9);
|
|
5017
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] rm failed`, result);
|
|
5018
|
+
}
|
|
5019
|
+
async function recreateSwapFile(ssh2, options) {
|
|
5020
|
+
if (!await needsSwapRecreation(ssh2, options)) return "ok";
|
|
5021
|
+
const disableResult = await disableSwap(ssh2, options.path);
|
|
5022
|
+
if (typeof disableResult !== "boolean") return disableResult;
|
|
5023
|
+
const removeResult = await removeSwapFile(ssh2, options.path);
|
|
5024
|
+
if (typeof removeResult !== "boolean") return removeResult;
|
|
5025
|
+
const createResult = await ensureSwapFilePresent({
|
|
5026
|
+
mode: options.mode,
|
|
5027
|
+
path: options.path,
|
|
5028
|
+
size: options.sizeForCommand,
|
|
5029
|
+
ssh: ssh2
|
|
5030
|
+
});
|
|
5031
|
+
return createResult === true ? "changed" : createResult;
|
|
5032
|
+
}
|
|
5033
|
+
async function enableSwap(ssh2, path) {
|
|
5034
|
+
if (await isSwapActive(ssh2, path)) return false;
|
|
5035
|
+
const result = await ssh2.exec(`swapon ${shellQuote(path)}`, EXEC_OPTS9);
|
|
5036
|
+
return result.code === 0 ? true : failedCommand(`[swap.file: ${path}] swapon failed`, result);
|
|
5037
|
+
}
|
|
5038
|
+
async function applyAbsentSwapFile(ssh2, options) {
|
|
5039
|
+
let swapChanged = false;
|
|
5040
|
+
const disableResult = await disableSwap(ssh2, options.path);
|
|
5041
|
+
if (typeof disableResult !== "boolean") return disableResult;
|
|
5042
|
+
if (disableResult) swapChanged = true;
|
|
5043
|
+
if (await ensureSwapFstabState({ desiredLine: null, path: options.path, ssh: ssh2 })) swapChanged = true;
|
|
5044
|
+
const removeResult = await removeSwapFile(ssh2, options.path);
|
|
5045
|
+
if (typeof removeResult !== "boolean") return removeResult;
|
|
5046
|
+
if (removeResult) swapChanged = true;
|
|
5047
|
+
return { status: swapChanged ? "changed" : "ok" };
|
|
5048
|
+
}
|
|
5049
|
+
async function applyPresentSwapFile(ssh2, options) {
|
|
5050
|
+
let swapChanged = false;
|
|
5051
|
+
const recreateResult = await recreateSwapFile(ssh2, options);
|
|
5052
|
+
if (typeof recreateResult !== "string") return recreateResult;
|
|
5053
|
+
if (recreateResult === "changed") swapChanged = true;
|
|
5054
|
+
const enableResult = await enableSwap(ssh2, options.path);
|
|
5055
|
+
if (typeof enableResult !== "boolean") return enableResult;
|
|
5056
|
+
if (enableResult) swapChanged = true;
|
|
5057
|
+
if (await ensureSwapFstabState({ desiredLine: options.expectedFstabLine, path: options.path, ssh: ssh2 })) {
|
|
5058
|
+
swapChanged = true;
|
|
5059
|
+
}
|
|
5060
|
+
return { status: swapChanged ? "changed" : "ok" };
|
|
5061
|
+
}
|
|
5062
|
+
async function applySwapFile(ssh2, options) {
|
|
5063
|
+
return options.state === "absent" ? applyAbsentSwapFile(ssh2, options) : applyPresentSwapFile(ssh2, options);
|
|
5064
|
+
}
|
|
5065
|
+
async function checkAbsent(ssh2, options) {
|
|
5066
|
+
if (await ssh2.exists(options.path)) return NEEDS_APPLY;
|
|
5067
|
+
if (await isSwapActive(ssh2, options.path)) return NEEDS_APPLY;
|
|
5068
|
+
return await hasNoSwapFstabEntry(ssh2, options.path) ? "ok" : NEEDS_APPLY;
|
|
5069
|
+
}
|
|
5070
|
+
async function checkPresent(ssh2, options) {
|
|
5071
|
+
if (await needsSwapRecreation(ssh2, options)) return NEEDS_APPLY;
|
|
5072
|
+
if (!await isSwapActive(ssh2, options.path)) return NEEDS_APPLY;
|
|
5073
|
+
return await hasSwapFstabEntry(ssh2, options) ? "ok" : NEEDS_APPLY;
|
|
5074
|
+
}
|
|
5075
|
+
async function checkSwapFile(ssh2, options) {
|
|
5076
|
+
return options.state === "absent" ? checkAbsent(ssh2, options) : checkPresent(ssh2, options);
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
// src/modules/sysctl.ts
|
|
5080
|
+
var EXEC_OPTS10 = { ignoreExitCode: true, silent: true };
|
|
4379
5081
|
var SYSCTL_DIR = "/etc/sysctl.d";
|
|
4380
5082
|
var SYSCTL_CONFIG_MODE = "0644";
|
|
4381
5083
|
function sanitizeKey(key) {
|
|
@@ -4411,28 +5113,28 @@ var sysctl = {
|
|
|
4411
5113
|
if (!conn) return failed(`[sysctl.set: ${key}] SSH connection is required`);
|
|
4412
5114
|
if (state === "present") {
|
|
4413
5115
|
const assignment = `${key}=${value}`;
|
|
4414
|
-
const result = await conn.exec(`sysctl -w ${shellQuote(assignment)}`,
|
|
5116
|
+
const result = await conn.exec(`sysctl -w ${shellQuote(assignment)}`, EXEC_OPTS10);
|
|
4415
5117
|
if (result.code !== 0) {
|
|
4416
5118
|
return failedCommand(`[sysctl.set: ${key}] sysctl -w failed`, result);
|
|
4417
5119
|
}
|
|
4418
5120
|
await conn.writeFile(configPath, expectedContent, { mode: SYSCTL_CONFIG_MODE });
|
|
4419
5121
|
} else {
|
|
4420
|
-
await conn.exec(`rm -f ${shellQuote(configPath)}`,
|
|
5122
|
+
await conn.exec(`rm -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4421
5123
|
}
|
|
4422
5124
|
return { status: "changed" };
|
|
4423
5125
|
},
|
|
4424
5126
|
async check(conn) {
|
|
4425
5127
|
if (!conn) return NEEDS_APPLY;
|
|
4426
5128
|
if (state === "present") {
|
|
4427
|
-
const result = await conn.exec(`sysctl -n ${shellQuote(key)}`,
|
|
5129
|
+
const result = await conn.exec(`sysctl -n ${shellQuote(key)}`, EXEC_OPTS10);
|
|
4428
5130
|
const currentValue = result.stdout.trim();
|
|
4429
5131
|
if (currentValue !== value) return NEEDS_APPLY;
|
|
4430
|
-
const configExists = await conn.exec(`test -f ${shellQuote(configPath)}`,
|
|
5132
|
+
const configExists = await conn.exec(`test -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4431
5133
|
if (configExists.code !== 0) return NEEDS_APPLY;
|
|
4432
5134
|
const fileContent = await conn.readFile(configPath);
|
|
4433
5135
|
return fileContent.trim() === expectedContent.trim() ? "ok" : NEEDS_APPLY;
|
|
4434
5136
|
}
|
|
4435
|
-
const fileExists = await conn.exec(`test -f ${shellQuote(configPath)}`,
|
|
5137
|
+
const fileExists = await conn.exec(`test -f ${shellQuote(configPath)}`, EXEC_OPTS10);
|
|
4436
5138
|
return fileExists.code === 0 ? NEEDS_APPLY : "ok";
|
|
4437
5139
|
},
|
|
4438
5140
|
name: state === "present" ? `sysctl.set: ${key}=${value}` : `sysctl.set: absent ${key}`
|
|
@@ -4440,6 +5142,53 @@ var sysctl = {
|
|
|
4440
5142
|
}
|
|
4441
5143
|
};
|
|
4442
5144
|
|
|
5145
|
+
// src/modules/swap.ts
|
|
5146
|
+
var swap = {
|
|
5147
|
+
/**
|
|
5148
|
+
* Ensure a file-backed swap area exists, is activated, and is persisted in `/etc/fstab`.
|
|
5149
|
+
*
|
|
5150
|
+
* @param options - Configuration for the swap file.
|
|
5151
|
+
* @param options.mode - File mode applied to the swap file. Defaults to `0600`.
|
|
5152
|
+
* @param options.path - Absolute path to the swap file, e.g. `/swapfile`.
|
|
5153
|
+
* @param options.priority - Optional swap priority written into the fstab entry.
|
|
5154
|
+
* @param options.size - Desired file size as bytes or a shell-friendly size string such as `2G`.
|
|
5155
|
+
* @param options.state - Whether the swap file should be `present` (default) or `absent`.
|
|
5156
|
+
* @returns A Module that manages the swap file lifecycle.
|
|
5157
|
+
*/
|
|
5158
|
+
file(options) {
|
|
5159
|
+
const normalized = normalizeSwapFileOptions(options);
|
|
5160
|
+
return {
|
|
5161
|
+
async apply(ssh2) {
|
|
5162
|
+
if (!ssh2) return failed(`[swap.file: ${normalized.path}] SSH connection is required`);
|
|
5163
|
+
return applySwapFile(ssh2, normalized);
|
|
5164
|
+
},
|
|
5165
|
+
async check(ssh2) {
|
|
5166
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
5167
|
+
return checkSwapFile(ssh2, normalized);
|
|
5168
|
+
},
|
|
5169
|
+
name: normalized.state === "present" ? `swap.file: ${normalized.path} (${normalized.sizeForCommand})` : `swap.file: absent ${normalized.path}`
|
|
5170
|
+
};
|
|
5171
|
+
},
|
|
5172
|
+
/**
|
|
5173
|
+
* Persist `vm.swappiness`.
|
|
5174
|
+
*
|
|
5175
|
+
* @param value - Desired swappiness value.
|
|
5176
|
+
* @returns A Module that manages `vm.swappiness`.
|
|
5177
|
+
*/
|
|
5178
|
+
swappiness(value) {
|
|
5179
|
+
return sysctl.set("vm.swappiness", String(value));
|
|
5180
|
+
},
|
|
5181
|
+
/**
|
|
5182
|
+
* Persist `vm.vfs_cache_pressure`.
|
|
5183
|
+
*
|
|
5184
|
+
* @param value - Desired VFS cache pressure value.
|
|
5185
|
+
* @returns A Module that manages `vm.vfs_cache_pressure`.
|
|
5186
|
+
*/
|
|
5187
|
+
vfsCachePressure(value) {
|
|
5188
|
+
return sysctl.set("vm.vfs_cache_pressure", String(value));
|
|
5189
|
+
}
|
|
5190
|
+
};
|
|
5191
|
+
|
|
4443
5192
|
// src/modules/system.ts
|
|
4444
5193
|
function parseOsRelease(content) {
|
|
4445
5194
|
const result = {};
|
|
@@ -4610,8 +5359,8 @@ ${message}`);
|
|
|
4610
5359
|
};
|
|
4611
5360
|
|
|
4612
5361
|
// src/modules/systemd.ts
|
|
4613
|
-
var
|
|
4614
|
-
var
|
|
5362
|
+
var SYSTEMCTL4 = "systemctl";
|
|
5363
|
+
var UNIT_NAME_PATTERN3 = new RegExp("^[\\w@.\\-]+$", "v");
|
|
4615
5364
|
var SYSTEMD_UNIT_MODE2 = "0644";
|
|
4616
5365
|
var systemd = {
|
|
4617
5366
|
/**
|
|
@@ -4623,7 +5372,7 @@ var systemd = {
|
|
|
4623
5372
|
return {
|
|
4624
5373
|
async apply(ssh2) {
|
|
4625
5374
|
if (!ssh2) return failed("[systemd.daemonReload] SSH connection is required");
|
|
4626
|
-
const result = await ssh2.exec(`${
|
|
5375
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} daemon-reload`, {
|
|
4627
5376
|
ignoreExitCode: true,
|
|
4628
5377
|
silent: true
|
|
4629
5378
|
});
|
|
@@ -4645,7 +5394,7 @@ var systemd = {
|
|
|
4645
5394
|
return {
|
|
4646
5395
|
async apply(ssh2) {
|
|
4647
5396
|
if (!ssh2) return failed(`[systemd.masked: ${name}] SSH connection is required`);
|
|
4648
|
-
const result = await ssh2.exec(`${
|
|
5397
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} mask ${shellQuote(name)}`, {
|
|
4649
5398
|
ignoreExitCode: true,
|
|
4650
5399
|
silent: true
|
|
4651
5400
|
});
|
|
@@ -4653,7 +5402,7 @@ var systemd = {
|
|
|
4653
5402
|
},
|
|
4654
5403
|
async check(ssh2) {
|
|
4655
5404
|
if (!ssh2) return NEEDS_APPLY;
|
|
4656
|
-
const result = await ssh2.exec(`${
|
|
5405
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} is-enabled ${shellQuote(name)}`, {
|
|
4657
5406
|
ignoreExitCode: true,
|
|
4658
5407
|
silent: true
|
|
4659
5408
|
});
|
|
@@ -4674,15 +5423,15 @@ var systemd = {
|
|
|
4674
5423
|
* @returns A Module that ensures the unit file is present with the given content.
|
|
4675
5424
|
*/
|
|
4676
5425
|
unit(name, content) {
|
|
4677
|
-
if (!
|
|
4678
|
-
throw new Error(`systemd.unit: name must match ${String(
|
|
5426
|
+
if (!UNIT_NAME_PATTERN3.test(name)) {
|
|
5427
|
+
throw new Error(`systemd.unit: name must match ${String(UNIT_NAME_PATTERN3)}, got: ${name}`);
|
|
4679
5428
|
}
|
|
4680
5429
|
const filePath = `/etc/systemd/system/${name}`;
|
|
4681
5430
|
return {
|
|
4682
5431
|
async apply(ssh2) {
|
|
4683
5432
|
if (!ssh2) return failed(`[systemd.unit: ${name}] SSH connection is required`);
|
|
4684
5433
|
await ssh2.writeFile(filePath, content, { mode: SYSTEMD_UNIT_MODE2 });
|
|
4685
|
-
const result = await ssh2.exec(`${
|
|
5434
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} daemon-reload`, {
|
|
4686
5435
|
ignoreExitCode: true,
|
|
4687
5436
|
silent: true
|
|
4688
5437
|
});
|
|
@@ -4707,7 +5456,7 @@ var systemd = {
|
|
|
4707
5456
|
return {
|
|
4708
5457
|
async apply(ssh2) {
|
|
4709
5458
|
if (!ssh2) return failed(`[systemd.unmasked: ${name}] SSH connection is required`);
|
|
4710
|
-
const result = await ssh2.exec(`${
|
|
5459
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} unmask ${shellQuote(name)}`, {
|
|
4711
5460
|
ignoreExitCode: true,
|
|
4712
5461
|
silent: true
|
|
4713
5462
|
});
|
|
@@ -4715,7 +5464,7 @@ var systemd = {
|
|
|
4715
5464
|
},
|
|
4716
5465
|
async check(ssh2) {
|
|
4717
5466
|
if (!ssh2) return NEEDS_APPLY;
|
|
4718
|
-
const result = await ssh2.exec(`${
|
|
5467
|
+
const result = await ssh2.exec(`${SYSTEMCTL4} is-enabled ${shellQuote(name)}`, {
|
|
4719
5468
|
ignoreExitCode: true,
|
|
4720
5469
|
silent: true
|
|
4721
5470
|
});
|
|
@@ -4979,6 +5728,7 @@ export {
|
|
|
4979
5728
|
mount,
|
|
4980
5729
|
net,
|
|
4981
5730
|
op,
|
|
5731
|
+
quadlet,
|
|
4982
5732
|
releaseUpgrade,
|
|
4983
5733
|
rsync,
|
|
4984
5734
|
script,
|
|
@@ -4986,9 +5736,10 @@ export {
|
|
|
4986
5736
|
ssh,
|
|
4987
5737
|
sshd,
|
|
4988
5738
|
sysctl,
|
|
5739
|
+
swap,
|
|
4989
5740
|
system,
|
|
4990
5741
|
systemd,
|
|
4991
5742
|
ufw,
|
|
4992
5743
|
user
|
|
4993
5744
|
};
|
|
4994
|
-
//# sourceMappingURL=chunk-
|
|
5745
|
+
//# sourceMappingURL=chunk-ENWMSERJ.js.map
|