podman: quote Quadlet labels with spaces

Quote generated Quadlet key-value entries when attrset-backed values contain whitespace, preserving the value as a single systemd/Quadlet field.

Without quoting, label values such as Traefik rules are split by podman-system-generator into multiple invalid --label arguments.

Extend the container NMT fixture with a label containing spaces and assert the generated service preserves it as one label.
This commit is contained in:
Austin Horstman 2026-05-14 20:21:15 -05:00 committed by Robert Helgesson
parent 92a8736142
commit f63af17f47
3 changed files with 22 additions and 7 deletions

View file

@ -15,17 +15,28 @@ let
normalizeKeyValue =
k: v:
let
mkValueString = lib.generators.mkValueStringDefault { };
quoteIfSpaced =
s: if lib.match ".*[[:space:]].*" s == null then s else ''"${lib.escape [ "\\" "\"" ] s}"'';
normalizeAttrValue =
name: value:
lib.optionalString (value != null) (
if builtins.isAttrs value then
normalizeKeyValue name value
else
quoteIfSpaced "${name}=${mkValueString value}"
);
v' =
if builtins.isBool v then
(if v then "true" else "false")
else if builtins.isAttrs v then
if builtins.isAttrs v then
(concatStringsSep ''
${k}='' (mapAttrsToList normalizeKeyValue v))
${k}='' (mapAttrsToList normalizeAttrValue v))
else
toString v;
mkValueString v;
in
if isNull v then "" else "${k}=${v'}";
lib.optionalString (v != null) "${k}=${v'}";
primitiveAttrs = with types; attrsOf (either primitive (listOf primitive));
primitiveList = with types; listOf primitive;

View file

@ -14,6 +14,7 @@ Environment=VAL_B=2
Environment=VAL_C=false
Image=docker.io/alpine:latest
Label=nix.home-manager.managed=true
Label="traefik.rule=Host(`example.com`) && PathPrefix(`/api`)"
Network=mynet
NetworkAlias=test-alias-1
NetworkAlias=test-alias-2
@ -38,7 +39,7 @@ Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name my-container --replace --rm --cgroups=split --entrypoint=/sleep.sh --network-alias test-alias-1 --network-alias test-alias-2 --read-only-tmpfs --network mynet --sdnotify=conmon -d --device /dev/null:/dev/null -v /tmp:/tmp --label io.containers.autoupdate=registry --publish 8080:80 --env VAL_A=A --env VAL_B=2 --env VAL_C=false --label nix.home-manager.managed=true --security-opt=no-new-privileges docker.io/alpine:latest
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name my-container --replace --rm --cgroups=split --entrypoint=/sleep.sh --network-alias test-alias-1 --network-alias test-alias-2 --read-only-tmpfs --network mynet --sdnotify=conmon -d --device /dev/null:/dev/null -v /tmp:/tmp --label io.containers.autoupdate=registry --publish 8080:80 --env VAL_A=A --env VAL_B=2 --env VAL_C=false --label nix.home-manager.managed=true --label "traefik.rule=Host(`example.com`)\x20&&\x20PathPrefix(`/api`)" --security-opt=no-new-privileges docker.io/alpine:latest
[Unit]
Wants=podman-user-wait-network-online.service

View file

@ -27,6 +27,9 @@
Unit.Before = "fake.target";
};
image = "docker.io/alpine:latest";
labels = {
"traefik.rule" = "Host(`example.com`) && PathPrefix(`/api`)";
};
# Should not generate Requires/After for network because there is no
# services.podman.networks.mynet.
network = "mynet";