diff options
Diffstat (limited to 'modules/foundation/services/services.nix')
| -rw-r--r-- | modules/foundation/services/services.nix | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/modules/foundation/services/services.nix b/modules/foundation/services/services.nix new file mode 100644 index 0000000..d9489aa --- /dev/null +++ b/modules/foundation/services/services.nix @@ -0,0 +1,326 @@ +{ + 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) + ); + }; + }; +} |
