{ config, lib, pkgs, ... }: let inherit (lib) mkOption types; cfg = config.services.podman; podman-lib = import ./podman-lib.nix { inherit pkgs lib config; }; createQuadletSource = name: containerDef: let extractQuadletReference = type: value: let regex = "([a-zA-Z0-9_-]+\\." + type + ").*"; parts = builtins.match regex value; in if parts == null then value else builtins.elemAt parts 0; dependencyBySuffix = type: value: if (lib.hasInfix ".${type}" value) then let name = extractQuadletReference type value; in if (lib.hasAttr name cfg.internal.builtQuadlets) then [ (cfg.internal.builtQuadlets.${name}) ] else [ ] else [ ]; withResolverFor = type: value: let resolve = v: dependencyBySuffix type v; in if builtins.isList value then builtins.concatLists (map resolve value) # Flatten list of lists else resolve value; dependencyServices = (withResolverFor "image" containerDef.image) ++ (withResolverFor "build" containerDef.image) ++ (withResolverFor "network" containerDef.network) ++ (withResolverFor "volume" containerDef.volumes); checkQuadletReference = types: value: if builtins.isList value then builtins.concatLists (map (checkQuadletReference types) value) else let type = lib.findFirst (t: lib.hasInfix ".${t}" value) null types; in if (type != null) then let quadletName = extractQuadletReference type value; quadletsOfType = lib.filterAttrs ( n: v: v.quadletData.resourceType == type ) cfg.internal.builtQuadlets; in if (lib.hasAttr quadletName quadletsOfType) then [ (lib.replaceStrings [ quadletName ] [ "podman-${quadletName}" ] value) ] else [ value ] else if ((lib.hasInfix "/nix/store" value) == false && lib.hasAttr value cfg.internal.builtQuadlets) then lib.warn '' A value for Podman container '${name}' might use a reference to another quadlet: ${value}. Append the type '.${ cfg.internal.builtQuadlets.${value}.quadletData.resourceType }' to '${lib.baseName value}' if this is intended. '' [ value ] else [ value ]; quadlet = ( podman-lib.deepMerge { Container = { AddCapability = containerDef.addCapabilities; AddDevice = containerDef.devices; AutoUpdate = containerDef.autoUpdate; ContainerName = name; DropCapability = containerDef.dropCapabilities; Entrypoint = containerDef.entrypoint; Environment = containerDef.environment; EnvironmentFile = containerDef.environmentFile; Exec = containerDef.exec; Group = containerDef.group; Image = checkQuadletReference [ "build" "image" ] containerDef.image; IP = containerDef.ip4; IP6 = containerDef.ip6; Label = (containerDef.labels // { "nix.home-manager.managed" = true; }); Network = checkQuadletReference [ "network" ] containerDef.network; NetworkAlias = containerDef.networkAlias; PodmanArgs = containerDef.extraPodmanArgs; PublishPort = containerDef.ports; UserNS = containerDef.userNS; User = containerDef.user; Volume = checkQuadletReference [ "volume" ] containerDef.volumes; }; Install = { WantedBy = lib.optionals containerDef.autoStart [ "default.target" "multi-user.target" ]; }; Service = { Environment = { PATH = ( builtins.concatStringsSep ":" [ "/run/wrappers/bin" "/run/current-system/sw/bin" "${pkgs.nftables}/bin" "${config.home.homeDirectory}/.nix-profile/bin" "${pkgs.systemd}/bin" ] ); }; Restart = "always"; TimeoutStopSec = 30; }; Unit = { Description = ( if (builtins.isString containerDef.description) then containerDef.description else "Service for container ${name}" ); }; } containerDef.extraConfig ); in { dependencies = dependencyServices; attrs = quadlet; text = '' # Automatically generated by home-manager podman container configuration # DO NOT EDIT THIS FILE DIRECTLY # # ${name}.container ${podman-lib.toQuadletIni quadlet} ''; }; toQuadletInternal = name: containerDef: let src = createQuadletSource name containerDef; in { assertions = podman-lib.buildConfigAsserts name containerDef.extraConfig; dependencies = src.dependencies; resourceType = "container"; serviceName = "podman-${src.attrs.Container.ContainerName}"; # generated service name: 'podman-.service' source = podman-lib.removeBlankLines src.text; }; # Define the container user type as the user interface containerDefinitionType = types.submodule { options = { addCapabilities = mkOption { type = with types; listOf str; default = [ ]; example = [ "CAP_DAC_OVERRIDE" "CAP_IPC_OWNER" ]; description = "The capabilities to add to the container."; }; autoStart = mkOption { type = types.bool; default = true; description = '' Whether to start the container on boot (requires user lingering). ''; }; autoUpdate = mkOption { type = types.enum [ null "registry" "local" ]; default = null; example = "registry"; description = "The autoupdate policy for the container."; }; description = mkOption { type = with types; nullOr str; default = null; example = "My Container"; description = "The description of the container."; }; devices = mkOption { type = with types; listOf str; default = [ ]; example = [ "/dev/:/dev/" ]; description = "The devices to mount into the container"; }; dropCapabilities = mkOption { type = with types; listOf str; default = [ ]; example = [ "CAP_DAC_OVERRIDE" "CAP_IPC_OWNER" ]; description = "The capabilities to drop from the container."; }; entrypoint = mkOption { type = with types; nullOr str; default = null; example = "/foo.sh"; description = "The container entrypoint."; }; environment = mkOption { type = podman-lib.primitiveAttrs; default = { }; example = lib.literalExpression '' { VAR1 = "0:100"; VAR2 = true; VAR3 = 5; } ''; description = "Environment variables to set in the container."; }; environmentFile = mkOption { type = with types; listOf str; default = [ ]; example = [ "/etc/environment" "/etc/other-env" ]; description = '' Paths to files containing container environment variables. ''; }; exec = mkOption { type = with types; nullOr str; default = null; example = "sleep inf"; description = "The command to run after the container start."; }; extraPodmanArgs = mkOption { type = types.listOf types.str; default = [ ]; example = [ "--security-opt=no-new-privileges" "--security-opt=seccomp=unconfined" ]; description = "Extra arguments to pass to the podman run command."; }; extraConfig = mkOption { type = podman-lib.extraConfigType; default = { }; example = lib.literalExpression '' { Container = { User = 1000; }; Service = { TimeoutStartSec = 15; }; } ''; description = '' INI sections and values to populate the Container Quadlet. ''; }; group = mkOption { type = with types; nullOr (either int str); default = null; description = "The group ID inside the container."; }; image = mkOption { type = types.str; example = "registry.access.redhat.com/ubi9-minimal:latest"; description = "The container image."; }; ip4 = mkOption { type = with types; nullOr str; default = null; description = "Set an IPv4 address for the container."; }; ip6 = mkOption { type = with types; nullOr str; default = null; description = "Set an IPv6 address for the container."; }; labels = mkOption { type = with types; attrsOf str; default = { }; example = { app = "myapp"; some-label = "somelabel"; }; description = "The labels to apply to the container."; }; network = mkOption { type = with types; either str (listOf str); default = [ ]; apply = value: if lib.isString value then [ value ] else value; example = lib.literalMD '' `"host"` or `"bridge_network_1"` or `[ "bridge_network_1" "bridge_network_2" ]` ''; description = '' The network mode or network/s to connect the container to. Equivalent to `podman run --network=