paratix 0.5.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 +2 -0
- package/dist/{chunk-LI47NIKN.js → chunk-ENWMSERJ.js} +212 -47
- package/dist/chunk-ENWMSERJ.js.map +1 -0
- package/dist/{chunk-3WK4QNJK.js → chunk-IUY5BJHA.js} +2 -2
- package/dist/{chunk-MHPFGCEY.js → chunk-JJRF37BP.js} +34 -1
- package/dist/chunk-JJRF37BP.js.map +1 -0
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +2 -2
- package/dist/{user-CraCJci7.d.ts → user-CiAMlpWO.d.ts} +64 -13
- package/llm-guide.md +4 -3
- package/package.json +1 -1
- package/dist/chunk-LI47NIKN.js.map +0 -1
- package/dist/chunk-MHPFGCEY.js.map +0 -1
- /package/dist/{chunk-3WK4QNJK.js.map → chunk-IUY5BJHA.js.map} +0 -0
package/README.md
CHANGED
|
@@ -104,6 +104,8 @@ Signals are deferred side effects such as `service.reload(...)` or `service.rest
|
|
|
104
104
|
|
|
105
105
|
For Podman-native services, Paratix now also includes `quadlet.container(...)`. It writes a `.container` file under `/etc/containers/systemd`, reloads systemd when the content changes, and works cleanly with `service.enabled(...)` and `service.running(...)` for the generated service.
|
|
106
106
|
|
|
107
|
+
When you need a targeted image refresh outside the normal deploy flow, `quadlet.updateImage(...)` pulls exactly one image, reuses existing Podman registry auth on the host, optionally supports `authFile`, and only restarts the generated service when the pull actually downloaded a newer image.
|
|
108
|
+
|
|
107
109
|
### Guards
|
|
108
110
|
|
|
109
111
|
Paratix also supports declarative host-state guards. Use `when.packageInstalled(...)`, `when.commandExists(...)`, `when.fileExists(...)`, `when.pathExists(...)`, `when.symlinkExists(...)`, or `when.socketExists(...)` and their inverted forms to gate modules or recipes on remote host state without shell-heavy playbooks.
|
|
@@ -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) {
|
|
@@ -3360,57 +3360,157 @@ var pkg = {
|
|
|
3360
3360
|
}
|
|
3361
3361
|
};
|
|
3362
3362
|
|
|
3363
|
-
// src/modules/
|
|
3363
|
+
// src/modules/quadletHelpers.ts
|
|
3364
3364
|
var CONTAINERS_SYSTEMD_DIRECTORY = "/etc/containers/systemd";
|
|
3365
|
-
var
|
|
3366
|
-
|
|
3367
|
-
|
|
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
|
+
];
|
|
3368
3373
|
function sanitizeQuadletValue(value) {
|
|
3369
3374
|
return value.replaceAll(new RegExp("[\\n\\r]", "gv"), "");
|
|
3370
3375
|
}
|
|
3371
3376
|
function renderQuadletLine(key, value) {
|
|
3372
3377
|
return `${key}=${sanitizeQuadletValue(value)}`;
|
|
3373
3378
|
}
|
|
3374
|
-
function renderQuadletSection(name, lines) {
|
|
3375
|
-
return [`[${name}]`, ...lines, ""].join("\n");
|
|
3376
|
-
}
|
|
3377
|
-
function validateQuadletName(name) {
|
|
3378
|
-
if (!UNIT_NAME_PATTERN2.test(name)) {
|
|
3379
|
-
throw new Error(`quadlet.container: name must match ${String(UNIT_NAME_PATTERN2)}, got: ${name}`);
|
|
3380
|
-
}
|
|
3381
|
-
}
|
|
3382
|
-
function renderQuadletEnvironment(environment) {
|
|
3383
|
-
return Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => renderQuadletLine("Environment", `${key}=${value}`));
|
|
3384
|
-
}
|
|
3385
3379
|
function renderQuadletRepeated(key, values) {
|
|
3386
3380
|
return values.map((value) => renderQuadletLine(key, value));
|
|
3387
3381
|
}
|
|
3388
|
-
function
|
|
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) {
|
|
3389
3386
|
if (value == null || value === "") return null;
|
|
3390
|
-
return renderQuadletLine(key,
|
|
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));
|
|
3391
3396
|
}
|
|
3392
3397
|
function compactQuadletLines(lines) {
|
|
3393
3398
|
return lines.filter((line) => line != null);
|
|
3394
3399
|
}
|
|
3395
|
-
function
|
|
3396
|
-
return
|
|
3397
|
-
renderQuadletLine("Description", options.description ?? `Podman container: ${options.name}`),
|
|
3398
|
-
"Wants=network-online.target",
|
|
3399
|
-
"After=network-online.target"
|
|
3400
|
-
]);
|
|
3400
|
+
function renderQuadletSection(name, lines) {
|
|
3401
|
+
return [`[${name}]`, ...lines, ""].join("\n");
|
|
3401
3402
|
}
|
|
3402
|
-
function
|
|
3403
|
-
return
|
|
3403
|
+
function buildQuadletIdentityLines(options) {
|
|
3404
|
+
return [
|
|
3404
3405
|
renderQuadletLine("Image", options.image),
|
|
3405
3406
|
maybeRenderQuadletLine("ContainerName", options.containerName),
|
|
3406
3407
|
maybeRenderQuadletLine("AutoUpdate", options.autoUpdate),
|
|
3408
|
+
maybeRenderQuadletLine("Pull", options.pull),
|
|
3409
|
+
maybeRenderQuadletLine("Entrypoint", options.entrypoint?.join(" ")),
|
|
3407
3410
|
maybeRenderQuadletLine("Exec", options.exec?.join(" ")),
|
|
3408
|
-
maybeRenderQuadletLine("
|
|
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),
|
|
3409
3419
|
...renderQuadletRepeated("Network", options.networks ?? []),
|
|
3410
|
-
...renderQuadletRepeated("
|
|
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 [
|
|
3411
3449
|
...renderQuadletRepeated("PublishPort", options.publishPorts ?? []),
|
|
3450
|
+
...renderQuadletRepeated("ExposeHostPort", options.exposeHostPort ?? []),
|
|
3412
3451
|
...renderQuadletRepeated("Volume", options.volumes ?? []),
|
|
3413
|
-
...
|
|
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"
|
|
3414
3514
|
]);
|
|
3415
3515
|
}
|
|
3416
3516
|
function buildQuadletInstallSection(options) {
|
|
@@ -3418,15 +3518,46 @@ function buildQuadletInstallSection(options) {
|
|
|
3418
3518
|
renderQuadletLine("WantedBy", options.wantedBy ?? "multi-user.target")
|
|
3419
3519
|
]);
|
|
3420
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";
|
|
3421
3549
|
function generateContainerQuadlet(options) {
|
|
3422
|
-
|
|
3550
|
+
const serviceLines = buildQuadletServiceLines(options);
|
|
3551
|
+
const sections = [
|
|
3423
3552
|
buildQuadletUnitSection(options),
|
|
3424
3553
|
renderQuadletSection("Container", buildQuadletContainerLines(options)),
|
|
3554
|
+
...serviceLines.length > 0 ? [renderQuadletSection("Service", serviceLines)] : [],
|
|
3425
3555
|
buildQuadletInstallSection(options)
|
|
3426
|
-
]
|
|
3556
|
+
];
|
|
3557
|
+
return sections.join("\n").trimEnd();
|
|
3427
3558
|
}
|
|
3428
3559
|
async function createQuadletDirectory(ssh2) {
|
|
3429
|
-
return ssh2.exec(
|
|
3560
|
+
return ssh2.exec(CONTAINERS_SYSTEMD_DIRECTORY_COMMAND, {
|
|
3430
3561
|
ignoreExitCode: true,
|
|
3431
3562
|
silent: true
|
|
3432
3563
|
});
|
|
@@ -3465,24 +3596,11 @@ var quadlet = {
|
|
|
3465
3596
|
* and `service.running(name)`.
|
|
3466
3597
|
*
|
|
3467
3598
|
* @param options - Configuration for the Quadlet container definition.
|
|
3468
|
-
* @param options.name - Quadlet base name without `.container`.
|
|
3469
|
-
* @param options.image - Container image reference.
|
|
3470
|
-
* @param options.description - Optional systemd unit description.
|
|
3471
|
-
* @param options.containerName - Optional explicit Podman container name.
|
|
3472
|
-
* @param options.autoUpdate - Optional Podman auto-update policy.
|
|
3473
|
-
* @param options.environment - Optional environment variables.
|
|
3474
|
-
* @param options.exec - Optional command and arguments for `Exec=`.
|
|
3475
|
-
* @param options.networks - Optional `Network=` entries.
|
|
3476
|
-
* @param options.podmanArgs - Optional `PodmanArgs=` entries.
|
|
3477
|
-
* @param options.publishPorts - Optional `PublishPort=` entries.
|
|
3478
|
-
* @param options.restart - Optional `Restart=` policy in the `[Container]` section.
|
|
3479
|
-
* @param options.volumes - Optional `Volume=` entries.
|
|
3480
|
-
* @param options.wantedBy - Optional install target. Defaults to `multi-user.target`.
|
|
3481
3599
|
* @returns A Module that ensures the Quadlet file is present and up to date.
|
|
3482
3600
|
*/
|
|
3483
3601
|
container(options) {
|
|
3484
3602
|
validateQuadletName(options.name);
|
|
3485
|
-
const filePath =
|
|
3603
|
+
const filePath = getQuadletContainerFilePath(options.name);
|
|
3486
3604
|
const content = generateContainerQuadlet(options);
|
|
3487
3605
|
return {
|
|
3488
3606
|
async apply(ssh2) {
|
|
@@ -3495,6 +3613,53 @@ var quadlet = {
|
|
|
3495
3613
|
},
|
|
3496
3614
|
name: `quadlet.container: ${options.name}`
|
|
3497
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
|
+
};
|
|
3498
3663
|
}
|
|
3499
3664
|
};
|
|
3500
3665
|
|
|
@@ -5577,4 +5742,4 @@ export {
|
|
|
5577
5742
|
ufw,
|
|
5578
5743
|
user
|
|
5579
5744
|
};
|
|
5580
|
-
//# sourceMappingURL=chunk-
|
|
5745
|
+
//# sourceMappingURL=chunk-ENWMSERJ.js.map
|