{ lib, config, pkgs, ... }: let inherit (lib) mkOption types; serviceOptions = { options = { image = mkOption { type = types.nullOr types.package; default = null; }; fullImage = mkOption { type = with types; nullOr (submodule { options = { image = mkOption { type = str; }; imageFile = mkOption { type = package; }; base = mkOption { type = nullOr anything; }; }; }); default = null; }; ports = mkOption { type = with types; listOf (listOf ints.u16); default = [ ]; }; volumes = mkOption { type = with types; listOf (listOf str); default = [ ]; }; entrypoint = mkOption { type = types.nullOr types.str; default = null; }; cmd = mkOption { type = with types; listOf str; default = [ ]; }; workdir = mkOption { type = types.nullOr types.str; default = null; }; environment = mkOption { type = types.attrsOf types.str; default = { }; }; environmentFiles = mkOption { type = types.listOf types.path; default = [ ]; }; }; }; cfg = config.foundation; mkName = { group, name, full ? false, }: let isGroup = group != ""; isDefault = name == "default" || name == group; shortName = if isGroup && isDefault then group else name; fullName = if isGroup then (if isDefault then group else "${group}-${name}") else name; in assert name != ""; assert isDefault -> isGroup; if full then fullName else shortName; processServices = group: serviceSet: lib.mapAttrsToList ( name: c: { name = mkName { inherit name group; }; fullName = mkName { inherit name group; full = true; }; inherit group; } // c ) serviceSet; singleServices = processServices "" cfg.services; groupedServices = lib.mapAttrs ( group: groupServices: processServices group groupServices ) cfg.service; allServices = let allSingleServices = singleServices; allGroupedServices = lib.flatten (lib.attrValues groupedServices); in allSingleServices ++ allGroupedServices; groupStructure = lib.mapAttrs ( group: groupServices: lib.catAttrs "fullName" groupServices ) groupedServices; in { options.foundation = { service = mkOption { type = with types; attrsOf (attrsOf (submodule serviceOptions)); default = { }; }; services = mkOption { type = with types; attrsOf (submodule serviceOptions); default = { }; }; }; config = lib.mkIf (cfg.service != { } || cfg.services != { }) { virtualisation.oci-containers.containers = let mkOciPort = portTuple: let host = builtins.elemAt portTuple 0; container = builtins.elemAt portTuple 1; in "127.0.0.1:${toString host}:${toString container}"; mkOciVolume = volumeTuple: let hostPath = builtins.elemAt volumeTuple 0; containerPath = builtins.elemAt volumeTuple 1; in "${hostPath}:${containerPath}"; mkImage = { oldImage, imageStream, }: if oldImage != null then { inherit (oldImage) image imageFile; } else if imageStream != null then { inherit imageStream; } else throw "can't use both `fullImage` and `image` together."; mkOciContainer = { name, fullImage, image, ports, volumes, entrypoint ? null, cmd ? null, workdir ? null, environment ? null, environmentFiles ? null, group, ... }: { inherit entrypoint cmd workdir environment environmentFiles ; ports = map mkOciPort ports; volumes = map mkOciVolume volumes; extraOptions = lib.mkIf (group != "") [ "--network-alias=${name}" "--network=${group}" ]; } // (mkImage { oldImage = fullImage; imageStream = image; }); in builtins.listToAttrs (map (v: lib.nameValuePair v.fullName (mkOciContainer v)) allServices); systemd = let mkGroupRootTargetName = group: "docker-${group}-root"; mkServiceName = fullName: "docker-${fullName}"; mkNetworkServiceName = group: "docker-${group}-network"; genericContainerService = group: let network = "${mkNetworkServiceName group}.service"; root = "${mkGroupRootTargetName group}.target"; in { serviceConfig = { Restart = lib.mkOverride 90 "always"; RestartMaxDelaySec = lib.mkOverride 90 "1m"; RestartSec = lib.mkOverride 90 "100ms"; RestartSteps = lib.mkOverride 90 9; }; after = [ network ]; requires = [ network ]; partOf = [ root ]; wantedBy = [ root ]; }; networkService = group: let root = "${mkGroupRootTargetName group}.target"; in { path = [ pkgs.docker ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStop = "docker network rm -f ${group}"; }; script = '' docker network inspect ${group} || docker network create ${group} --driver=bridge ''; partOf = [ root ]; wantedBy = [ root ]; }; in { services = lib.concatMapAttrs ( group: serviceNames: { ${mkNetworkServiceName group} = networkService group; } // lib.genAttrs (map mkServiceName serviceNames) (_v: genericContainerService group) ) groupStructure; targets = lib.mapAttrs' ( group: _v: lib.nameValuePair (mkGroupRootTargetName group) { wantedBy = [ "multi-user.target" ]; } ) groupStructure; }; }; }