{ pkgs, lib, config, ... }: let cfg = config.container-services; healthcheck = with pkgs.lib; { test = mkOption { type = types.oneOf types.str (types.listOf types.str); }; interval = mkOption { type = types.str; default = "30s"; }; startupInterval = mkOption { type = types.str; default = "30s"; }; startPeriod = mkOption { type = types.str; default = "0s"; }; }; service = with pkgs.lib; { name = mkOption { type = types.nilOr types.str; default = nil; }; alias = mkOption { type = types.nilOr types.str; default = nil; }; dependsOn = mkOption { type = types.listOf service; default = [ ]; }; image = mkOption { type = types.str; }; volumes = mkOption { type = types.listOf types.str; default = [ ]; }; ports = mkOption { type = types.listOf types.str; default = [ ]; }; environment = mkOption { type = types.attrsOf types.str; default = { }; }; environmentFiles = mkOption { type = types.listOf types.str; default = [ ]; }; cmd = mkOption { type = types.nilOr (types.listOf types.str); default = nil; }; healthCheck = mkOption { type = types.nilOr (types.submodule healthcheck); default = nil; }; }; pod = with pkgs.lib; { name = mkOption { type = types.nilOr str; default = nil; }; description = mkOption { type = types.nilOr str; default = nil; }; services = mkOption { type = types.attrsOf (types.submodule service); default = { }; }; }; backend = config.virtualisation.oci-containers.backend; volumeName = pod: name: "${pod.name}-${name}"; volumeServiceName = pod: name: "${backend}-volume-${volumeName pod name}"; volumeServiceRef = pod: name: config.systemd.services."${backend}-volume-${volumeName pod name}".name + ".service"; serviceName = pod: service: "${backend}-${pod.name}-${service.name}"; serviceRef = pod: service: config.systemd.services."${backend}-${pod.name}-${service.name}".name + ".service"; networkName = pod: "${pod.name}-default"; networkServiceName = pod: "${backend}-network-${pod.name}"; networkServiceRef = pod: config.systemd.services."${backend}-network-${pod.name}".name + ".service"; podName = pod: "${backend}-pod-${pod.name}"; podRef = pod: config.systemd.targets."${backend}-pod-${pod.name}".name + ".target"; namedVolumes = service: lib.filter (volume: (! ((lib.hasPrefix "/" volume) || (lib.hasPrefix "./" volume)))) (lib.map (volume: lib.head (lib.splitString ":" volume)) service.volumes); oneShotService = { pod, description, script }: { path = [ (if backend == "podman" then pkgs.podman else pkgs.docker) ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; inherit description; inherit script; partOf = [ podRef pod ]; wantedBy = [ podRef pod ]; }; in { options = with pkgs.lib; { container-services = mkOption { type = types.attrsOf (types.submodule pod); default = { }; }; }; config = { container-services = lib.mapAttrs (name: pod: { name = lib.mkDefault pod.name; services = lib.mapAttrs (name: service: { name = lib.mkDefault name; }) pod.services; }) cfg; systemd.targets = lib.mapAttrs' (_: pod: lib.nameValuePair (podName pod) { wantedBy = [ "multi-user.target" ]; unitConfig = { Description = pod.description or pod.name; }; } ); systemd.services = lib.foldl (services: pod: lib.mergeAttrsList [ services { "${networkServiceName pod}" = oneShotService { description = "Network for ${pod.name}"; script = '' ${backend} network inspect ${networkName pod} || ${backend} network create ${networkName pod} ''; }; } (lib.mapAttrs' (_: service: let dependencies = (lib.map (service: serviceRef pod.name service.name) service.dependsOn) ++ (lib.map (name: volumeServiceRef pod.name name) (namedVolumes service)) ++ (networkServiceRef pod); in lib.nameValuePair (serviceName pod service) { serviceConfig = { Restart = lib.mkOverride 500 "always"; }; after = dependencies; requires = dependencies; partOf = [ (podRef pod) ]; wantedBy = [ (podRef pod) ]; } ) pod.services) (lib.listToAttrs (lib.flatten ( lib.map (service: ( lib.map (volume: { name = volumeServiceName pod volume; value = oneShotService { description = "Volume ${volume} of ${pod.name}"; script = '' ${backend} volume inspect ${volumeName pod volume} || ${backend} volume create ${volumeName pod volume} ''; }; }) namedVolumes) ) pod.services))) ]) { } cfg; virtualisation.oci-containers.containers = lib.foldl (containers: pod: containers // ( lib.mapAttrs' (_: service: lib.nameValuePair (serviceName pod service) { image = service.image; volumes = service.volumes; log-driver = "journal"; ports = service.ports; environment = service.environment; environmentFiles = service.environmentFiles; cmd = service.cmd; extraOptions = [ "--network-alias=${service.alias or service.name}" "--network=${networkName pod}" ] ++ (if (service.healthCheck != null) then [ "--health-cmd=${ if (builtins.isList service.healthCheck.test) then builtins.toJSON service.healthCheck.test else service.healthCheck.test }" "--health-interval=${service.healthCheck.interval}" "--health-startup-interval=${service.healthCheck.startupInterval}" "--health-start-period=${service.healthCheck.startPeriod}" ] else [ ]); } ) )) { } cfg; }; }