{ 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 ( oneOf [(listOf port) str port] ); default = [ ]; }; volumes = mkOption { type = with types; listOf (listOf str); default = [ ]; }; devices = mkOption { type = with types; listOf str; default = [ ]; }; capabilities = mkOption { type = with types; 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 = [ ]; }; customNetwork = mkOption { type = types.nullOr types.str; default = null; }; }; }; 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 = portSetting: if builtins.isList portSetting then let host = builtins.elemAt portSetting 0; container = builtins.elemAt portSetting 1; in "127.0.0.1:${toString host}:${toString container}" else if builtins.isInt portSetting then "127.0.0.1:${toString portSetting}:${toString portSetting}" else portSetting; 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; image = "${imageStream.imageName}:${imageStream.imageTag}"; } else throw "can't use both `fullImage` and `image` together."; mkOciContainer = { name, fullImage, image, ports, volumes, devices, capabilities, entrypoint ? null, cmd ? null, workdir ? null, environment ? null, environmentFiles ? null, customNetwork ? null, group, ... }: { inherit entrypoint cmd workdir environment environmentFiles ; ports = map mkOciPort ports; volumes = map mkOciVolume volumes; extraOptions = let mapOptions = optionName: values: map (v: "--${optionName}=${v}") values; networkOptions = if customNetwork != null then [ "--network=${customNetwork}" ] else if group != "" then [ "--network-alias=${name}" "--network=${group}" ] else []; capabilityOptions = mapOptions "cap-add" capabilities; deviceOptions = mapOptions "device" devices; in networkOptions ++ capabilityOptions ++ deviceOptions; } // (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; }; }; }