From de53c7ccfb748444faf1c4bb59db2843a97e6302 Mon Sep 17 00:00:00 2001 From: Nicholas Hassan Date: Fri, 22 Dec 2023 10:54:18 +1030 Subject: [PATCH] podman: add new module 'podman' Adds a new Podman module for creating user containers and networks as systemd services. These are installed to the user's XDG_CONFIG/systemd/user directory. --- modules/lib/maintainers.nix | 10 + modules/modules.nix | 1 + modules/services/podman-linux/containers.nix | 343 ++++++++++++++++++ modules/services/podman-linux/default.nix | 11 + .../services/podman-linux/install-quadlet.nix | 169 +++++++++ modules/services/podman-linux/networks.nix | 54 +++ modules/services/podman-linux/options.nix | 36 ++ modules/services/podman-linux/podman-lib.nix | 83 +++++ modules/services/podman-linux/services.nix | 60 +++ 9 files changed, 767 insertions(+) create mode 100644 modules/services/podman-linux/containers.nix create mode 100644 modules/services/podman-linux/default.nix create mode 100644 modules/services/podman-linux/install-quadlet.nix create mode 100644 modules/services/podman-linux/networks.nix create mode 100644 modules/services/podman-linux/options.nix create mode 100644 modules/services/podman-linux/podman-lib.nix create mode 100644 modules/services/podman-linux/services.nix diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index 28327916..2607c5cf 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -231,6 +231,16 @@ github = "nilp0inter"; githubId = 1224006; }; + n-hass = { + name = "Nicholas Hassan"; + email = "nick@hassan.host"; + github = "n-hass"; + githubId = 72363381; + keys = [{ + longkeyid = "ed25519/0xA37159732728A6A6"; + fingerprint = "FDEE 6116 DBA7 8840 7323 4466 A371 5973 2728 A6A6"; + }]; + }; seylerius = { email = "sable@seyleri.us"; name = "Sable Seyler"; diff --git a/modules/modules.nix b/modules/modules.nix index d526c352..cc71150d 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -335,6 +335,7 @@ let ./services/plan9port.nix ./services/playerctld.nix ./services/plex-mpv-shim.nix + ./services/podman-linux/default.nix ./services/polybar.nix ./services/poweralertd.nix ./services/psd.nix diff --git a/modules/services/podman-linux/containers.nix b/modules/services/podman-linux/containers.nix new file mode 100644 index 00000000..f405c386 --- /dev/null +++ b/modules/services/podman-linux/containers.nix @@ -0,0 +1,343 @@ +{ config, lib, ... }: + +with lib; + +let + podman-lib = import ./podman-lib.nix { inherit lib; }; + + createQuadletSource = name: containerDef: + let + ### Definitions + serviceName = if containerDef.serviceName != null then containerDef.serviceName else name; + containerName = name; # Use the submodule name as the container name + mergedServiceConfig = podman-lib.serviceConfigDefaults // containerDef.serviceConfig; + mergedUnitConfig = podman-lib.unitConfigDefaults // containerDef.unitConfig; + ### + + ### Helpers + ifNotNull = condition: text: if condition != null then text else ""; + ifNotEmptyList = list: text: if list != [] then text else ""; + ifNotEmptySet = set: text: if set != {} then text else ""; + ### + + ### Formatters + formatExtraConfig = podman-lib.formatExtraConfig; + formatPrimitiveValue = podman-lib.formatPrimitiveValue; + + formatNetworkDependencies = networks: + let + formatElement = network: "podman-${network}-network.service"; + in + concatStringsSep " " (map formatElement networks); + + formatEnvironment = env: + if env != {} then + concatStringsSep " " (mapAttrsToList (k: v: "${k}=${formatPrimitiveValue v}") env) + else + ""; + + formatPorts = ports: + if ports != [] then + concatStringsSep "\n" (map (port: "PublishPort=${port}") ports) + else + ""; + + formatVolumes = volumes: + if volumes != [] then + concatStringsSep "\n" (map (volume: "Volume=${volume}") volumes) + else + ""; + + formatDevices = devices: + if devices != [] then + concatStringsSep "\n" (map (device: "AddDevice=${device}") devices) + else + ""; + + formatCapabilities = action: capabilities: + if capabilities != [] then + concatStringsSep "\n" (map (capability: "${action}Capability=${capability}") capabilities) + else + ""; + + formatLabels = labels: + if labels != [] then + concatStringsSep "\n" (map (label: "Label=${label}") labels) + else + ""; + + formatAutoUpdate = autoupdate: + if autoupdate == "registry" then + "AutoUpdate=registry" + else if autoupdate == "local" then + "AutoUpdate=local" + else + ""; + + # TODO: check that the user hasn't supplied both networkMode and networks + formatNetwork = containerDef: + if containerDef.networkMode != null then + "Network=${containerDef.networkMode}" + else if containerDef.networks != [] then + "Network=${concatStringsSep "," containerDef.networks}" + else + ""; + + formatPodmanArgs = containerDef: + let + networkAliasArg = if containerDef.networkAlias != null then "--network-alias ${containerDef.networkAlias}" else null; + entrypointArg = if containerDef.entrypoint != null then "--entrypoint ${containerDef.entrypoint}" else null; + allArgs = [networkAliasArg entrypointArg] ++ containerDef.extraOptions; + in + if allArgs != [] && allArgs != [""] then + "PodmanArgs=${concatStringsSep " " (filter (arg: arg != null && arg != "") allArgs)}" + else + ""; + ### + + configText = '' + # Automatically generated by home-manager podman containers module + # DO NOT EDIT THIS FILE DIRECTLY + # + # ${serviceName}.container + [Unit] + Description=${if containerDef.description != null then containerDef.description else "Service for container ${containerName}"} + After=network.target + ${ifNotEmptyList containerDef.networks "After=${formatNetworkDependencies containerDef.networks}"} + ${formatExtraConfig mergedUnitConfig} + + [Container] + ContainerName=${containerName} + Image=${containerDef.image} + Label=nix.home-manager.managed=true + ${ifNotEmptySet containerDef.environment "Environment=${formatEnvironment containerDef.environment}"} + ${ifNotNull containerDef.environmentFile "EnvironmentFile=${containerDef.environmentFile}"} + ${ifNotNull containerDef.command "Exec=${containerDef.command}"} + ${ifNotNull containerDef.user "User=${formatPrimitiveValue containerDef.user}"} + ${ifNotNull containerDef.userNS "UserNS=${containerDef.userNS}"} + ${ifNotNull containerDef.group "Group=${formatPrimitiveValue containerDef.group}"} + ${ifNotEmptyList containerDef.ports (formatPorts containerDef.ports)} + ${ifNotNull containerDef.networkMode "Network=${containerDef.networkMode}"} + ${formatNetwork containerDef} + ${ifNotNull containerDef.ip4 "IP=${containerDef.ip4}"} + ${ifNotNull containerDef.ip6 "IP6=${containerDef.ip6}"} + ${ifNotEmptyList containerDef.volumes (formatVolumes containerDef.volumes)} + ${ifNotEmptyList containerDef.devices (formatDevices containerDef.devices)} + ${formatAutoUpdate containerDef.autoupdate} + ${ifNotEmptyList containerDef.addCapabilities (formatCapabilities "Add" containerDef.addCapabilities)} + ${ifNotEmptyList containerDef.dropCapabilities (formatCapabilities "Drop" containerDef.dropCapabilities)} + ${ifNotEmptyList containerDef.labels (formatLabels containerDef.labels)} + ${formatPodmanArgs containerDef} + ${formatExtraConfig containerDef.extraContainerConfig} + + [Service] + Environment="PATH=/run/wrappers/bin:/run/current-system/sw/bin:${config.home.homeDirectory}/.nix-profile/bin" + ${formatExtraConfig mergedServiceConfig} + + [Install] + ${if containerDef.autostart then "WantedBy=multi-user.target default.target" else ""} + ''; + + removeBlankLines = text: + let + lines = splitString "\n" text; + nonEmptyLines = filter (line: line != "") lines; + in + concatStringsSep "\n" nonEmptyLines; + + in + removeBlankLines configText; + + toQuadletInternal = name: containerDef: + let + allAssertions = (podman-lib.assertConfigTypes podman-lib.serviceConfigTypeRules containerDef.serviceConfig name) ++ + (podman-lib.assertConfigTypes podman-lib.unitConfigTypeRules containerDef.unitConfig name); + in + { + serviceName = if containerDef.serviceName != null then containerDef.serviceName else "podman-${name}"; + source = createQuadletSource name containerDef; + unitType = "container"; + assertions = allAssertions; + }; +in + +let + # Define the container user type as the user interface + containerDefinitionType = types.submodule { + options = { + serviceName = mkOption { + type = with types; nullOr str; + description = "The name of the systemd service to generate for the container."; + default = null; + }; + + description = mkOption { + type = with types; nullOr str; + description = "The description of the container."; + default = null; + }; + + image = mkOption { + type = types.str; + description = "The container image."; + }; + + entrypoint = mkOption { + type = with types; nullOr str; + description = "The container entrypoint."; + default = null; + }; + + command = mkOption { + type = with types; nullOr str; + description = "The command to run after the container specification."; + default = null; + }; + + environment = mkOption { + type = podman-lib.primitiveAttrs; + default = {}; + }; + + environmentFile = mkOption { + type = with types; nullOr str; + default = null; + }; + + ports = mkOption { + type = with types; listOf str; + default = []; + }; + + user = mkOption { + type = with types; nullOr (oneOf [ str int ]); + default = null; + }; + + userNS = mkOption { + type = with types; nullOr str; + default = null; + }; + + group = mkOption { + type = with types; nullOr (oneOf [ str int ]); + default = null; + }; + + networkMode = mkOption { + type = with types; nullOr str; + default = null; + }; + + networks = mkOption { + type = with types; listOf str; + default = []; + }; + + ip4 = mkOption { + type = with types; nullOr str; + default = null; + }; + + ip6 = mkOption { + type = with types; nullOr str; + default = null; + }; + + networkAlias = mkOption { + type = with types; nullOr str; + default = null; + }; + + volumes = mkOption { + type = with types; listOf str; + default = []; + }; + + devices = mkOption { + type = types.listOf types.str; + default = []; + description = "The devices to mount into the container, in the format '/dev/:/dev/'."; + }; + + autoupdate = mkOption { + type = with types; enum [ + "" + "registry" + "local" + ]; + default = ""; + }; + + autostart = mkOption { + type = types.bool; + default = true; + }; + + addCapabilities = mkOption { + type = with types; listOf str; + default = []; + }; + + dropCapabilities = mkOption { + type = with types; listOf str; + default = []; + }; + + labels = mkOption { + type = with types; listOf str; + default = []; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = []; + }; + + extraContainerConfig = mkOption { + type = podman-lib.primitiveAttrs; + default = {}; + example = literalExample '' + extraContainerConfig = { + UIDMap = "0:1000:1"; + ReadOnlyTmpfs = true; + EnvironmentFile = [ /etc/environment /root/.env]; + }; + ''; + }; + + serviceConfig = mkOption { + type = podman-lib.serviceConfigType; + default = {}; + }; + + unitConfig = mkOption { + type = podman-lib.unitConfigType; + default = {}; + }; + + }; + }; + +in { + + imports = [ + ./options.nix + ]; + + options.services.podman.containers = mkOption { + type = types.attrsOf containerDefinitionType; + default = {}; + description = "Attribute set of container definitions."; + }; + + config = let + containerQuadlets = mapAttrsToList toQuadletInternal config.services.podman.containers; + in { + internal.podman-quadlet-definitions = containerQuadlets; + assertions = lib.flatten (map (container: container.assertions) config.internal.podman-quadlet-definitions); + + # manifest file + home.file."${config.xdg.configHome}/podman/containers.manifest".text = podman-lib.generateManifestText containerQuadlets; + }; +} diff --git a/modules/services/podman-linux/default.nix b/modules/services/podman-linux/default.nix new file mode 100644 index 00000000..55dbfee8 --- /dev/null +++ b/modules/services/podman-linux/default.nix @@ -0,0 +1,11 @@ +{ pkgs, lib, ... }: + +{ + imports = + [ ./services.nix ./networks.nix ./containers.nix ./install-quadlet.nix ]; + + config = { + assertions = + [ (lib.hm.assertions.assertPlatform "podman" pkgs lib.platforms.linux) ]; + }; +} \ No newline at end of file diff --git a/modules/services/podman-linux/install-quadlet.nix b/modules/services/podman-linux/install-quadlet.nix new file mode 100644 index 00000000..53757cee --- /dev/null +++ b/modules/services/podman-linux/install-quadlet.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + quadletActivationCleanupScript = '' + resourceManifest=() + # Define VERBOSE_ENABLED as a function + VERBOSE_ENABLED() { + if [[ -n "''${VERBOSE:-}" ]]; then + return 0 + else + return 1 + fi + } + + # Function to fill resourceManifest from the manifest file + function loadManifest { + local manifestFile="$1" + VERBOSE_ENABLED && echo "Loading manifest from $manifestFile..." + IFS=$'\n' read -r -d "" -a resourceManifest <<< "$(cat "$manifestFile")" || true + } + + function isResourceInManifest { + local resource="$1" + for manifestEntry in "''${resourceManifest[@]}"; do + if [ "$resource" = "$manifestEntry" ]; then + return 0 # Resource found in manifest + fi + done + return 1 # Resource not found in manifest + } + + function removeContainer { + echo "Removing orphaned container: $1" + if [[ -n "''${DRY_RUN:-}" ]]; then + echo "Would run podman stop $1" + echo "Would run podman $resourceType rm -f $1" + else + ${pkgs.podman}/bin/podman stop "$1" + ${pkgs.podman}/bin/podman $resourceType rm -f "$1" + fi + } + + function removeNetwork { + echo "Removing orphaned network: $1" + if [[ -n "''${DRY_RUN:-}" ]]; then + echo "Would run podman network rm $1" + else + if ! ${pkgs.podman}/bin/podman network rm "$1"; then + echo "Failed to remove network $1. Is it still in use by a container?" + return 1 + fi + fi + } + + function cleanup { + local resourceType=$1 + local manifestFile="${config.xdg.configHome}/podman/$2" + local extraListCommands="''${3:-}" + [[ $resourceType = "container" ]] && extraListCommands+=" -a" + + VERBOSE_ENABLED && echo "Cleaning up ''${resourceType}s not in manifest..." + + loadManifest "$manifestFile" + + formatString="{{.Name}}" + [[ $resourceType = "container" ]] && formatString="{{.Names}}" + + # Capture the output of the podman command to a variable + local listOutput=$(${pkgs.podman}/bin/podman $resourceType ls $extraListCommands --filter 'label=nix.home-manager.managed=true' --format "$formatString") + + IFS=$'\n' read -r -d "" -a podmanResources <<< "$listOutput" || true + + # Check if the array is populated and iterate over it + if [ ''${#resourceManifest[@]} -eq 0 ]; then + VERBOSE_ENABLED && echo "No ''${resourceType}s available to process." + else + for resource in "''${podmanResources[@]}"; do + if ! isResourceInManifest "$resource"; then + + [[ $resourceType = "container" ]] && removeContainer "$resource" + [[ $resourceType = "network" ]] && removeNetwork "$resource" + + else + if VERBOSE_ENABLED; then + echo "Keeping managed $resourceType: $resource" + fi + fi + done + fi + } + + # Cleanup containers + cleanup "container" "containers.manifest" + + # Cleanup networks + cleanup "network" "networks.manifest" + ''; + + # derivation to build a single Podman quadlet, outputting its systemd unit files + buildPodmanQuadlet = quadlet: pkgs.stdenv.mkDerivation { + name = "home-${quadlet.unitType}-${quadlet.serviceName}"; + + buildInputs = [ pkgs.podman ]; + + dontUnpack = true; + + buildPhase = '' + mkdir $out + # Directory for the quadlet file + mkdir -p $out/quadlets + # Directory for systemd unit files + mkdir -p $out/units + + # Write the quadlet file + echo -n "${quadlet.source}" > $out/quadlets/${quadlet.serviceName}.${quadlet.unitType} + + # Generate systemd unit file/s from the quadlet file + export QUADLET_UNIT_DIRS=$out/quadlets + ${pkgs.podman}/lib/systemd/user-generators/podman-user-generator $out/units + ''; + + passthru = { + outPath = self.out; + quadletData = quadlet; + }; + }; + + # Create a derivation for each quadlet spec + builtQuadlets = map buildPodmanQuadlet config.internal.podman-quadlet-definitions; + + accumulateUnitFiles = prefix: path: quadlet: let + entries = builtins.readDir path; + processEntry = name: type: + let + newPath = "${path}/${name}"; + newPrefix = prefix + (if prefix == "" then "" else "/") + name; + in + if type == "directory" then accumulateUnitFiles newPrefix newPath quadlet + else [{ + key = newPrefix; + value = { path = newPath; parentQuadlet = quadlet; }; + }]; + in flatten (map (name: processEntry name (getAttr name entries)) (attrNames entries)); + + allUnitFiles = concatMap (builtQuadlet: accumulateUnitFiles "" "${builtQuadlet.outPath}/units" builtQuadlet.quadletData ) builtQuadlets; + + # we're doing this because the home-manager recursive file linking implementation can't + # merge from multiple sources. so we link each file explicitly, which is fine for all unique files + generateSystemdFileLinks = files: listToAttrs (map (unitFile: { + name = "${config.xdg.configHome}/systemd/user/${unitFile.key}"; + value = { + source = unitFile.value.path; + }; + }) files); + +in { + imports = [ + ./options.nix + ]; + + config = { + home.file = generateSystemdFileLinks allUnitFiles; + + # if the length of builtQuadlets is 0, then we don't need register the activation script + home.activation.podmanQuadletCleanup = lib.mkIf (lib.length builtQuadlets >= 1) (lib.hm.dag.entryAfter ["reloadSystemd"] quadletActivationCleanupScript); + }; +} diff --git a/modules/services/podman-linux/networks.nix b/modules/services/podman-linux/networks.nix new file mode 100644 index 00000000..ef8ca92a --- /dev/null +++ b/modules/services/podman-linux/networks.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +with lib; + +let + podman-lib = import ./podman-lib.nix { inherit lib; }; + + createQuadletSource = name: networkDef: + '' + # Automatically generated by home-manager for podman network configuration + # DO NOT EDIT THIS FILE DIRECTLY + [Network] + Label=nix.home-manager.managed=true + ${podman-lib.formatExtraConfig networkDef} + + [Install] + WantedBy=multi-user.target default.target + ''; + + toQuadletInternal = name: networkDef: + { + serviceName = "podman-${name}"; # becomes podman--network.service because of quadlet + source = createQuadletSource name networkDef; + unitType = "network"; + }; + +in +{ + options = { + services.podman.networks = mkOption { + type = types.attrsOf (podman-lib.primitiveAttrs); + default = {}; + example = literalExample '' + { + mynetwork = { + Subnet = "192.168.1.0/24"; + Gateway = "192.168.1.1"; + NetworkName = "mynetwork"; + }; + } + ''; + description = "Defines Podman network quadlet configurations."; + }; + }; + + config = let + networkQuadlets = mapAttrsToList toQuadletInternal config.services.podman.networks; + in { + internal.podman-quadlet-definitions = networkQuadlets; + + # manifest file + home.file."${config.xdg.configHome}/podman/networks.manifest".text = podman-lib.generateManifestText networkQuadlets; + }; +} diff --git a/modules/services/podman-linux/options.nix b/modules/services/podman-linux/options.nix new file mode 100644 index 00000000..37a0f7b3 --- /dev/null +++ b/modules/services/podman-linux/options.nix @@ -0,0 +1,36 @@ +{lib, ...}: + +let + # Define the systemd service type + quadletInternalType = lib.types.submodule { + options = { + serviceName = lib.mkOption { + type = lib.types.str; + description = "The name of the systemd service."; + }; + + unitType = lib.mkOption { + type = lib.types.str; + default = ""; + description = "The type of the systemd unit."; + }; + + source = lib.mkOption { + type = lib.types.str; + description = "The quadlet source file content."; + }; + + assertions = lib.mkOption { + type = with lib.types; listOf unspecified; + default = []; + description = "List of Nix type assertions."; + }; + }; + }; +in { + options.internal.podman-quadlet-definitions = lib.mkOption { + type = lib.types.listOf quadletInternalType; + default = {}; + description = "List of quadlet source file content and service names."; + }; +} \ No newline at end of file diff --git a/modules/services/podman-linux/podman-lib.nix b/modules/services/podman-linux/podman-lib.nix new file mode 100644 index 00000000..43569e58 --- /dev/null +++ b/modules/services/podman-linux/podman-lib.nix @@ -0,0 +1,83 @@ +{ lib, ... }: + +with lib; + +let + primitive = with types; nullOr (oneOf [ bool int str path ]); + primitiveAttrs = with types; attrsOf (either primitive (listOf primitive)); + + formatPrimitiveValue = value: + if isBool value then + (if value then "true" else "false") + else if isList value then + concatStringsSep " " (map toString value) + else + toString value; +in { + inherit primitive; + inherit primitiveAttrs; + inherit formatPrimitiveValue; + + serviceConfigTypeRules = { + Restart = types.enum [ "no" "always" "on-failure" "unless-stopped" ]; + TimeoutStopSec = types.int; + }; + serviceConfigDefaults = { + Restart = "always"; + TimeoutStopSec = 30; + ExecStartPre = null; + }; + serviceConfigType = with types; attrsOf (either primitive (listOf primitive)); + + unitConfigTypeRules = { + After = with types; nullOr (listOf str); + }; + unitConfigDefaults = { + After = null; + }; + unitConfigType = with types; attrsOf (either primitive (listOf primitive)); + + assertConfigTypes = configTypeRules: config: containerName: + lib.flatten (lib.mapAttrsToList (name: value: + if lib.hasAttr name configTypeRules then + [{ + assertion = configTypeRules.${name}.check value; + message = "in '${containerName}' config. ${name}: '${toString value}' does not match expected type: ${configTypeRules.${name}.description}"; + }] + else [] + ) config); + + formatExtraConfig = extraConfig: + let + nonNullConfig = lib.filterAttrs (name: value: value != null) extraConfig; + in + concatStringsSep "\n" ( + mapAttrsToList (name: value: "${name}=${formatPrimitiveValue value}") nonNullConfig + ); + + # input is expecting a list of quadletInternalType with all the same unitType + generateManifestText = quadlets: + let + # create a list of all unique quadlet.unitTypes in quadlets + quadletTypes = unique (map (quadlet: quadlet.unitType) quadlets); + # if quadletTypes is not length 1, then all quadlets are not the same type + allQuadletsSameType = length quadletTypes == 1; + + # ensures the service name is formatted correctly to be easily read by the activation script and matches `podman ls` output + formatServiceName = quadlet: + let + # remove the podman- prefix from the service name string + strippedName = builtins.replaceStrings ["podman-"] [""] quadlet.serviceName; + in + # specific logic for writing the unit name goes here. It should be identical to what `podman ls` shows + { + "container" = strippedName; + "network" = strippedName; + }."${quadlet.unitType}"; + in + if allQuadletsSameType then '' + ${concatStringsSep "\n" (map (quadlet: formatServiceName quadlet) quadlets)} + '' + else + abort "All quadlets must be of the same type.\nQuadlet types in this manifest: ${concatStringsSep ", " quadletTypes}"; +} \ No newline at end of file diff --git a/modules/services/podman-linux/services.nix b/modules/services/podman-linux/services.nix new file mode 100644 index 00000000..23c3d0b1 --- /dev/null +++ b/modules/services/podman-linux/services.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.podman; +in { + options.services.podman = { + + auto-update = { + enable = mkOption { + type = types.bool; + default = false; + description = "Automatically update the podman images."; + }; + + OnCalendar = mkOption { + type = types.str; + default = "Sun *-*-* 00:00"; + description = "Systemd OnCalendar expression for the update"; + }; + }; + + }; + + config = mkMerge [ + ( mkIf cfg.auto-update.enable { + systemd.user.services."podman-auto-update" = { + Unit = { + Description = "Podman auto-update service"; + Documentation = "man:podman-auto-update(1)"; + Wants = [ "network-online.target" ]; + After = [ "network-online.target" ]; + }; + Service = { + Type = "oneshot"; + Environment = "PATH=/run/wrappers/bin:/run/current-system/sw/bin:${config.home.homeDirectory}/.nix-profile/bin"; + ExecStart = "${pkgs.podman}/bin/podman auto-update"; + ExecStartPost = "${pkgs.podman}/bin/podman image prune -f"; + TimeoutStartSec = "300s"; + TimeoutStopSec = "10s"; + }; + }; + + systemd.user.timers."podman-auto-update" = { + Unit = { + Description = "Podman auto-update timer"; + }; + Timer = { + OnCalendar = cfg.auto-update.OnCalendar; + RandomizedDelaySec = 300; + Persistent = true; + }; + Install = { + WantedBy = [ "timers.target" ]; + }; + }; + }) + ]; +}