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 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-MHPFGCEY.js";
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/quadlet.ts
3363
+ // src/modules/quadletHelpers.ts
3364
3364
  var CONTAINERS_SYSTEMD_DIRECTORY = "/etc/containers/systemd";
3365
- var QUADLET_FILE_MODE = "0644";
3366
- var SYSTEMCTL = "systemctl";
3367
- var UNIT_NAME_PATTERN2 = new RegExp("^[\\w@.\\-]+$", "v");
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 maybeRenderQuadletLine(key, value, transform) {
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, transform == null ? value : transform(value));
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 buildQuadletUnitSection(options) {
3396
- return renderQuadletSection("Unit", [
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 buildQuadletContainerLines(options) {
3403
- return compactQuadletLines([
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("Restart", options.restart),
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("PodmanArgs", options.podmanArgs ?? []),
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
- ...renderQuadletEnvironment(options.environment ?? {})
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
- return [
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
- ].join("\n").trimEnd();
3556
+ ];
3557
+ return sections.join("\n").trimEnd();
3427
3558
  }
3428
3559
  async function createQuadletDirectory(ssh2) {
3429
- return ssh2.exec(`mkdir -p ${shellQuote(CONTAINERS_SYSTEMD_DIRECTORY)}`, {
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 = `${CONTAINERS_SYSTEMD_DIRECTORY}/${options.name}.container`;
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-LI47NIKN.js.map
5745
+ //# sourceMappingURL=chunk-ENWMSERJ.js.map