summary refs log tree commit diff
path: root/modules/foundation/services/services.nix
diff options
context:
space:
mode:
Diffstat (limited to 'modules/foundation/services/services.nix')
-rw-r--r--modules/foundation/services/services.nix326
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)
+          );
+        };
+    };
+}