From ce9cb2496c48ebb33e42ee0ab82267f67b82f71e Mon Sep 17 00:00:00 2001 From: bamhm182 Date: Mon, 10 Mar 2025 00:02:05 -0400 Subject: [PATCH] podman: added volume, image, and build quadlets (#6137) Added support for build, image, and volume quadlets Resolved test failures due to podman 5.3.0 upgrade Replaced several instances of pkgs.podman with services.podman.package --- modules/services/podman-linux/activation.nix | 27 +-- modules/services/podman-linux/builds.nix | 168 ++++++++++++++++ modules/services/podman-linux/containers.nix | 75 +++++-- modules/services/podman-linux/default.nix | 11 +- modules/services/podman-linux/images.nix | 162 +++++++++++++++ .../services/podman-linux/install-quadlet.nix | 2 +- modules/services/podman-linux/networks.nix | 12 +- modules/services/podman-linux/podman-lib.nix | 29 ++- modules/services/podman-linux/services.nix | 4 +- modules/services/podman-linux/volumes.nix | 186 ++++++++++++++++++ .../podman-linux/build-expected.service | 30 +++ tests/modules/services/podman-linux/build.nix | 44 +++++ .../podman-linux/container-expected.service | 1 - .../modules/services/podman-linux/default.nix | 3 + .../podman-linux/image-expected.service | 28 +++ tests/modules/services/podman-linux/image.nix | 18 ++ .../integration-build-expected.service | 30 +++ ...integration-container-bld-expected.service | 38 ++++ .../integration-container-expected.service | 8 +- .../integration-image-expected.service | 28 +++ .../integration-volume-expected.service | 33 ++++ .../services/podman-linux/integration.nix | 46 ++++- .../podman-linux/volume-expected.service | 36 ++++ .../modules/services/podman-linux/volume.nix | 35 ++++ 24 files changed, 1000 insertions(+), 54 deletions(-) create mode 100644 modules/services/podman-linux/builds.nix create mode 100644 modules/services/podman-linux/images.nix create mode 100644 modules/services/podman-linux/volumes.nix create mode 100644 tests/modules/services/podman-linux/build-expected.service create mode 100644 tests/modules/services/podman-linux/build.nix create mode 100644 tests/modules/services/podman-linux/image-expected.service create mode 100644 tests/modules/services/podman-linux/image.nix create mode 100644 tests/modules/services/podman-linux/integration-build-expected.service create mode 100644 tests/modules/services/podman-linux/integration-container-bld-expected.service create mode 100644 tests/modules/services/podman-linux/integration-image-expected.service create mode 100644 tests/modules/services/podman-linux/integration-volume-expected.service create mode 100644 tests/modules/services/podman-linux/volume-expected.service create mode 100644 tests/modules/services/podman-linux/volume.nix diff --git a/modules/services/podman-linux/activation.nix b/modules/services/podman-linux/activation.nix index 5791f19b0..205b6651e 100644 --- a/modules/services/podman-linux/activation.nix +++ b/modules/services/podman-linux/activation.nix @@ -18,6 +18,7 @@ local manifestFile="${config.xdg.configHome}/podman/$2" local extraListCommands="''${3:-}" [[ $resourceType = "container" ]] && extraListCommands+=" -a" + [[ $resourceType = "volume" ]] && extraListCommands+=" --filter label=nix.home-manager.preserve=false" [ ! -f "$manifestFile" ] && VERBOSE_ENABLED && echo "Manifest does not exist: $manifestFile" && return 0 @@ -27,6 +28,7 @@ formatString="{{.Name}}" [[ $resourceType = "container" ]] && formatString="{{.Names}}" + [[ $resourceType = "image" ]] && formatString="{{.Repository}}" local listOutput=$(${config.services.podman.package}/bin/podman $resourceType ls $extraListCommands --filter 'label=nix.home-manager.managed=true' --format "$formatString") @@ -36,12 +38,12 @@ VERBOSE_ENABLED && echo "No ''${resourceType}s available to process." || true else for resource in "''${podmanResources[@]}"; do - if ! isResourceInManifest "$resource"; then - removeResource "$resourceType" "$resource" - else - VERBOSE_ENABLED && echo "Keeping managed $resourceType: $resource" || true - fi - done + if ! isResourceInManifest "$resource"; then + removeResource "$resourceType" "$resource" + else + VERBOSE_ENABLED && echo "Keeping managed $resourceType: $resource" || true + fi + done fi } @@ -69,19 +71,20 @@ commands=() case "$resourceType" in "container") - commands+="${config.services.podman.package}/bin/podman $resourceType stop $resource" - commands+="${config.services.podman.package}/bin/podman $resourceType rm -f $resource" + commands+=("${config.services.podman.package}/bin/podman $resourceType stop $resource") + commands+=("${config.services.podman.package}/bin/podman $resourceType rm -f $resource") ;; - "network") - commands+="${config.services.podman.package}/bin/podman $resourceType rm $resource" + "image" | "network" | "volume") + commands+=("${config.services.podman.package}/bin/podman $resourceType rm $resource") ;; esac for command in "''${commands[@]}"; do command=$(echo $command | tr -d ';&|`') DRYRUN_ENABLED && echo "Would run: $command" && continue || true VERBOSE_ENABLED && echo "Running: $command" || true - if [[ "$(eval "$command")" != "$resource" ]]; then + if [[ "$(eval "$command")" != *"$resource" ]]; then echo -e "\tCommand failed: ''${command}" + [ "$resourceType" == "image" ] && resourceType="ancestor" usedByContainers=$(${config.services.podman.package}/bin/podman container ls -a --filter "$resourceType=$resource" --format "{{.Names}}") echo -e "\t$resource in use by containers: $usedByContainers" fi @@ -92,7 +95,7 @@ [[ "$@" == *"--verbose"* ]] && VERBOSE="true" [[ "$@" == *"--dry-run"* ]] && DRY_RUN="true" - for type in "container" "network"; do + for type in "container" "image" "network" "volume"; do cleanup "$type" "''${type}s.manifest" done ''; diff --git a/modules/services/podman-linux/builds.nix b/modules/services/podman-linux/builds.nix new file mode 100644 index 000000000..937a206b0 --- /dev/null +++ b/modules/services/podman-linux/builds.nix @@ -0,0 +1,168 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.podman; + + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; + + createQuadletSource = name: buildDef: + let + buildConfig = podman-lib.deepMerge { + Build = { + AuthFile = buildDef.authFile; + Environment = buildDef.environment; + File = buildDef.file; + ImageTag = [ "homemanager/${name}" ] ++ buildDef.tags; + Label = buildDef.labels // { "nix.home-manager.managed" = true; }; + PodmanArgs = buildDef.extraPodmanArgs; + SetWorkingDirectory = buildDef.workingDirectory; + TLSVerify = buildDef.tlsVerify; + }; + Install = { + WantedBy = optionals buildDef.autoStart [ + "default.target" + "multi-user.target" + ]; + }; + Service = { + TimeoutStartSec = 300; + RemainAfterExit = "yes"; + }; + Unit = { Description = buildDef.description; }; + } buildDef.extraConfig; + in '' + # Automatically generated by home-manager for podman build configuration + # DO NOT EDIT THIS FILE DIRECTLY + # + # ${name}.build + ${podman-lib.toQuadletIni buildConfig} + ''; + + toQuadletInternal = name: buildDef: { + assertions = podman-lib.buildConfigAsserts name buildDef.extraConfig; + serviceName = + "podman-${name}"; # quadlet service name: 'podman--build.service + source = podman-lib.removeBlankLines (createQuadletSource name buildDef); + resourceType = "build"; + }; +in let + buildDefinitionType = types.submodule ({ name, ... }: { + options = { + + autoStart = mkOption { + type = types.bool; + default = true; + description = + "Whether to start the build on boot. Requires user lingering."; + }; + + authFile = mkOption { + type = with types; nullOr path; + default = null; + description = "Path of the authentication file."; + }; + + description = mkOption { + type = with types; nullOr str; + default = "Service for build ${name}"; + defaultText = "Service for build \${name}"; + example = "My Build"; + description = "The description of the build."; + }; + + environment = mkOption { + type = podman-lib.primitiveAttrs; + default = { }; + example = literalExpression '' + { + VAR1 = "0:100"; + VAR2 = true; + VAR3 = 5; + } + ''; + description = "Environment variables to set in the build."; + }; + + extraConfig = mkOption { + type = podman-lib.extraConfigType; + default = { }; + example = literalExpression '' + { + Build = { + Arch = "aarch64"; + }; + Service = { + TimeoutStartSec = 15; + }; + } + ''; + description = "INI sections and values to populate the Build Quadlet."; + }; + + extraPodmanArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "--retries 5" ]; + description = "Extra arguments to pass to the podman build command."; + }; + + file = mkOption { + type = types.str; + example = literalExpression '' + `"xdg.configFile."containerfiles/my-img/Containerfile"` + or + `"https://github.com/.../my-img/Containerfile"` + ''; + description = + "Path to a Containerfile which contains instructions to build the image."; + }; + + tags = mkOption { + type = with types; listOf str; + default = [ ]; + description = '' + Name associated with the build. + First tag will always be "homemanager/". + ''; + }; + + labels = mkOption { + type = with types; attrsOf str; + default = { }; + example = { + app = "myapp"; + some-label = "somelabel"; + }; + description = "The labels to apply to the build."; + }; + + tlsVerify = mkOption { + type = types.bool; + default = true; + description = + "Require HTTPS and verification of certificates when contacting registries."; + }; + + workingDirectory = mkOption { + type = with types; nullOr path; + default = null; + description = "WorkingDirectory of the systemd unit file."; + }; + }; + }); +in { + options.services.podman.builds = mkOption { + type = types.attrsOf buildDefinitionType; + default = { }; + description = "Defines Podman build quadlet configurations."; + }; + + config = let buildQuadlets = mapAttrsToList toQuadletInternal cfg.builds; + in mkIf cfg.enable { + services.podman.internal.quadletDefinitions = buildQuadlets; + assertions = flatten (map (build: build.assertions) buildQuadlets); + + xdg.configFile."podman/images.manifest".text = + podman-lib.generateManifestText buildQuadlets; + }; +} diff --git a/modules/services/podman-linux/containers.nix b/modules/services/podman-linux/containers.nix index 41ab29130..d5836119a 100644 --- a/modules/services/podman-linux/containers.nix +++ b/modules/services/podman-linux/containers.nix @@ -5,24 +5,60 @@ with lib; let cfg = config.services.podman; - podman-lib = import ./podman-lib.nix { inherit lib config; }; + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; createQuadletSource = name: containerDef: let - mapHmNetworks = network: - if builtins.hasAttr network cfg.networks then - "podman-${network}-network.service" - else - null; + formatServiceNameForType = type: name: + { + image = "podman-${name}-image.service"; + build = "podman-${name}-build.service"; + network = "podman-${name}-network.service"; + volume = "podman-${name}-volume.service"; + }."${type}"; - finalConfig = let - managedNetworks = if lib.isList containerDef.network then - map mapHmNetworks containerDef.network - else if containerDef.network != null then - map mapHmNetworks [ containerDef.network ] + dependencyByHomeManagerQuadlet = type: name: + let + definitionsOfType = + filter (q: q.resourceType == type) cfg.internal.quadletDefinitions; + matchingName = + filter (q: q.serviceName == "podman-${name}") definitionsOfType; + in if ((length matchingName) == 1) then + [ (formatServiceNameForType type name) ] else [ ]; - in (podman-lib.deepMerge { + + forEachValue = type: value: + let resolve = v: dependencyByHomeManagerQuadlet type v; + in if isList value then + concatLists (map resolve value) + else + resolve value; + + withResolverFor = type: value: + { + "image" = forEachValue "image" value; + "build" = forEachValue "build" value; + "network" = forEachValue "network" value; + "volume" = let + a = if isList value then value else [ value ]; + volumes = map (v: elemAt (splitString ":" v) 0) a; + in forEachValue "volume" volumes; + }.${type}; + + dependencyServices = (withResolverFor "image" containerDef.image) + ++ (withResolverFor "build" containerDef.image) + ++ (withResolverFor "network" containerDef.network) + ++ (withResolverFor "volume" containerDef.volumes); + + resolvedImage = if (builtins.hasAttr containerDef.image cfg.images) then + cfg.images."${containerDef.image}".image + else if (builtins.hasAttr containerDef.image cfg.builds) then + "localhost/homemanager/${containerDef.image}" + else + containerDef.image; + + quadlet = (podman-lib.deepMerge { Container = { AddCapability = containerDef.addCapabilities; AddDevice = containerDef.devices; @@ -34,7 +70,7 @@ let EnvironmentFile = containerDef.environmentFile; Exec = containerDef.exec; Group = containerDef.group; - Image = containerDef.image; + Image = resolvedImage; IP = containerDef.ip4; IP6 = containerDef.ip6; Label = @@ -48,11 +84,10 @@ let Volume = containerDef.volumes; }; Install = { - WantedBy = (if containerDef.autoStart then [ + WantedBy = optionals containerDef.autoStart [ "default.target" "multi-user.target" - ] else - [ ]); + ]; }; Service = { Environment = { @@ -66,8 +101,8 @@ let TimeoutStopSec = 30; }; Unit = { - After = [ "network.target" ] ++ managedNetworks; - Requires = managedNetworks; + After = dependencyServices; + Requires = dependencyServices; Description = (if (builtins.isString containerDef.description) then containerDef.description else @@ -79,7 +114,7 @@ let # DO NOT EDIT THIS FILE DIRECTLY # # ${name}.container - ${podman-lib.toQuadletIni finalConfig} + ${podman-lib.toQuadletIni quadlet} ''; toQuadletInternal = name: containerDef: { @@ -307,7 +342,7 @@ in { flatten (map (container: container.assertions) containerQuadlets); # manifest file - home.file."${config.xdg.configHome}/podman/containers.manifest".text = + xdg.configFile."podman/containers.manifest".text = podman-lib.generateManifestText containerQuadlets; }; } diff --git a/modules/services/podman-linux/default.nix b/modules/services/podman-linux/default.nix index 459762d46..a08541a6d 100644 --- a/modules/services/podman-linux/default.nix +++ b/modules/services/podman-linux/default.nix @@ -5,8 +5,15 @@ let in { meta.maintainers = with lib.hm.maintainers; [ bamhm182 n-hass ]; - imports = - [ ./containers.nix ./install-quadlet.nix ./networks.nix ./services.nix ]; + imports = [ + ./builds.nix + ./containers.nix + ./images.nix + ./install-quadlet.nix + ./networks.nix + ./services.nix + ./volumes.nix + ]; options.services.podman = { enable = lib.mkEnableOption "Podman, a daemonless container engine"; diff --git a/modules/services/podman-linux/images.nix b/modules/services/podman-linux/images.nix new file mode 100644 index 000000000..51aa9f1be --- /dev/null +++ b/modules/services/podman-linux/images.nix @@ -0,0 +1,162 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.podman; + + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; + + createQuadletSource = name: imageDef: + let + credsString = + (if imageDef.username != null then imageDef.username else "") + + (if imageDef.password != null then ":${imageDef.password}" else ""); + + imageConfig = podman-lib.deepMerge { + Image = { + AuthFile = imageDef.authFile; + CertDir = imageDef.certDir; + Creds = (if credsString != "" then credsString else null); + DecryptionKey = imageDef.decryptionKeyFile; + Image = imageDef.image; + ImageTag = imageDef.tag; + PodmanArgs = imageDef.extraPodmanArgs; + TLSVerify = imageDef.tlsVerify; + }; + Install = { + WantedBy = optionals imageDef.autoStart [ + "default.target" + "multi-user.target" + ]; + }; + Service = { + ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ]; + TimeoutStartSec = 300; + RemainAfterExit = "yes"; + }; + Unit = { Description = imageDef.description; }; + } imageDef.extraConfig; + in '' + # Automatically generated by home-manager for podman image configuration + # DO NOT EDIT THIS FILE DIRECTLY + # + # ${name}.image + ${podman-lib.toQuadletIni imageConfig} + ''; + + toQuadletInternal = name: imageDef: { + assertions = podman-lib.buildConfigAsserts name imageDef.extraConfig; + serviceName = + "podman-${name}"; # quadlet service name: 'podman--image.service + source = podman-lib.removeBlankLines (createQuadletSource name imageDef); + resourceType = "image"; + }; +in let + imageDefinitionType = types.submodule ({ name, ... }: { + options = { + autoStart = mkOption { + type = types.bool; + default = true; + description = + "Whether to pull the image on boot. Requires user lingering."; + }; + + authFile = mkOption { + type = with types; nullOr path; + default = null; + description = + "Path of the authentication file used to connect to registry."; + }; + + certDir = mkOption { + type = with types; nullOr path; + default = null; + description = + "Path of certificates (*.{crt,cert,key}) used to connect to registry."; + }; + + decryptionKeyFile = mkOption { + type = with types; nullOr path; + default = null; + description = "Path to key used for decrpytion of images."; + }; + + description = mkOption { + type = with types; nullOr str; + default = "Service for image ${name}"; + defaultText = "Service for image \${name}"; + example = "My Image"; + description = "The description of the image."; + }; + + extraConfig = mkOption { + type = podman-lib.extraConfigType; + default = { }; + example = literalExpression '' + { + Image = { + ContainersConfModule = "/etc/nvd.conf"; + }; + } + ''; + description = "INI sections and values to populate the Image Quadlet."; + }; + + extraPodmanArgs = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "--os=linux" ]; + description = + "Extra arguments to pass to the podman image pull command."; + }; + + image = mkOption { + type = types.str; + example = "quay.io/centos/centos:latest"; + description = "Image to pull."; + }; + + password = mkOption { + type = with types; nullOr str; + default = null; + example = "P@ssw0rd"; + description = + "Password used to connect to registry. (Will be visible in nix store)"; + }; + + tag = mkOption { + type = with types; nullOr str; + default = null; + example = "quay.io/centos/centos:latest"; + description = + "FQIN of referenced Image when source is a file or directory archive."; + }; + + tlsVerify = mkOption { + type = types.bool; + default = true; + description = + "Require HTTPS and verification of certificates when contacting registries."; + }; + + username = mkOption { + type = with types; nullOr str; + default = null; + example = "bob"; + description = "Username used to connect to registry."; + }; + + }; + }); +in { + options.services.podman.images = mkOption { + type = types.attrsOf imageDefinitionType; + default = { }; + description = "Defines Podman image quadlet configurations."; + }; + + config = let imageQuadlets = mapAttrsToList toQuadletInternal cfg.images; + in mkIf cfg.enable { + services.podman.internal.quadletDefinitions = imageQuadlets; + assertions = flatten (map (image: image.assertions) imageQuadlets); + }; +} diff --git a/modules/services/podman-linux/install-quadlet.nix b/modules/services/podman-linux/install-quadlet.nix index 8ce6a33e8..d0157020c 100644 --- a/modules/services/podman-linux/install-quadlet.nix +++ b/modules/services/podman-linux/install-quadlet.nix @@ -5,7 +5,7 @@ with lib; let cfg = config.services.podman; - podman-lib = import ./podman-lib.nix { inherit lib config; }; + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; activation = import ./activation.nix { inherit config podman-lib; }; activationCleanupScript = activation.cleanup; diff --git a/modules/services/podman-linux/networks.nix b/modules/services/podman-linux/networks.nix index bb4ac5915..8fee143e4 100644 --- a/modules/services/podman-linux/networks.nix +++ b/modules/services/podman-linux/networks.nix @@ -5,13 +5,7 @@ with lib; let cfg = config.services.podman; - podman-lib = import ./podman-lib.nix { inherit lib config; }; - - awaitPodmanUnshare = pkgs.writeShellScript "await-podman-unshare" '' - until ${cfg.package}/bin/podman unshare ${pkgs.coreutils}/bin/true; do - sleep 1; - done - ''; + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; createQuadletSource = name: networkDef: let @@ -39,7 +33,7 @@ let "${makeBinPath [ pkgs.su pkgs.coreutils ]}" ]); }; - ExecStartPre = [ "${awaitPodmanUnshare}" ]; + ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ]; TimeoutStartSec = 15; RemainAfterExit = "yes"; }; @@ -162,7 +156,7 @@ in { services.podman.internal.quadletDefinitions = networkQuadlets; assertions = flatten (map (network: network.assertions) networkQuadlets); - home.file."${config.xdg.configHome}/podman/networks.manifest".text = + xdg.configFile."podman/networks.manifest".text = podman-lib.generateManifestText networkQuadlets; }; } diff --git a/modules/services/podman-linux/podman-lib.nix b/modules/services/podman-linux/podman-lib.nix index 1acc930fc..30ffcc836 100644 --- a/modules/services/podman-linux/podman-lib.nix +++ b/modules/services/podman-linux/podman-lib.nix @@ -1,4 +1,4 @@ -{ lib, config, ... }: +{ pkgs, lib, config, ... }: with lib; @@ -63,8 +63,10 @@ in { buildConfigAsserts = quadletName: extraConfig: let configRules = { + Build = { ImageTag = (with types; listOf str); }; Container = { ContainerName = types.enum [ quadletName ]; }; Network = { NetworkName = types.enum [ quadletName ]; }; + Volume = { VolumeName = types.enum [ quadletName ]; }; }; # Function to build assertions for a specific section and its attributes. @@ -83,8 +85,23 @@ in { else [ ]; + checkImageTag = extraConfig: + let + imageTags = (extraConfig.Build or { }).ImageTag or [ ]; + containsRequiredTag = + builtins.elem "homemanager/${quadletName}" imageTags; + imageTagsStr = concatMapStringsSep ''" "'' toString imageTags; + in [{ + assertion = imageTags == [ ] || containsRequiredTag; + message = '' + In '${quadletName}' config. Build.ImageTag: '[ "${imageTagsStr}" ]' does not contain 'homemanager/${quadletName}'.''; + }]; + # Flatten assertions from all sections in `extraConfig`. - in flatten (mapAttrsToList buildSectionAsserts extraConfig); + in flatten (concatLists [ + (mapAttrsToList buildSectionAsserts extraConfig) + (checkImageTag extraConfig) + ]); extraConfigType = with types; attrsOf (attrsOf (oneOf [ primitiveAttrs primitiveList primitive ])); @@ -107,8 +124,10 @@ in { # specific logic for writing the unit name goes here. It should be # identical to what `podman ls` shows in { + "build" = "localhost/homemanager/${strippedName}"; "container" = strippedName; "network" = strippedName; + "volume" = strippedName; }."${quadlet.resourceType}"; in if allQuadletsSameType then '' ${concatStringsSep "\n" @@ -133,4 +152,10 @@ in { lines = splitString "\n" text; nonEmptyLines = filter (line: line != "") lines; in concatStringsSep "\n" nonEmptyLines; + + awaitPodmanUnshare = pkgs.writeShellScript "await-podman-unshare" '' + until ${config.services.podman.package}/bin/podman unshare ${pkgs.coreutils}/bin/true; do + ${pkgs.coreutils}/bin/sleep 1 + done + ''; } diff --git a/modules/services/podman-linux/services.nix b/modules/services/podman-linux/services.nix index 5d31485f4..4a22b703f 100644 --- a/modules/services/podman-linux/services.nix +++ b/modules/services/podman-linux/services.nix @@ -42,8 +42,8 @@ in { "${config.home.homeDirectory}/.nix-profile/bin" ] }"; - ExecStart = "${pkgs.podman}/bin/podman auto-update"; - ExecStartPost = "${pkgs.podman}/bin/podman image prune -f"; + ExecStart = "${cfg.package}/bin/podman auto-update"; + ExecStartPost = "${cfg.package}/bin/podman image prune -f"; TimeoutStartSec = "300s"; TimeoutStopSec = "10s"; }; diff --git a/modules/services/podman-linux/volumes.nix b/modules/services/podman-linux/volumes.nix new file mode 100644 index 000000000..10f7cd927 --- /dev/null +++ b/modules/services/podman-linux/volumes.nix @@ -0,0 +1,186 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.podman; + + podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; + + createQuadletSource = name: volumeDef: + let + volumeConfig = podman-lib.deepMerge { + Install = { + WantedBy = optionals volumeDef.autoStart [ + "default.target" + "multi-user.target" + ]; + }; + Service = { + Environment = { + PATH = (builtins.concatStringsSep ":" [ + "${podman-lib.newuidmapPaths}" + "${makeBinPath [ pkgs.su pkgs.coreutils ]}" + ]); + }; + ExecStartPre = [ "${podman-lib.awaitPodmanUnshare}" ]; + TimeoutStartSec = 15; + RemainAfterExit = "yes"; + }; + Unit = { Description = volumeDef.description; }; + Volume = { + Copy = volumeDef.copy; + Device = volumeDef.device; + Driver = volumeDef.driver; + Group = volumeDef.group; + Image = volumeDef.image; + Label = volumeDef.labels // { + "nix.home-manager.managed" = true; + "nix.home-manager.preserve" = volumeDef.preserve; + }; + PodmanArgs = volumeDef.extraPodmanArgs; + Type = volumeDef.type; + User = volumeDef.user; + VolumeName = name; + }; + } volumeDef.extraConfig; + in '' + # Automatically generated by home-manager for podman volume configuration + # DO NOT EDIT THIS FILE DIRECTLY + # + # ${name}.volume + ${podman-lib.toQuadletIni volumeConfig} + ''; + + toQuadletInternal = name: volumeDef: { + assertions = podman-lib.buildConfigAsserts name volumeDef.extraConfig; + serviceName = + "podman-${name}"; # quadlet service name: 'podman--volume.service' + source = podman-lib.removeBlankLines (createQuadletSource name volumeDef); + resourceType = "volume"; + }; + +in let + volumeDefinitionType = types.submodule ({ name, ... }: { + options = { + + autoStart = mkOption { + type = types.bool; + default = true; + description = "Whether to create the volume on boot."; + }; + + copy = mkOption { + type = types.bool; + default = true; + description = + "Copy content of the image located at the mountpoint of the volume on first run."; + }; + + description = mkOption { + type = with types; nullOr str; + default = "Service for volume ${name}"; + defaultText = "Service for volume \${name}"; + example = "My Volume"; + description = "The description of the volume."; + }; + + device = mkOption { + type = with types; nullOr str; + default = null; + example = "tmpfs"; + description = "The path of a device which is mounted for the volume."; + }; + + driver = mkOption { + type = with types; nullOr str; + default = null; + example = "image"; + description = "The volume driver to use."; + }; + + extraConfig = mkOption { + type = podman-lib.extraConfigType; + default = { }; + example = literalExpression '' + { + Volume = { + ContainerConfModule = "/etc/nvd.conf"; + }; + } + ''; + description = "INI sections and values to populate the Volume Quadlet."; + }; + + extraPodmanArgs = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "--opt copy" ]; + description = + "Extra arguments to pass to the podman volume create command."; + }; + + group = mkOption { + type = with types; nullOr (either int str); + default = null; + description = "The group ID owning the volume inside the container."; + }; + + image = mkOption { + type = with types; nullOr str; + default = null; + example = "quay.io/centos/centos:latest"; + description = + "Specifies the image the volume is based on when Driver is set to the image."; + }; + + labels = mkOption { + type = with types; attrsOf str; + default = { }; + example = { + app = "myapp"; + some-label = "somelabel"; + }; + description = "The labels to apply to the volume."; + }; + + preserve = mkOption { + type = types.bool; + default = true; + description = '' + Whether the volume should be preserved if it is removed from the configuration. + Setting this to false will cause the volume to be deleted if the volume is removed from the configuration + ''; + }; + + type = mkOption { + type = with types; nullOr str; + default = null; + example = "tmpfs"; + description = + "Filesystem type of Device. (used as -t in mount commands)"; + }; + + user = mkOption { + type = with types; nullOr (either int str); + default = null; + description = "The user ID owning the volume inside the container."; + }; + }; + }); +in { + options.services.podman.volumes = mkOption { + type = types.attrsOf volumeDefinitionType; + default = { }; + description = "Defines Podman volume quadlet configurations."; + }; + + config = let volumeQuadlets = mapAttrsToList toQuadletInternal cfg.volumes; + in mkIf cfg.enable { + services.podman.internal.quadletDefinitions = volumeQuadlets; + assertions = flatten (map (volume: volume.assertions) volumeQuadlets); + + xdg.configFile."podman/volumes.manifest".text = + podman-lib.generateManifestText volumeQuadlets; + }; +} diff --git a/tests/modules/services/podman-linux/build-expected.service b/tests/modules/services/podman-linux/build-expected.service new file mode 100644 index 000000000..ab4064c22 --- /dev/null +++ b/tests/modules/services/podman-linux/build-expected.service @@ -0,0 +1,30 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman build configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-bld.build +[X-Build] +Environment= +File=/nix/store/00000000000000000000000000000000-Containerfile +ImageTag=homemanager/my-bld +Label=nix.home-manager.managed=true +TLSVerify=true + +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +RemainAfterExit=yes +TimeoutStartSec=300 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman build --tls-verify --tag homemanager/my-bld --label nix.home-manager.managed=true --file /nix/store/00000000000000000000000000000000-Containerfile +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for build my-bld +RequiresMountsFor=%t/containers +SourcePath=/nix/store/00000000000000000000000000000000-home-build-podman-my-bld/quadlets/podman-my-bld.build diff --git a/tests/modules/services/podman-linux/build.nix b/tests/modules/services/podman-linux/build.nix new file mode 100644 index 000000000..27052c6a4 --- /dev/null +++ b/tests/modules/services/podman-linux/build.nix @@ -0,0 +1,44 @@ +{ pkgs, ... }: + +{ + imports = [ ./podman-stubs.nix ]; + + services.podman = { + enable = true; + builds = { + "my-bld" = { + file = let + containerFile = pkgs.writeTextFile { + name = "Containerfile"; + text = '' + FROM docker.io/alpine:latest + ''; + }; + in "${containerFile}"; + }; + + "my-bld-2" = { + file = "https://www.github.com/././Containerfile"; + extraConfig = { + Build.ImageTag = [ "locahost/somethingelse" "localhost/anothertag" ]; + }; + }; + }; + }; + + test.asserts.assertions.expected = [ + '' + In 'my-bld-2' config. Build.ImageTag: '[ "locahost/somethingelse" "localhost/anothertag" ]' does not contain 'homemanager/my-bld-2'.'' + ]; + + nmt.script = '' + configPath=home-files/.config/systemd/user + buildFile=$configPath/podman-my-bld-build.service + + assertFileExists $buildFile + + buildFile=$(normalizeStorePaths $buildFile) + + assertFileContent $buildFile ${./build-expected.service} + ''; +} diff --git a/tests/modules/services/podman-linux/container-expected.service b/tests/modules/services/podman-linux/container-expected.service index d40e24bec..df5bcb2a0 100644 --- a/tests/modules/services/podman-linux/container-expected.service +++ b/tests/modules/services/podman-linux/container-expected.service @@ -43,7 +43,6 @@ ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --na [Unit] Wants=podman-user-wait-network-online.service After=podman-user-wait-network-online.service -After=network.target Before=fake.target Description=home-manager test SourcePath=/nix/store/00000000000000000000000000000000-home-container-podman-my-container/quadlets/podman-my-container.container diff --git a/tests/modules/services/podman-linux/default.nix b/tests/modules/services/podman-linux/default.nix index a5ba9467e..c7f04bdaf 100644 --- a/tests/modules/services/podman-linux/default.nix +++ b/tests/modules/services/podman-linux/default.nix @@ -1,7 +1,10 @@ { podman-configuration = ./configuration.nix; podman-container = ./container.nix; + podman-build = ./build.nix; + podman-image = ./image.nix; podman-integration = ./integration.nix; podman-manifest = ./manifest.nix; podman-network = ./network.nix; + podman-volume = ./volume.nix; } diff --git a/tests/modules/services/podman-linux/image-expected.service b/tests/modules/services/podman-linux/image-expected.service new file mode 100644 index 000000000..2a2e688e3 --- /dev/null +++ b/tests/modules/services/podman-linux/image-expected.service @@ -0,0 +1,28 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman image configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-img.image +[X-Image] +Image=docker.io/alpine:latest +TLSVerify=true + +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare +RemainAfterExit=yes +TimeoutStartSec=300 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman image pull --tls-verify docker.io/alpine:latest +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for image my-img +SourcePath=/nix/store/00000000000000000000000000000000-home-image-podman-my-img/quadlets/podman-my-img.image +RequiresMountsFor=%t/containers diff --git a/tests/modules/services/podman-linux/image.nix b/tests/modules/services/podman-linux/image.nix new file mode 100644 index 000000000..98560e34d --- /dev/null +++ b/tests/modules/services/podman-linux/image.nix @@ -0,0 +1,18 @@ +{ + imports = [ ./podman-stubs.nix ]; + + services.podman = { + enable = true; + images = { "my-img" = { image = "docker.io/alpine:latest"; }; }; + }; + + nmt.script = '' + configPath=home-files/.config/systemd/user + imageFile=$configPath/podman-my-img-image.service + assertFileExists $imageFile + + imageFile=$(normalizeStorePaths $imageFile) + + assertFileContent $imageFile ${./image-expected.service} + ''; +} diff --git a/tests/modules/services/podman-linux/integration-build-expected.service b/tests/modules/services/podman-linux/integration-build-expected.service new file mode 100644 index 000000000..ab4064c22 --- /dev/null +++ b/tests/modules/services/podman-linux/integration-build-expected.service @@ -0,0 +1,30 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman build configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-bld.build +[X-Build] +Environment= +File=/nix/store/00000000000000000000000000000000-Containerfile +ImageTag=homemanager/my-bld +Label=nix.home-manager.managed=true +TLSVerify=true + +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +RemainAfterExit=yes +TimeoutStartSec=300 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman build --tls-verify --tag homemanager/my-bld --label nix.home-manager.managed=true --file /nix/store/00000000000000000000000000000000-Containerfile +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for build my-bld +RequiresMountsFor=%t/containers +SourcePath=/nix/store/00000000000000000000000000000000-home-build-podman-my-bld/quadlets/podman-my-bld.build diff --git a/tests/modules/services/podman-linux/integration-container-bld-expected.service b/tests/modules/services/podman-linux/integration-container-bld-expected.service new file mode 100644 index 000000000..7201fea35 --- /dev/null +++ b/tests/modules/services/podman-linux/integration-container-bld-expected.service @@ -0,0 +1,38 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager podman container configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-container-bld.container +[X-Container] +ContainerName=my-container-bld +Environment= +Image=localhost/homemanager/my-bld +Label=nix.home-manager.managed=true + +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +Environment=PATH=/run/wrappers/bin:/run/current-system/sw/bin:/home/hm-user/.nix-profile/bin +Restart=always +TimeoutStopSec=30 +Environment=PODMAN_SYSTEMD_UNIT=%n +KillMode=mixed +ExecStop=/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid +ExecStopPost=-/nix/store/00000000000000000000000000000000-podman/bin/podman rm -v -f -i --cidfile=%t/%N.cid +Delegate=yes +Type=notify +NotifyAccess=all +SyslogIdentifier=%N +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name my-container-bld --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --label nix.home-manager.managed=true localhost/homemanager/my-bld + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +After=podman-my-bld-build.service +Description=Service for container my-container-bld +Requires=podman-my-bld-build.service +SourcePath=/nix/store/00000000000000000000000000000000-home-container-podman-my-container-bld/quadlets/podman-my-container-bld.container +RequiresMountsFor=%t/containers diff --git a/tests/modules/services/podman-linux/integration-container-expected.service b/tests/modules/services/podman-linux/integration-container-expected.service index 528d9c18e..ca92559d4 100644 --- a/tests/modules/services/podman-linux/integration-container-expected.service +++ b/tests/modules/services/podman-linux/integration-container-expected.service @@ -11,6 +11,7 @@ Image=docker.io/alpine:latest Label=nix.home-manager.managed=true Network=my-net Network=externalnet +Volume=my-vol:/data [Install] WantedBy=default.target @@ -28,14 +29,17 @@ Delegate=yes Type=notify NotifyAccess=all SyslogIdentifier=%N -ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --network my-net --network externalnet --sdnotify=conmon -d --label nix.home-manager.managed=true docker.io/alpine:latest +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman run --name my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --network my-net --network externalnet --sdnotify=conmon -d -v my-vol:/data --label nix.home-manager.managed=true docker.io/alpine:latest [Unit] Wants=podman-user-wait-network-online.service After=podman-user-wait-network-online.service -After=network.target +After=podman-my-img-image.service After=podman-my-net-network.service +After=podman-my-vol-volume.service Description=Service for container my-container +Requires=podman-my-img-image.service Requires=podman-my-net-network.service +Requires=podman-my-vol-volume.service SourcePath=/nix/store/00000000000000000000000000000000-home-container-podman-my-container/quadlets/podman-my-container.container RequiresMountsFor=%t/containers diff --git a/tests/modules/services/podman-linux/integration-image-expected.service b/tests/modules/services/podman-linux/integration-image-expected.service new file mode 100644 index 000000000..2a2e688e3 --- /dev/null +++ b/tests/modules/services/podman-linux/integration-image-expected.service @@ -0,0 +1,28 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman image configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-img.image +[X-Image] +Image=docker.io/alpine:latest +TLSVerify=true + +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare +RemainAfterExit=yes +TimeoutStartSec=300 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman image pull --tls-verify docker.io/alpine:latest +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for image my-img +SourcePath=/nix/store/00000000000000000000000000000000-home-image-podman-my-img/quadlets/podman-my-img.image +RequiresMountsFor=%t/containers diff --git a/tests/modules/services/podman-linux/integration-volume-expected.service b/tests/modules/services/podman-linux/integration-volume-expected.service new file mode 100644 index 000000000..da5d36968 --- /dev/null +++ b/tests/modules/services/podman-linux/integration-volume-expected.service @@ -0,0 +1,33 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman volume configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-vol.volume +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +Environment=PATH=/run/wrappers/bin:/usr/bin:/bin:/usr/sbin:/sbin:@shadow@/bin:/nix/store/00000000000000000000000000000000-coreutils/bin +ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare +RemainAfterExit=yes +TimeoutStartSec=15 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman volume create --ignore --opt copy --opt device=tmpfs --opt type=tmpfs --label nix.home-manager.managed=true --label nix.home-manager.preserve=false my-vol +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for volume my-vol +SourcePath=/nix/store/00000000000000000000000000000000-home-volume-podman-my-vol/quadlets/podman-my-vol.volume +RequiresMountsFor=%t/containers + +[X-Volume] +Copy=true +Device=tmpfs +Label=nix.home-manager.managed=true +Label=nix.home-manager.preserve=false +Type=tmpfs +VolumeName=my-vol diff --git a/tests/modules/services/podman-linux/integration.nix b/tests/modules/services/podman-linux/integration.nix index 6b28fcd7a..26aef7019 100644 --- a/tests/modules/services/podman-linux/integration.nix +++ b/tests/modules/services/podman-linux/integration.nix @@ -1,29 +1,69 @@ +{ pkgs, ... }: + { imports = [ ./podman-stubs.nix ]; services.podman = { enable = true; - containers."my-container" = { - image = "docker.io/alpine:latest"; - network = [ "my-net" "externalnet" ]; + builds."my-bld" = { + file = let + containerFile = pkgs.writeTextFile { + name = "Containerfile"; + text = '' + FROM docker.io/alpine:latest + ''; + }; + in "${containerFile}"; }; + containers = { + "my-container" = { + image = "my-img"; + network = [ "my-net" "externalnet" ]; + volumes = [ "my-vol:/data" ]; + }; + "my-container-bld" = { image = "my-bld"; }; + }; + images."my-img" = { image = "docker.io/alpine:latest"; }; networks."my-net" = { gateway = "192.168.123.1"; subnet = "192.168.123.0/24"; }; + volumes."my-vol" = { + device = "tmpfs"; + preserve = false; + type = "tmpfs"; + }; }; nmt.script = '' configPath=home-files/.config/systemd/user + buildFile=$configPath/podman-my-bld-build.service containerFile=$configPath/podman-my-container.service + containerBldFile=$configPath/podman-my-container-bld.service + imageFile=$configPath/podman-my-img-image.service networkFile=$configPath/podman-my-net-network.service + volumeFile=$configPath/podman-my-vol-volume.service + assertFileExists $buildFile assertFileExists $containerFile + assertFileExists $containerBldFile + assertFileExists $imageFile assertFileExists $networkFile + assertFileExists $volumeFile + buildFile=$(normalizeStorePaths $buildFile) containerFile=$(normalizeStorePaths $containerFile) + containerBldFile=$(normalizeStorePaths $containerBldFile) + imageFile=$(normalizeStorePaths $imageFile) networkFile=$(normalizeStorePaths $networkFile) + volumeFile=$(normalizeStorePaths $volumeFile) + assertFileContent $buildFile ${./integration-build-expected.service} assertFileContent $containerFile ${./integration-container-expected.service} + assertFileContent $containerBldFile ${ + ./integration-container-bld-expected.service + } + assertFileContent $imageFile ${./integration-image-expected.service} assertFileContent $networkFile ${./integration-network-expected.service} + assertFileContent $volumeFile ${./integration-volume-expected.service} ''; } diff --git a/tests/modules/services/podman-linux/volume-expected.service b/tests/modules/services/podman-linux/volume-expected.service new file mode 100644 index 000000000..73d4c049f --- /dev/null +++ b/tests/modules/services/podman-linux/volume-expected.service @@ -0,0 +1,36 @@ +# Automatically generated by /nix/store/00000000000000000000000000000000-podman/lib/systemd/user-generators/podman-user-generator +# +# Automatically generated by home-manager for podman volume configuration +# DO NOT EDIT THIS FILE DIRECTLY +# +# my-vol.volume +[Install] +WantedBy=default.target +WantedBy=multi-user.target + +[Service] +Environment=PATH=/run/wrappers/bin:/usr/bin:/bin:/usr/sbin:/sbin:@shadow@/bin:/nix/store/00000000000000000000000000000000-coreutils/bin +ExecStartPre=/nix/store/00000000000000000000000000000000-await-podman-unshare +RemainAfterExit=yes +TimeoutStartSec=15 +ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman volume create --ignore --opt copy --opt device=tmpfs --opt type=tmpfs --opt o=uid=1000,gid=1000 --label nix.home-manager.managed=true --label nix.home-manager.preserve=true --module=/etc/nvd.conf my-vol +SyslogIdentifier=%N +Type=oneshot + +[Unit] +Wants=podman-user-wait-network-online.service +After=podman-user-wait-network-online.service +Description=Service for volume my-vol +SourcePath=/nix/store/00000000000000000000000000000000-home-volume-podman-my-vol/quadlets/podman-my-vol.volume +RequiresMountsFor=%t/containers + +[X-Volume] +Copy=true +Device=tmpfs +Group=1000 +Label=nix.home-manager.managed=true +Label=nix.home-manager.preserve=true +PodmanArgs=--module=/etc/nvd.conf +Type=tmpfs +User=1000 +VolumeName=my-vol diff --git a/tests/modules/services/podman-linux/volume.nix b/tests/modules/services/podman-linux/volume.nix new file mode 100644 index 000000000..885662b2f --- /dev/null +++ b/tests/modules/services/podman-linux/volume.nix @@ -0,0 +1,35 @@ +{ + imports = [ ./podman-stubs.nix ]; + + services.podman = { + enable = true; + volumes = { + "my-vol" = { + device = "tmpfs"; + extraConfig = { Volume = { User = 1000; }; }; + extraPodmanArgs = [ "--module=/etc/nvd.conf" ]; + group = 1000; + type = "tmpfs"; + }; + + "my-vol-2" = { + extraConfig = { Volume = { VolumeName = "some-other-volume-name"; }; }; + }; + }; + }; + + test.asserts.assertions.expected = [ + '' + In 'my-vol-2' config. Volume.VolumeName: 'some-other-volume-name' does not match expected type: value "my-vol-2" (singular enum)'' + ]; + + nmt.script = '' + configPath=home-files/.config/systemd/user + volumeFile=$configPath/podman-my-vol-volume.service + assertFileExists $volumeFile + + volumeFile=$(normalizeStorePaths $volumeFile) + + assertFileContent $volumeFile ${./volume-expected.service} + ''; +}