paratix 0.15.1 → 0.16.1
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/{chunk-C5KXNY6R.js → chunk-KC7XBI77.js} +197 -68
- package/dist/cli.js +2 -2
- package/dist/{index-Da9ccU5K.d.ts → index-jbXRUlJ6.d.ts} +36 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +1 -1
- package/llm-guide.md +27 -14
- package/package.json +1 -1
|
@@ -2519,6 +2519,13 @@ async function validateExistingExtractDestination(conn, parameters) {
|
|
|
2519
2519
|
// src/modules/fileMetadataHelpers.ts
|
|
2520
2520
|
var EXEC_OPTS2 = { ignoreExitCode: true, silent: true };
|
|
2521
2521
|
var DEFAULT_FILE_WRITE_MODE = "0644";
|
|
2522
|
+
var EMPTY_OWNERSHIP = {
|
|
2523
|
+
group: "",
|
|
2524
|
+
groupId: "",
|
|
2525
|
+
mode: "",
|
|
2526
|
+
owner: "",
|
|
2527
|
+
ownerId: ""
|
|
2528
|
+
};
|
|
2522
2529
|
function assertValidChownOwnershipSpec(ownerSpec) {
|
|
2523
2530
|
if (ownerSpec === "") {
|
|
2524
2531
|
throw new Error("chown ownership spec must not be empty");
|
|
@@ -2581,20 +2588,23 @@ function renderGuardedMetadataCommand(kind, remotePath, value) {
|
|
|
2581
2588
|
].join("\n");
|
|
2582
2589
|
}
|
|
2583
2590
|
async function readOwnership(ssh2, remotePath) {
|
|
2584
|
-
const result = await ssh2.exec(`stat -c '%a %U %G' ${shellQuote(remotePath)}`, EXEC_OPTS2);
|
|
2585
|
-
if (result.code !== 0) return {
|
|
2586
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
2587
|
-
return { group: group2, mode, owner };
|
|
2591
|
+
const result = await ssh2.exec(`stat -c '%a %U %G %u %g' ${shellQuote(remotePath)}`, EXEC_OPTS2);
|
|
2592
|
+
if (result.code !== 0) return { ...EMPTY_OWNERSHIP };
|
|
2593
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
2594
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
2588
2595
|
}
|
|
2589
2596
|
function normalizeMode(mode) {
|
|
2590
2597
|
return mode.startsWith("0") ? mode : `0${mode}`;
|
|
2591
2598
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2599
|
+
var NUMERIC_ID_PATTERN = new RegExp("^\\d+$", "v");
|
|
2600
|
+
function isNumericId(value) {
|
|
2601
|
+
return NUMERIC_ID_PATTERN.test(value);
|
|
2594
2602
|
}
|
|
2595
|
-
function
|
|
2596
|
-
if (
|
|
2597
|
-
|
|
2603
|
+
function ownershipComponentMatches(expected, actual, actualId) {
|
|
2604
|
+
if (expected === "") return true;
|
|
2605
|
+
if (actual === expected || actualId === expected) return true;
|
|
2606
|
+
if (!isNumericId(expected) || !isNumericId(actualId)) return false;
|
|
2607
|
+
return Number(expected) === Number(actualId);
|
|
2598
2608
|
}
|
|
2599
2609
|
async function resolveWriteMode(ssh2, remotePath, explicitMode) {
|
|
2600
2610
|
if (explicitMode != null) return explicitMode;
|
|
@@ -2635,10 +2645,10 @@ function ownershipMatches(current, options) {
|
|
|
2635
2645
|
if (options?.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "")) return false;
|
|
2636
2646
|
if (options?.owner == null) return true;
|
|
2637
2647
|
const expectsGroup = options.owner.includes(":");
|
|
2638
|
-
const [expectedOwner, expectedGroup = ""] = options.owner.split(":", 2);
|
|
2639
|
-
if (!ownershipComponentMatches(expectedOwner, current.owner)) return false;
|
|
2640
|
-
if (!
|
|
2641
|
-
return
|
|
2648
|
+
const [expectedOwner = "", expectedGroup = ""] = options.owner.split(":", 2);
|
|
2649
|
+
if (!ownershipComponentMatches(expectedOwner, current.owner, current.ownerId)) return false;
|
|
2650
|
+
if (!expectsGroup) return true;
|
|
2651
|
+
return ownershipComponentMatches(expectedGroup, current.group, current.groupId);
|
|
2642
2652
|
}
|
|
2643
2653
|
function createMetadataModule(kind, remotePath, value) {
|
|
2644
2654
|
const name = `file.${kind}: ${remotePath}`;
|
|
@@ -2676,6 +2686,7 @@ var SILENT = { silent: true };
|
|
|
2676
2686
|
var FLAGS_DIR = "/var/lib/paratix/flags";
|
|
2677
2687
|
var ARCHIVE_MARKER_MODE = "0644";
|
|
2678
2688
|
var ARCHIVE_OWNER_MEMBER_CONCURRENCY = 8;
|
|
2689
|
+
var ARCHIVE_STAT_OWNERSHIP_FIELDS = 4;
|
|
2679
2690
|
async function mapWithConcurrencyLimit2(items, limit, mapper) {
|
|
2680
2691
|
if (items.length === 0) return [];
|
|
2681
2692
|
const results = [];
|
|
@@ -3055,11 +3066,9 @@ async function applyExtract(conn, parameters) {
|
|
|
3055
3066
|
}
|
|
3056
3067
|
}
|
|
3057
3068
|
function ownerMatchesStat(stdout, owner) {
|
|
3058
|
-
const [actualUser = "", actualGroup = ""] = stdout.trim().split(new RegExp("\\s+", "v"),
|
|
3069
|
+
const [actualUser = "", actualGroup = "", actualUserId = "", actualGroupId = ""] = stdout.trim().split(new RegExp("\\s+", "v"), ARCHIVE_STAT_OWNERSHIP_FIELDS);
|
|
3059
3070
|
const [expectedUser = "", expectedGroup = ""] = owner.split(":", 2);
|
|
3060
|
-
|
|
3061
|
-
if (expectedGroup !== "" && actualGroup !== expectedGroup) return false;
|
|
3062
|
-
return true;
|
|
3071
|
+
return ownershipComponentMatches(expectedUser, actualUser, actualUserId) && ownershipComponentMatches(expectedGroup, actualGroup, actualGroupId);
|
|
3063
3072
|
}
|
|
3064
3073
|
function isStringArray(value) {
|
|
3065
3074
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
@@ -3071,7 +3080,7 @@ async function extractedMemberOwnerMatches(conn, parameters) {
|
|
|
3071
3080
|
EXEC_OPTS3
|
|
3072
3081
|
);
|
|
3073
3082
|
if (exists.code !== 0) return false;
|
|
3074
|
-
const stat5 = await conn.exec(`stat -c '%U %G' -- ${shellQuote(path3)}`, EXEC_OPTS3);
|
|
3083
|
+
const stat5 = await conn.exec(`stat -c '%U %G %u %g' -- ${shellQuote(path3)}`, EXEC_OPTS3);
|
|
3075
3084
|
if (stat5.code !== 0) return false;
|
|
3076
3085
|
return ownerMatchesStat(stat5.stdout, owner);
|
|
3077
3086
|
}
|
|
@@ -3446,19 +3455,8 @@ async function resolveComposeConfigImages(parameters) {
|
|
|
3446
3455
|
`${composeCommand(parameters.runtime, parameters.projectDirectory)} config --format json`,
|
|
3447
3456
|
EXEC_OPTS4
|
|
3448
3457
|
);
|
|
3449
|
-
if (result.code !== 0)
|
|
3450
|
-
|
|
3451
|
-
`[compose.pull] failed to resolve images for ${parameters.projectDirectory}`,
|
|
3452
|
-
result
|
|
3453
|
-
);
|
|
3454
|
-
}
|
|
3455
|
-
const images = parseComposeConfigImages(result.stdout);
|
|
3456
|
-
if (images === null) {
|
|
3457
|
-
return failed(
|
|
3458
|
-
`[compose.pull] failed to parse compose config images for ${parameters.projectDirectory}`
|
|
3459
|
-
);
|
|
3460
|
-
}
|
|
3461
|
-
return images;
|
|
3458
|
+
if (result.code !== 0) return null;
|
|
3459
|
+
return parseComposeConfigImages(result.stdout);
|
|
3462
3460
|
}
|
|
3463
3461
|
async function inspectComposeImageBeforePull(parameters) {
|
|
3464
3462
|
const result = await parameters.ssh.exec(
|
|
@@ -4194,7 +4192,15 @@ var compose = {
|
|
|
4194
4192
|
runtime,
|
|
4195
4193
|
ssh: connection
|
|
4196
4194
|
});
|
|
4197
|
-
if (
|
|
4195
|
+
if (images === null) {
|
|
4196
|
+
const fallbackResult = await connection.exec(
|
|
4197
|
+
`${composeCommand(runtime, projectDirectory)} pull 2>&1`,
|
|
4198
|
+
EXEC_OPTS4
|
|
4199
|
+
);
|
|
4200
|
+
if (fallbackResult.code !== 0)
|
|
4201
|
+
return failedCommand(`[compose.pull] failed for ${projectDirectory}`, fallbackResult);
|
|
4202
|
+
return { status: "changed" };
|
|
4203
|
+
}
|
|
4198
4204
|
const beforePull = await snapshotComposeImagesBeforePull({
|
|
4199
4205
|
images,
|
|
4200
4206
|
runtime,
|
|
@@ -5357,18 +5363,22 @@ function rejectSensitiveHeadersOverHttp(parameters) {
|
|
|
5357
5363
|
);
|
|
5358
5364
|
}
|
|
5359
5365
|
async function readDownloadOwnership(conn, destination) {
|
|
5360
|
-
const result = await conn.exec(`stat -c '%a %U %G' ${shellQuote(destination)}`, {
|
|
5366
|
+
const result = await conn.exec(`stat -c '%a %U %G %u %g' ${shellQuote(destination)}`, {
|
|
5361
5367
|
ignoreExitCode: true,
|
|
5362
5368
|
silent: true
|
|
5363
5369
|
});
|
|
5364
|
-
if (result.code !== 0) return { group: "", mode: "", owner: "" };
|
|
5365
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
5366
|
-
return { group: group2, mode, owner };
|
|
5370
|
+
if (result.code !== 0) return { group: "", groupId: "", mode: "", owner: "", ownerId: "" };
|
|
5371
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
5372
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
5373
|
+
}
|
|
5374
|
+
function downloadComponentMatches(expected, actual, actualId) {
|
|
5375
|
+
if (expected == null) return true;
|
|
5376
|
+
return ownershipComponentMatches(expected, actual, actualId);
|
|
5367
5377
|
}
|
|
5368
5378
|
function downloadOwnershipMatches(current, options) {
|
|
5369
5379
|
if (options.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "")) return false;
|
|
5370
|
-
if (options.owner
|
|
5371
|
-
if (options.group
|
|
5380
|
+
if (!downloadComponentMatches(options.owner, current.owner, current.ownerId)) return false;
|
|
5381
|
+
if (!downloadComponentMatches(options.group, current.group, current.groupId)) return false;
|
|
5372
5382
|
return true;
|
|
5373
5383
|
}
|
|
5374
5384
|
async function metadataMatches(conn, destination, options) {
|
|
@@ -5567,7 +5577,7 @@ function downloadModeDrifted(current, options) {
|
|
|
5567
5577
|
return options.mode != null && current.mode !== options.mode.replace(new RegExp("^0+", "v"), "");
|
|
5568
5578
|
}
|
|
5569
5579
|
function downloadOwnerDrifted(current, options) {
|
|
5570
|
-
return options.owner
|
|
5580
|
+
return !downloadComponentMatches(options.owner, current.owner, current.ownerId) || !downloadComponentMatches(options.group, current.group, current.groupId);
|
|
5571
5581
|
}
|
|
5572
5582
|
async function healModeDrift(conn, parameters, current) {
|
|
5573
5583
|
if (parameters.mode == null || !downloadModeDrifted(current, parameters)) {
|
|
@@ -6925,18 +6935,27 @@ ${endMarker}`
|
|
|
6925
6935
|
};
|
|
6926
6936
|
}
|
|
6927
6937
|
async function readPropertiesState(ssh2, remotePath) {
|
|
6928
|
-
const result = await ssh2.exec(`stat -c '%a %U %G' ${shellQuote(remotePath)}`, EXEC_OPTS6);
|
|
6929
|
-
if (result.code !== 0) return { group: "", mode: "", owner: "" };
|
|
6930
|
-
const [mode = "", owner = "", group2 = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
6931
|
-
return { group: group2, mode, owner };
|
|
6938
|
+
const result = await ssh2.exec(`stat -c '%a %U %G %u %g' ${shellQuote(remotePath)}`, EXEC_OPTS6);
|
|
6939
|
+
if (result.code !== 0) return { group: "", groupId: "", mode: "", owner: "", ownerId: "" };
|
|
6940
|
+
const [mode = "", owner = "", group2 = "", ownerId = "", groupId = ""] = result.stdout.trim().split(new RegExp("\\s+", "v"));
|
|
6941
|
+
return { group: group2, groupId, mode, owner, ownerId };
|
|
6932
6942
|
}
|
|
6933
6943
|
function modeMatches(current, desired) {
|
|
6934
6944
|
return current === desired.replace(new RegExp("^0+", "v"), "");
|
|
6935
6945
|
}
|
|
6946
|
+
function assertValidOwnershipComponent(value, kind) {
|
|
6947
|
+
if (isNumericId(value)) return;
|
|
6948
|
+
if (kind === "user") assertValidUserName(value);
|
|
6949
|
+
else assertValidGroupName(value);
|
|
6950
|
+
}
|
|
6951
|
+
function propertiesComponentMatches(expected, actual, actualId) {
|
|
6952
|
+
if (expected == null) return true;
|
|
6953
|
+
return ownershipComponentMatches(expected, actual, actualId);
|
|
6954
|
+
}
|
|
6936
6955
|
function assertValidPropertiesOptions(options) {
|
|
6937
6956
|
if (options.mode != null) validateMode(options.mode);
|
|
6938
|
-
if (options.owner != null)
|
|
6939
|
-
if (options.group != null)
|
|
6957
|
+
if (options.owner != null) assertValidOwnershipComponent(options.owner, "user");
|
|
6958
|
+
if (options.group != null) assertValidOwnershipComponent(options.group, "group");
|
|
6940
6959
|
}
|
|
6941
6960
|
function isDriftFailure(result) {
|
|
6942
6961
|
return typeof result !== "boolean";
|
|
@@ -6952,8 +6971,8 @@ async function applyModeDrift(context) {
|
|
|
6952
6971
|
}
|
|
6953
6972
|
async function maybeApplyCombinedChown(context) {
|
|
6954
6973
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6955
|
-
const ownerNeedsUpdate = options.owner != null && current.owner
|
|
6956
|
-
const groupNeedsUpdate = options.group != null && current.group
|
|
6974
|
+
const ownerNeedsUpdate = options.owner != null && !propertiesComponentMatches(options.owner, current.owner, current.ownerId);
|
|
6975
|
+
const groupNeedsUpdate = options.group != null && !propertiesComponentMatches(options.group, current.group, current.groupId);
|
|
6957
6976
|
if (!ownerNeedsUpdate || !groupNeedsUpdate || options.owner == null || options.group == null) {
|
|
6958
6977
|
return false;
|
|
6959
6978
|
}
|
|
@@ -6966,7 +6985,9 @@ async function maybeApplyCombinedChown(context) {
|
|
|
6966
6985
|
}
|
|
6967
6986
|
async function maybeApplySingleChown(context) {
|
|
6968
6987
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6969
|
-
if (options.owner == null || current.owner
|
|
6988
|
+
if (options.owner == null || propertiesComponentMatches(options.owner, current.owner, current.ownerId)) {
|
|
6989
|
+
return false;
|
|
6990
|
+
}
|
|
6970
6991
|
const result = await ssh2.exec(renderGuardedChownCommand(options.owner, remotePath), EXEC_OPTS6);
|
|
6971
6992
|
if (result.code !== 0) {
|
|
6972
6993
|
return failedCommand(`[file.properties: ${remotePath}] chown failed`, result);
|
|
@@ -6975,7 +6996,9 @@ async function maybeApplySingleChown(context) {
|
|
|
6975
6996
|
}
|
|
6976
6997
|
async function maybeApplySingleChgrp(context) {
|
|
6977
6998
|
const { current, options, remotePath, ssh: ssh2 } = context;
|
|
6978
|
-
if (options.group == null || current.group
|
|
6999
|
+
if (options.group == null || propertiesComponentMatches(options.group, current.group, current.groupId)) {
|
|
7000
|
+
return false;
|
|
7001
|
+
}
|
|
6979
7002
|
const result = await ssh2.exec(renderGuardedChgrpCommand(options.group, remotePath), EXEC_OPTS6);
|
|
6980
7003
|
if (result.code !== 0) {
|
|
6981
7004
|
return failedCommand(`[file.properties: ${remotePath}] chgrp failed`, result);
|
|
@@ -7014,10 +7037,10 @@ function properties(remotePath, options) {
|
|
|
7014
7037
|
async check(ssh2) {
|
|
7015
7038
|
if (!ssh2) return NEEDS_APPLY;
|
|
7016
7039
|
if (await isSymlink(ssh2, remotePath)) return NEEDS_APPLY;
|
|
7017
|
-
const { group: group2, mode, owner } = await readPropertiesState(ssh2, remotePath);
|
|
7040
|
+
const { group: group2, groupId, mode, owner, ownerId } = await readPropertiesState(ssh2, remotePath);
|
|
7018
7041
|
if (options.mode != null && !modeMatches(mode, options.mode)) return NEEDS_APPLY;
|
|
7019
|
-
if (options.owner
|
|
7020
|
-
if (options.group
|
|
7042
|
+
if (!propertiesComponentMatches(options.owner, owner, ownerId)) return NEEDS_APPLY;
|
|
7043
|
+
if (!propertiesComponentMatches(options.group, group2, groupId)) return NEEDS_APPLY;
|
|
7021
7044
|
return "ok";
|
|
7022
7045
|
},
|
|
7023
7046
|
name: `file.properties: ${remotePath}`
|
|
@@ -11030,10 +11053,11 @@ async function snapshotQuadletFile(ssh2, filePath) {
|
|
|
11030
11053
|
mode: normalizeQuadletMode(rawMode)
|
|
11031
11054
|
};
|
|
11032
11055
|
}
|
|
11033
|
-
async function restoreQuadletFileSnapshot(
|
|
11056
|
+
async function restoreQuadletFileSnapshot(parameters) {
|
|
11057
|
+
const { filePath, label, snapshot, ssh: ssh2 } = parameters;
|
|
11034
11058
|
if (snapshot.exists) {
|
|
11035
11059
|
if (await isSymlink(ssh2, filePath)) {
|
|
11036
|
-
throw new Error(`[
|
|
11060
|
+
throw new Error(`[${label}] refuses to restore through symlink at ${filePath}`);
|
|
11037
11061
|
}
|
|
11038
11062
|
await ssh2.writeFile(filePath, snapshot.content, { mode: snapshot.mode });
|
|
11039
11063
|
return;
|
|
@@ -11041,24 +11065,30 @@ async function restoreQuadletFileSnapshot(ssh2, filePath, snapshot) {
|
|
|
11041
11065
|
await ssh2.exec(`rm -f ${shellQuote(filePath)}`, { ignoreExitCode: true, silent: true });
|
|
11042
11066
|
}
|
|
11043
11067
|
async function applyQuadletFile(parameters) {
|
|
11068
|
+
const label = parameters.label ?? "quadlet.container";
|
|
11044
11069
|
const mkdirResult = await parameters.ssh.exec(CONTAINERS_SYSTEMD_DIRECTORY_COMMAND, {
|
|
11045
11070
|
ignoreExitCode: true,
|
|
11046
11071
|
silent: true
|
|
11047
11072
|
});
|
|
11048
11073
|
if (mkdirResult.code !== 0) {
|
|
11049
11074
|
return failedCommand(
|
|
11050
|
-
`[
|
|
11075
|
+
`[${label}: ${parameters.name}] failed to create quadlet directory`,
|
|
11051
11076
|
mkdirResult
|
|
11052
11077
|
);
|
|
11053
11078
|
}
|
|
11054
11079
|
if (await isSymlink(parameters.ssh, parameters.filePath)) {
|
|
11055
11080
|
return failed(
|
|
11056
|
-
`[
|
|
11081
|
+
`[${label}: ${parameters.name}] refuses to write through symlink at ${parameters.filePath}`
|
|
11057
11082
|
);
|
|
11058
11083
|
}
|
|
11059
11084
|
const snapshot = await snapshotQuadletFile(parameters.ssh, parameters.filePath);
|
|
11060
11085
|
const restoreSnapshot = async () => {
|
|
11061
|
-
await restoreQuadletFileSnapshot(
|
|
11086
|
+
await restoreQuadletFileSnapshot({
|
|
11087
|
+
filePath: parameters.filePath,
|
|
11088
|
+
label,
|
|
11089
|
+
snapshot,
|
|
11090
|
+
ssh: parameters.ssh
|
|
11091
|
+
});
|
|
11062
11092
|
};
|
|
11063
11093
|
try {
|
|
11064
11094
|
await parameters.ssh.writeFile(parameters.filePath, parameters.content, {
|
|
@@ -11067,7 +11097,7 @@ async function applyQuadletFile(parameters) {
|
|
|
11067
11097
|
} catch (error) {
|
|
11068
11098
|
const reason = error instanceof Error ? error.message : String(error);
|
|
11069
11099
|
return withRollbackFailure(
|
|
11070
|
-
failed(`[
|
|
11100
|
+
failed(`[${label}: ${parameters.name}] failed to write quadlet file: ${reason}`),
|
|
11071
11101
|
restoreSnapshot,
|
|
11072
11102
|
"quadlet apply failed"
|
|
11073
11103
|
);
|
|
@@ -11078,10 +11108,7 @@ async function applyQuadletFile(parameters) {
|
|
|
11078
11108
|
});
|
|
11079
11109
|
if (daemonReload.code === 0) return { status: "changed" };
|
|
11080
11110
|
return withRollbackFailure(
|
|
11081
|
-
failedCommand(
|
|
11082
|
-
`[quadlet.container: ${parameters.name}] systemctl daemon-reload failed`,
|
|
11083
|
-
daemonReload
|
|
11084
|
-
),
|
|
11111
|
+
failedCommand(`[${label}: ${parameters.name}] systemctl daemon-reload failed`, daemonReload),
|
|
11085
11112
|
restoreSnapshot,
|
|
11086
11113
|
"quadlet apply failed"
|
|
11087
11114
|
);
|
|
@@ -11117,7 +11144,7 @@ var QUADLET_PULL_CHANGED_OUTPUT_PATTERNS = [
|
|
|
11117
11144
|
];
|
|
11118
11145
|
function assertQuadletLineValue(key, value) {
|
|
11119
11146
|
if (QUADLET_CONTROL_CHARACTER_PATTERN.test(value)) {
|
|
11120
|
-
throw new Error(`quadlet
|
|
11147
|
+
throw new Error(`quadlet ${key} values must not contain control characters`);
|
|
11121
11148
|
}
|
|
11122
11149
|
}
|
|
11123
11150
|
function renderQuadletLine(key, value) {
|
|
@@ -11350,6 +11377,37 @@ function parseQuadletImageInspectOutput(output) {
|
|
|
11350
11377
|
};
|
|
11351
11378
|
}
|
|
11352
11379
|
|
|
11380
|
+
// src/modules/quadletNetworkHelpers.ts
|
|
11381
|
+
function getQuadletNetworkFilePath(name) {
|
|
11382
|
+
return `${CONTAINERS_SYSTEMD_DIRECTORY}/${name}.network`;
|
|
11383
|
+
}
|
|
11384
|
+
function buildQuadletNetworkUnitLines(options) {
|
|
11385
|
+
return compactQuadletLines([
|
|
11386
|
+
renderQuadletLine("NetworkName", options.name),
|
|
11387
|
+
maybeRenderQuadletLine("Driver", options.driver),
|
|
11388
|
+
maybeRenderQuadletLine("IPAMDriver", options.ipamDriver),
|
|
11389
|
+
maybeRenderQuadletBool("Internal", options.internal),
|
|
11390
|
+
maybeRenderQuadletBool("IPv6", options.ipv6),
|
|
11391
|
+
maybeRenderQuadletBool("DisableDNS", options.disableDns),
|
|
11392
|
+
maybeRenderQuadletLine("Subnet", options.subnet),
|
|
11393
|
+
maybeRenderQuadletLine("Gateway", options.gateway),
|
|
11394
|
+
maybeRenderQuadletLine("IPRange", options.ipRange),
|
|
11395
|
+
...renderQuadletRepeated("DNS", options.dns ?? []),
|
|
11396
|
+
...renderQuadletKeyValue("Options", options.options ?? {}),
|
|
11397
|
+
...renderQuadletKeyValue("Label", options.label ?? {}),
|
|
11398
|
+
...renderQuadletRepeated("PodmanArgs", options.podmanArgs ?? [])
|
|
11399
|
+
]);
|
|
11400
|
+
}
|
|
11401
|
+
function generateNetworkQuadlet(options) {
|
|
11402
|
+
const sections = [
|
|
11403
|
+
renderQuadletSection("Unit", [
|
|
11404
|
+
renderQuadletLine("Description", options.description ?? `Podman network: ${options.name}`)
|
|
11405
|
+
]),
|
|
11406
|
+
renderQuadletSection("Network", buildQuadletNetworkUnitLines(options))
|
|
11407
|
+
];
|
|
11408
|
+
return sections.join("\n").trimEnd();
|
|
11409
|
+
}
|
|
11410
|
+
|
|
11353
11411
|
// src/modules/quadletValidationHelpers.ts
|
|
11354
11412
|
var UNIT_NAME_PATTERN2 = new RegExp("^[\\w@.\\-]+$", "v");
|
|
11355
11413
|
var UNIT_NAME_ALL_DOTS_PATTERN = new RegExp("^\\.+$", "v");
|
|
@@ -11424,8 +11482,8 @@ function validateQuadletAuthFilePath(field, value) {
|
|
|
11424
11482
|
// src/modules/quadlet.ts
|
|
11425
11483
|
var QUADLET_RELOAD_HASH_LENGTH = 16;
|
|
11426
11484
|
var SYSTEMCTL2 = "systemctl";
|
|
11427
|
-
function buildQuadletReloadFlag(name, content) {
|
|
11428
|
-
const flagPrefix = `quadlet
|
|
11485
|
+
function buildQuadletReloadFlag(kind, name, content) {
|
|
11486
|
+
const flagPrefix = `quadlet-${kind}-${sha256String(name).slice(0, QUADLET_RELOAD_HASH_LENGTH)}-`;
|
|
11429
11487
|
return {
|
|
11430
11488
|
flagName: `${flagPrefix}${sha256String(content).slice(0, QUADLET_RELOAD_HASH_LENGTH)}`,
|
|
11431
11489
|
flagPrefix
|
|
@@ -11507,7 +11565,7 @@ async function applyQuadletImageUpdate(parameters) {
|
|
|
11507
11565
|
ssh: parameters.ssh
|
|
11508
11566
|
});
|
|
11509
11567
|
}
|
|
11510
|
-
async function
|
|
11568
|
+
async function buildQuadletDryRunDiff(ssh2, filePath, desired) {
|
|
11511
11569
|
try {
|
|
11512
11570
|
const exists = await ssh2.exists(filePath);
|
|
11513
11571
|
const current = exists ? await ssh2.readFile(filePath) : "";
|
|
@@ -11518,6 +11576,16 @@ async function buildQuadletContainerDryRunDiff(ssh2, filePath, desired) {
|
|
|
11518
11576
|
return void 0;
|
|
11519
11577
|
}
|
|
11520
11578
|
}
|
|
11579
|
+
async function podmanNetworkExists(ssh2, name) {
|
|
11580
|
+
const result = await ssh2.exec(`podman network exists -- ${shellQuote(name)}`, {
|
|
11581
|
+
ignoreExitCode: true,
|
|
11582
|
+
silent: true
|
|
11583
|
+
});
|
|
11584
|
+
return result.code === 0;
|
|
11585
|
+
}
|
|
11586
|
+
function quadletNetworkLiveHint(name) {
|
|
11587
|
+
return `network '${name}' already exists; the running network is not recreated automatically \u2014 remove it to apply changed settings`;
|
|
11588
|
+
}
|
|
11521
11589
|
var quadlet = {
|
|
11522
11590
|
/**
|
|
11523
11591
|
* Write a Podman Quadlet `.container` definition and reload systemd when it changes.
|
|
@@ -11533,11 +11601,11 @@ var quadlet = {
|
|
|
11533
11601
|
validateQuadletImageValue("image", options.image);
|
|
11534
11602
|
const filePath = getQuadletContainerFilePath(options.name);
|
|
11535
11603
|
const content = generateContainerQuadlet(options);
|
|
11536
|
-
const reloadFlag = buildQuadletReloadFlag(options.name, content);
|
|
11604
|
+
const reloadFlag = buildQuadletReloadFlag("container", options.name, content);
|
|
11537
11605
|
return {
|
|
11538
11606
|
async _applyDryRun(ssh2) {
|
|
11539
11607
|
if (!ssh2) return { status: "changed" };
|
|
11540
|
-
const diff = await
|
|
11608
|
+
const diff = await buildQuadletDryRunDiff(ssh2, filePath, content);
|
|
11541
11609
|
if (diff != null) return { diff, status: "changed" };
|
|
11542
11610
|
const flagPresent = await hasFlag(ssh2, reloadFlag.flagName);
|
|
11543
11611
|
if (flagPresent) return { status: "changed" };
|
|
@@ -11561,6 +11629,67 @@ var quadlet = {
|
|
|
11561
11629
|
name: `quadlet.container: ${options.name}`
|
|
11562
11630
|
};
|
|
11563
11631
|
},
|
|
11632
|
+
/**
|
|
11633
|
+
* Write a Podman Quadlet `.network` definition and reload systemd when it changes.
|
|
11634
|
+
*
|
|
11635
|
+
* Mirrors {@link quadlet.container}: the unit is written atomically to
|
|
11636
|
+
* `/etc/containers/systemd/<name>.network`, symlinks are refused, and
|
|
11637
|
+
* `systemctl daemon-reload` runs when the content changes. Containers
|
|
11638
|
+
* reference the network via their existing `networks` option
|
|
11639
|
+
* (e.g. `networks: ["app.network"]`).
|
|
11640
|
+
*
|
|
11641
|
+
* When the change lands and a network with the same name is already live on
|
|
11642
|
+
* the host, the result carries an advisory detail: Podman does not recreate
|
|
11643
|
+
* a running network, so changed `subnet`/`gateway`/… only take effect after
|
|
11644
|
+
* the network is removed.
|
|
11645
|
+
*
|
|
11646
|
+
* @param options - Configuration for the Quadlet network definition.
|
|
11647
|
+
* @returns A Module that ensures the Quadlet `.network` file is present and up to date.
|
|
11648
|
+
*/
|
|
11649
|
+
network(options) {
|
|
11650
|
+
validateQuadletName(options.name);
|
|
11651
|
+
const filePath = getQuadletNetworkFilePath(options.name);
|
|
11652
|
+
const content = generateNetworkQuadlet(options);
|
|
11653
|
+
const reloadFlag = buildQuadletReloadFlag("network", options.name, content);
|
|
11654
|
+
return {
|
|
11655
|
+
async _applyDryRun(ssh2) {
|
|
11656
|
+
if (!ssh2) return { status: "changed" };
|
|
11657
|
+
const diff = await buildQuadletDryRunDiff(ssh2, filePath, content);
|
|
11658
|
+
if (diff != null) {
|
|
11659
|
+
const liveHint = await podmanNetworkExists(ssh2, options.name) ? quadletNetworkLiveHint(options.name) : void 0;
|
|
11660
|
+
return liveHint == null ? { diff, status: "changed" } : { _dryRunDetail: liveHint, diff, status: "changed" };
|
|
11661
|
+
}
|
|
11662
|
+
const flagPresent = await hasFlag(ssh2, reloadFlag.flagName);
|
|
11663
|
+
return flagPresent ? { status: "changed" } : { _dryRunDetail: "(dry-run, daemon-reload pending)", status: "changed" };
|
|
11664
|
+
},
|
|
11665
|
+
_dryRunDiffProducer: true,
|
|
11666
|
+
async apply(ssh2) {
|
|
11667
|
+
if (!ssh2) return failed(`[quadlet.network: ${options.name}] SSH connection is required`);
|
|
11668
|
+
const contentChanges = await buildQuadletDryRunDiff(ssh2, filePath, content) != null;
|
|
11669
|
+
const result = await applyQuadletFile({
|
|
11670
|
+
content,
|
|
11671
|
+
filePath,
|
|
11672
|
+
label: "quadlet.network",
|
|
11673
|
+
name: options.name,
|
|
11674
|
+
ssh: ssh2
|
|
11675
|
+
});
|
|
11676
|
+
if (result.status !== "changed") return result;
|
|
11677
|
+
const flagFailure = await setVersionedFlag(ssh2, reloadFlag.flagName, reloadFlag.flagPrefix);
|
|
11678
|
+
if (flagFailure) return flagFailure;
|
|
11679
|
+
if (contentChanges && await podmanNetworkExists(ssh2, options.name)) {
|
|
11680
|
+
return { ...result, detail: quadletNetworkLiveHint(options.name) };
|
|
11681
|
+
}
|
|
11682
|
+
return result;
|
|
11683
|
+
},
|
|
11684
|
+
async check(ssh2) {
|
|
11685
|
+
if (!ssh2) return NEEDS_APPLY;
|
|
11686
|
+
const fileResult = await checkQuadletFile({ content, filePath, ssh: ssh2 });
|
|
11687
|
+
if (fileResult !== "ok") return fileResult;
|
|
11688
|
+
return await hasFlag(ssh2, reloadFlag.flagName) ? "ok" : NEEDS_APPLY;
|
|
11689
|
+
},
|
|
11690
|
+
name: `quadlet.network: ${options.name}`
|
|
11691
|
+
};
|
|
11692
|
+
},
|
|
11564
11693
|
/**
|
|
11565
11694
|
* Pull the latest image for a Quadlet-managed container and restart the service
|
|
11566
11695
|
* only when the image changed.
|
package/dist/cli.js
CHANGED
|
@@ -6262,7 +6262,7 @@ async function runApplyCommand(file, options, run = runPlaybook) {
|
|
|
6262
6262
|
if (options.diff && !options.dryRun) {
|
|
6263
6263
|
throw new CliUsageError("--diff requires --dry-run");
|
|
6264
6264
|
}
|
|
6265
|
-
printCliHeader("0.
|
|
6265
|
+
printCliHeader("0.16.1-728d4e1");
|
|
6266
6266
|
const environmentOverrides = applyCliEnvironmentOverrides(options.env, {
|
|
6267
6267
|
firstRun: options.firstRun
|
|
6268
6268
|
});
|
|
@@ -6300,7 +6300,7 @@ function renderSubcommandOptionsHelp(command) {
|
|
|
6300
6300
|
return ["", `Options for "paratix ${command.name()} <file>":`, ...rows].join("\n");
|
|
6301
6301
|
}
|
|
6302
6302
|
var program = new Command();
|
|
6303
|
-
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.
|
|
6303
|
+
program.name("paratix").description("Idempotent VPS setup tool in TypeScript").version("0.16.1-728d4e1");
|
|
6304
6304
|
var applyCommand = program.command("apply <file>").description("Apply a server definition").option(
|
|
6305
6305
|
"--diff",
|
|
6306
6306
|
"When combined with --dry-run, show a unified diff per module that would change.",
|
|
@@ -1017,7 +1017,7 @@ type PropertiesOptions = {
|
|
|
1017
1017
|
/**
|
|
1018
1018
|
* Set file or directory ownership and permissions on the remote host.
|
|
1019
1019
|
* Only the attributes specified in `options` are checked and applied. Drift
|
|
1020
|
-
* is detected via `stat -c '%a %U %G'` so `apply` only invokes the relevant
|
|
1020
|
+
* is detected via `stat -c '%a %U %G %u %g'` so `apply` only invokes the relevant
|
|
1021
1021
|
* `chmod`/`chown`/`chgrp` for fields that actually differ from the desired
|
|
1022
1022
|
* state. When all desired fields already match, `apply` returns `status: "ok"`
|
|
1023
1023
|
* instead of falsely reporting a change.
|
|
@@ -1466,6 +1466,23 @@ type QuadletImageUpdateOptions = {
|
|
|
1466
1466
|
serviceName?: string;
|
|
1467
1467
|
};
|
|
1468
1468
|
|
|
1469
|
+
type QuadletNetworkOptions = {
|
|
1470
|
+
description?: string;
|
|
1471
|
+
disableDns?: boolean;
|
|
1472
|
+
dns?: string[];
|
|
1473
|
+
driver?: string;
|
|
1474
|
+
gateway?: string;
|
|
1475
|
+
internal?: boolean;
|
|
1476
|
+
ipamDriver?: string;
|
|
1477
|
+
ipRange?: string;
|
|
1478
|
+
ipv6?: boolean;
|
|
1479
|
+
label?: Record<string, string>;
|
|
1480
|
+
name: string;
|
|
1481
|
+
options?: Record<string, string>;
|
|
1482
|
+
podmanArgs?: string[];
|
|
1483
|
+
subnet?: string;
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1469
1486
|
/**
|
|
1470
1487
|
* Modules for managing Podman Quadlet definitions.
|
|
1471
1488
|
*
|
|
@@ -1484,6 +1501,24 @@ declare const quadlet: {
|
|
|
1484
1501
|
* @returns A Module that ensures the Quadlet file is present and up to date.
|
|
1485
1502
|
*/
|
|
1486
1503
|
container(options: QuadletContainerOptions): Module;
|
|
1504
|
+
/**
|
|
1505
|
+
* Write a Podman Quadlet `.network` definition and reload systemd when it changes.
|
|
1506
|
+
*
|
|
1507
|
+
* Mirrors {@link quadlet.container}: the unit is written atomically to
|
|
1508
|
+
* `/etc/containers/systemd/<name>.network`, symlinks are refused, and
|
|
1509
|
+
* `systemctl daemon-reload` runs when the content changes. Containers
|
|
1510
|
+
* reference the network via their existing `networks` option
|
|
1511
|
+
* (e.g. `networks: ["app.network"]`).
|
|
1512
|
+
*
|
|
1513
|
+
* When the change lands and a network with the same name is already live on
|
|
1514
|
+
* the host, the result carries an advisory detail: Podman does not recreate
|
|
1515
|
+
* a running network, so changed `subnet`/`gateway`/… only take effect after
|
|
1516
|
+
* the network is removed.
|
|
1517
|
+
*
|
|
1518
|
+
* @param options - Configuration for the Quadlet network definition.
|
|
1519
|
+
* @returns A Module that ensures the Quadlet `.network` file is present and up to date.
|
|
1520
|
+
*/
|
|
1521
|
+
network(options: QuadletNetworkOptions): Module;
|
|
1487
1522
|
/**
|
|
1488
1523
|
* Pull the latest image for a Quadlet-managed container and restart the service
|
|
1489
1524
|
* only when the image changed.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-
|
|
2
|
-
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-
|
|
1
|
+
import { E as Environment, M as Module, a as ModuleMetaEntry, b as MetaEnvironmentValue, c as EnvironmentMetaEntry, S as SshdPortMetaEntry, d as SystemHostMetaEntry, e as SystemRebootMetaEntry, f as ModuleResult, g as ExecResult, h as ModuleStatus, i as SshConnection, O as OrchestrationStep, j as ShutdownSignal, k as ServerDefinition } from './index-jbXRUlJ6.js';
|
|
2
|
+
export { l as EnvironmentValue, m as ExecOptions, N as NEEDS_APPLY, n as SshConfig, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from './index-jbXRUlJ6.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fail the run if a condition on the current env is not satisfied.
|
package/dist/index.js
CHANGED
package/dist/modules/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { V as PackageSpec, W as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-
|
|
1
|
+
export { V as PackageSpec, W as UpgradeOptions, o as apt, p as archive, q as command, r as compose, s as cron, t as download, u as file, v as git, w as group, x as hostname, y as mount, z as net, A as op, B as package, C as quadlet, D as releaseUpgrade, F as rsync, G as script, H as service, I as ssh, J as sshd, K as swap, L as sysctl, P as system, Q as systemd, R as timer, T as ufw, U as user } from '../index-jbXRUlJ6.js';
|
package/dist/modules/index.js
CHANGED
package/llm-guide.md
CHANGED
|
@@ -186,6 +186,8 @@ the idiomatic way to ensure a previously installed cron job is gone.
|
|
|
186
186
|
| `compose.restart` | `(options: { projectDirectory: string; runtime?: "docker" \| "podman" }): Module` | No |
|
|
187
187
|
| `compose.systemd` | `(options: { detached?: boolean; name?: string; projectDirectory: string; runtime?: "docker" \| "podman" }): Module` | Yes |
|
|
188
188
|
|
|
189
|
+
> **`compose.pull` change detection:** `compose.pull` compares image digests before and after the pull to report `ok` (nothing new) vs `changed`. This needs `compose config --format json`. With `runtime: "podman"`, when the compose provider is the Python `podman-compose` (which does not support `--format json`), digest-based detection is unavailable: the pull still runs, but always reports `changed`. The Docker and Compose v2 paths are unaffected.
|
|
190
|
+
|
|
189
191
|
### `download`
|
|
190
192
|
|
|
191
193
|
| Method | Signature | Idempotent |
|
|
@@ -222,6 +224,15 @@ very slow artifact hosts or large downloads.
|
|
|
222
224
|
| `file.stat` | `(remotePath: string): Module` | No (always-applies) |
|
|
223
225
|
| `file.template` | `(remotePath: string, templatePath: string, options?: { mode?: string; owner?: string; strict?: boolean }): Module` | Yes |
|
|
224
226
|
|
|
227
|
+
**Note on `owner` and `group` values:** Every ownership option accepts either a POSIX name or a
|
|
228
|
+
numeric id — `owner: "www-data:www-data"` and `owner: "65532:65532"` are both valid, and the two
|
|
229
|
+
components may be mixed (`owner: "root:65532"`). Numeric ids are the only option for accounts that
|
|
230
|
+
have no passwd or group entry on the target host, such as `65532` in distroless images or `70` in
|
|
231
|
+
the postgres image. Drift detection compares the declared value against both the name and the id
|
|
232
|
+
reported by the host, so a numerically declared owner converges to `status: "ok"` like a named one.
|
|
233
|
+
`file.properties` keeps `owner` and `group` as separate options and therefore rejects a combined
|
|
234
|
+
`"user:group"` spec.
|
|
235
|
+
|
|
225
236
|
**Note on `file.copy` and the default mode:** When `options.mode` is omitted, `file.copy` sets the mode to the documented default of `0644`. The mode is applied during upload (`uploadFile` with `{ mode }`) and compared with the desired value in `check`, so subsequent runs detect mode drift (for example, a manual `chmod 0600`) as `needs-apply`. Pass `{ mode: "0600" }` explicitly when more restrictive permissions are required, such as for secrets.
|
|
226
237
|
|
|
227
238
|
**Note on `file.line`:** The `line` value must be a single line without CR/LF characters. Use `file.block` for multiline content.
|
|
@@ -360,6 +371,7 @@ Whitespace und Shell-Metazeichen werden abgelehnt. `package.absent` ist rein nam
|
|
|
360
371
|
| Method | Signature | Idempotent |
|
|
361
372
|
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
362
373
|
| `quadlet.container` | `(options: { addCapability?: string[]; addDevice?: string[]; annotation?: Record<string, string>; autoUpdate?: "local" \| "registry"; containerName?: string; description?: string; dns?: string[]; dnsOption?: string[]; dnsSearch?: string[]; dropCapability?: string[]; entrypoint?: string[]; environment?: Record<string, string>; environmentFiles?: string[]; exec?: string[]; exposeHostPort?: string[]; groupAdd?: string[]; healthCmd?: string; healthInterval?: string; healthOnFailure?: "kill" \| "none" \| "restart" \| "stop"; healthRetries?: number; healthStartPeriod?: string; healthTimeout?: string; hostName?: string; image: string; ip?: string; ip6?: string; label?: Record<string, string>; logDriver?: string; mask?: string[]; mount?: string[]; name: string; networks?: string[]; noNewPrivileges?: boolean; notify?: boolean; podmanArgs?: string[]; publishPorts?: string[]; pull?: "always" \| "missing" \| "never" \| "newer"; readOnly?: boolean; restart?: "always" \| "no" \| "on-abnormal" \| "on-abort" \| "on-failure" \| "on-success" \| "on-watchdog"; runInit?: boolean; secret?: string[]; seccompProfile?: string; securityLabelDisable?: boolean; securityLabelType?: string; stopTimeout?: number; sysctl?: Record<string, string>; timeoutStartSec?: number; timeoutStopSec?: number; timezone?: string; tmpfs?: string[]; ulimit?: string[]; unmask?: string[]; user?: string; userNs?: string; volumes?: string[]; wantedBy?: string; workingDir?: string }): Module` | Yes |
|
|
374
|
+
| `quadlet.network` | `(options: { description?: string; disableDns?: boolean; dns?: string[]; driver?: string; gateway?: string; internal?: boolean; ipRange?: string; ipamDriver?: string; ipv6?: boolean; label?: Record<string, string>; name: string; options?: Record<string, string>; podmanArgs?: string[]; subnet?: string }): Module` — writes `/etc/containers/systemd/<name>.network`; reference it from a container via `networks: ["<name>.network"]`. When the change lands and a network with the same name is already live, `changed` carries an advisory detail that the running network is not recreated until removed | Yes |
|
|
363
375
|
| `quadlet.updateImage` | `(options: { authFile?: string; image: string; name: string; serviceName?: string }): Module` — returns `changed` with the new registry digest (or local image ID fallback) in parentheses when a newer image was pulled | Partial |
|
|
364
376
|
|
|
365
377
|
### `releaseUpgrade`
|
|
@@ -803,20 +815,21 @@ Rules:
|
|
|
803
815
|
|
|
804
816
|
Diff-producing built-in modules:
|
|
805
817
|
|
|
806
|
-
| Module | Diff content
|
|
807
|
-
| ------------------------------------------- |
|
|
808
|
-
| `file.copy` | Unified diff between the remote file and the local source.
|
|
809
|
-
| `file.template` | Unified diff between the remote file and the rendered template.
|
|
810
|
-
| `sysctl.set` | `key = old → new` for the desired configuration.
|
|
811
|
-
| `hostname.set` | `hostname = old → new`.
|
|
812
|
-
| `swap.file` | Diff of the `/etc/fstab` entry (or removal of the line for `state: "absent"`).
|
|
813
|
-
| `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`).
|
|
814
|
-
| `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`).
|
|
815
|
-
| `cron.job` / `cron.absent` | Unified diff of the target user's crontab.
|
|
816
|
-
| `timer.scheduled` (present) | Concatenated diffs of the `.service` and `.timer` unit files.
|
|
817
|
-
| `timer.scheduled` (absent) / `timer.absent` | List of unit files to remove.
|
|
818
|
-
| `net.hosts` | Unified diff of `/etc/hosts`.
|
|
819
|
-
| `quadlet.container` | Unified diff of the `.container` unit file. If the file already matches but the `daemon-reload` flag is missing, `(dry-run, daemon-reload pending)` also appears as a detail suffix.
|
|
818
|
+
| Module | Diff content |
|
|
819
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
820
|
+
| `file.copy` | Unified diff between the remote file and the local source. |
|
|
821
|
+
| `file.template` | Unified diff between the remote file and the rendered template. |
|
|
822
|
+
| `sysctl.set` | `key = old → new` for the desired configuration. |
|
|
823
|
+
| `hostname.set` | `hostname = old → new`. |
|
|
824
|
+
| `swap.file` | Diff of the `/etc/fstab` entry (or removal of the line for `state: "absent"`). |
|
|
825
|
+
| `swap.swappiness` | `vm.swappiness = old → new` (via `sysctl.set`). |
|
|
826
|
+
| `swap.vfsCachePressure` | `vm.vfs_cache_pressure = old → new` (via `sysctl.set`). |
|
|
827
|
+
| `cron.job` / `cron.absent` | Unified diff of the target user's crontab. |
|
|
828
|
+
| `timer.scheduled` (present) | Concatenated diffs of the `.service` and `.timer` unit files. |
|
|
829
|
+
| `timer.scheduled` (absent) / `timer.absent` | List of unit files to remove. |
|
|
830
|
+
| `net.hosts` | Unified diff of `/etc/hosts`. |
|
|
831
|
+
| `quadlet.container` | Unified diff of the `.container` unit file. If the file already matches but the `daemon-reload` flag is missing, `(dry-run, daemon-reload pending)` also appears as a detail suffix. |
|
|
832
|
+
| `quadlet.network` | Unified diff of the `.network` unit file. If the file already matches but the `daemon-reload` flag is missing, `(dry-run, daemon-reload pending)` also appears as a detail suffix. When a network with the same name is already live, the advisory hint is appended too. |
|
|
820
833
|
|
|
821
834
|
### Implementing a Diff for a Custom Module
|
|
822
835
|
|