{ config, lib, pkgs, ...}: with lib; let cfg = config.virtualisation.oci-containers; proxy_env = config.networking.proxy.envVars; runDirSetup = '' if [ -z ''${XDG_RUNTIME_DIR} ]; then export XDG_RUNTIME_DIR="/run/"; fi ''; podOptions = { ... }: { options = { ports = mkOption { type = with types; listOf str; default = []; description = lib.mdDoc '' Network ports to publish from the pod to the outer host. Valid formats: - `::` - `::` - `:` - `` Both `hostPort` and `podPort` can be specified as a range of ports. When specifying ranges for both, the number of pod ports in the range must match the number of host ports in the range. Example: `1234-1236:1234-1236/tcp` When specifying a range for `hostPort` only, the `podPort` must *not* be a range. In this case, the pod port is published somewhere within the specified `hostPort` range. Example: `1234-1236:1234/tcp` ''; example = literalExpression '' [ "8080:9000" ] ''; }; volumes = mkOption { type = with types; listOf str; default = []; description = lib.mdDoc '' List of volumes to attach to this pod. Note that this is a list of `"src:dst"` strings to allow for `src` to refer to `/nix/store` paths, which would be difficult with an attribute set. There are also a variety of mount options available as a third field; ''; example = literalExpression '' [ "volume_name:/path/inside/container" "/path/on/host:/path/inside/container" ] ''; }; enableInfra = mkOption { type = types.bool; default = true; }; infraImage = mkOption { type = types.str; default = ""; }; infraName = mkOption { type = types.str; default = ""; }; dependsOn = mkOption { type = with types; listOf str; default = []; description = lib.mdDoc '' Define which other containers this one depends on. They will be added to both After and Requires for the unit. Use the same name as the attribute under `virtualisation.oci-containers.containers`. ''; example = literalExpression '' virtualisation.oci-containers.containers = { node1 = {}; node2 = { dependsOn = [ "node1" ]; } } ''; }; contains = mkOption { type = with types; listOf str; default = []; description = lib.mkDoc '' Define what containers are contained within this pod, with shared ports and volumes as decribed above. ''; example = literalExpression '' virtualisation.oci-containers.containers = { containers = { node1 = {}; node2 = {}; }; pods = { nodes = { contains = [ "node1" "node2" ]; }; }; } ''; }; extraOptions = mkOption { type = with types; listOf str; default = []; description = lib.mdDoc "Extra options for {command}`${defaultBackend} run`."; example = literalExpression '' ["--network=host"] ''; }; autoStart = mkOption { type = types.bool; default = true; description = lib.mdDoc '' When enabled, the container is automatically started on boot. If this option is set to false, the container has to be started on-demand via its service. ''; }; }; }; mkService = name: pod: let dependsOn = map (x: "${cfg.backend}-${x}.service") pod.dependsOn; contains = map (x: "${cfg.backend}-${x}.service") pod.contains; escapedName = escapeShellArg "${name}_pod"; podPID = "$XDG_RUNTIME_DIR/${escapedName}.pid"; in rec { wantedBy = [] ++ optional (pod.autoStart) "multi-user.target"; after = dependsOn; before = contains; requires = dependsOn; wants = contains; environment = proxy_env; path = [ config.virtualisation.podman.package ]; preStart = runDirSetup + '' ${cfg.backend} pod rm --ignore -f --pod-id-file=${podPID} || true ''; script = runDirSetup + (concatStringsSep " \\\n " ([ "exec ${cfg.backend} pod create" "--name=${escapedName}" "--pod-id-file=${podPID}" "--replace" ] ++ map escapeShellArg pod.extraOptions ++ (if pod.enableInfra then ([ "--infra-conmon-pidfile=$XDG_RUNTIME_DIR/${escapedName}-infra.pid" (if (pod.infraImage == "") then "" else "--infra-image=${pod.infraImage}") "--infra-name=${escapedName}-infra" ] ++ (map (p: "-p ${p}") pod.ports) ++ (map (v: "-v ${v}") pod.volumes)) else [ "" ]) ++ ["--infra=${lib.trivial.boolToString pod.enableInfra}"] )); preStop = runDirSetup + "${cfg.backend} pod stop --ignore --pod-id-file=${podPID}"; postStop = runDirSetup + '' ${cfg.backend} pod rm --ignore -f --pod-id-file=${podPID} || true rm ${podPID} ''; serviceConfig = { RemainAfterExit="yes"; }; }; in { options.virtualisation.oci-containers.pods = mkOption { default = {}; type = types.attrsOf (types.submodule podOptions); description = lib.mdDoc "OCI (podman) Pods to run as a systemd service"; }; config = let merge = p: lib.lists.foldr (e1: e2: e1 // e2) {} p; joinPods = pods: lib.lists.foldr lib.attrsets.unionOfDisjoint {} (map merge (attrValues (mapAttrs (name: pod: map (cont: {"${cont}".extraOptions=["--pod=${name}_pod"];}) pod.contains) pods))); makeBinds = pods: lib.lists.foldr lib.attrsets.unionOfDisjoint {} (map merge (attrValues (mapAttrs (name: pod: map (cont: {"${cfg.backend}-${cont}".partOf=["${cfg.backend}_pod-${name}.service"];}) pod.contains) pods))); test = pkgs.writeText "THISISATEST" (joinPods cfg.pods); in lib.mkIf (cfg.pods != {}) { systemd.services = (mapAttrs' (n: v: nameValuePair "${cfg.backend}_pod-${n}" (mkService n v)) cfg.pods) // (makeBinds cfg.pods); virtualisation.oci-containers.containers = joinPods cfg.pods; }; }