{ lib, config, ... }: let utils = import ./utils.nix { }; inherit (lib) mkOption mkIf mkOverride types mapAttrs mapAttrsToList catAttrs attrNames attrValues listToAttrs nameValuePair optional flatten ; inherit (utils) naming; cfg = config.foundation; in { options.foundation = let serviceSubmodule = types.submodule { 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 = [ ]; }; network = mkOption { type = types.nullOr types.str; default = null; description = '' Makes the container part of a network defined by `foundation.networks`. If null, the container network will either be that of the group it belongs to, or the default network as defined by `foundation.defaultNetwork`. If you need to connect the container to networks outside of the configuration defined ones, see: `customNetworkOption`. ''; }; customNetworkOption = mkOption { type = types.nullOr types.str; default = null; description = '' Overrides the normal group or default network with a custom network option. This makes the containers systemd service not depend on any networks defined in `foundation.networks`, so you have to ensure correct start-up order yourself. ''; }; }; }; in { service = mkOption { type = types.attrsOf (types.attrsOf serviceSubmodule); default = { }; }; services = mkOption { type = types.attrsOf serviceSubmodule; default = { }; }; }; config = let processServices = group: services: mapAttrsToList ( name: c: c // { name = naming.service { inherit name group; }; fullName = naming.service { inherit name group; full = true; }; network = if c.customNetworkOption != null then "" # overrides all network configuration else if c.network != null then c.network else if group != "" then group else cfg.defaultNetwork; inherit group; } ) services; singleServices = processServices "" cfg.services; groupedServices = mapAttrs (group: groupServices: processServices group groupServices) cfg.service; allServices = let allSingleServices = singleServices; allGroupedServices = flatten (attrValues groupedServices); in allSingleServices ++ allGroupedServices; groupToServices = mapAttrs ( group: groupServices: catAttrs "fullName" groupServices ) groupedServices; in 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, network, customNetworkOption ? null, 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 = let mapOptions = optionName: values: map (v: "--${optionName}=${v}") values; networkOptions = (optional (customNetworkOption != null) "--network=${customNetworkOption}") ++ (optional (network != "") "--network=${network}") ++ (optional (group != "" && customNetworkOption == null) "--network-alias=${name}"); # aliases not supported capabilityOptions = mapOptions "cap-add" capabilities; deviceOptions = mapOptions "device" devices; in networkOptions ++ capabilityOptions ++ deviceOptions; } // (mkImage { oldImage = fullImage; imageStream = image; }); in builtins.listToAttrs (map (v: nameValuePair v.fullName (mkOciContainer v)) allServices); # define networks for all groups foundation.networks = listToAttrs ( map ( group: nameValuePair group { enable = true; serviceGroup = group; } ) (lib.attrNames groupToServices) ); systemd = let containerService = service: let grouped = service.group != ""; networked = service.network != ""; network = "${naming.networkService service.network}.service"; group = "${naming.groupTarget service.group}.target"; in { serviceConfig = { Restart = mkOverride 90 "always"; RestartMaxDelaySec = mkOverride 90 "1m"; RestartSec = mkOverride 90 "100ms"; RestartSteps = mkOverride 90 9; }; after = optional networked network; requires = optional networked network; partOf = optional grouped group; wantedBy = optional grouped group; }; groupTarget = { wantedBy = [ "multi-user.target" ]; }; in { services = listToAttrs ( map ( service: nameValuePair (naming.serviceService service.fullName) (containerService service) ) allServices ); targets = listToAttrs ( map (group: nameValuePair (naming.groupTarget group) groupTarget) (attrNames groupToServices) ); }; }; }