diff options
| author | Mel <mel@rnrd.eu> | 2025-08-31 17:24:03 +0200 |
|---|---|---|
| committer | Mel <mel@rnrd.eu> | 2025-08-31 17:24:03 +0200 |
| commit | fcbc0446f11b8555c1204081c23fbd1442534aa0 (patch) | |
| tree | 4479bef5a2bd80987add6c34440fee4ba894abed /modules/foundation/services | |
| parent | 72ed2e170f32698f8a8596532c1d7655591267c3 (diff) | |
| download | network-fcbc0446f11b8555c1204081c23fbd1442534aa0.tar.zst network-fcbc0446f11b8555c1204081c23fbd1442534aa0.zip | |
Clean up & integrate service network configuration into foundation module
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'modules/foundation/services')
| -rw-r--r-- | modules/foundation/services/default.nix | 16 | ||||
| -rw-r--r-- | modules/foundation/services/networks.nix | 215 | ||||
| -rw-r--r-- | modules/foundation/services/services.nix | 326 | ||||
| -rw-r--r-- | modules/foundation/services/utils.nix | 29 |
4 files changed, 586 insertions, 0 deletions
diff --git a/modules/foundation/services/default.nix b/modules/foundation/services/default.nix new file mode 100644 index 0000000..25477b1 --- /dev/null +++ b/modules/foundation/services/default.nix @@ -0,0 +1,16 @@ +{ ... }: + +{ + imports = [ + ./services.nix + ./networks.nix + ]; + + foundation.networks.foundation-default = { + enable = true; + default = true; + + subnet = "2001:d0c:1::/48"; + driver = "bridge"; + }; +} diff --git a/modules/foundation/services/networks.nix b/modules/foundation/services/networks.nix new file mode 100644 index 0000000..d1f1a92 --- /dev/null +++ b/modules/foundation/services/networks.nix @@ -0,0 +1,215 @@ +{ + lib, + config, + pkgs, + ... +}: + +let + utils = import ./utils.nix { inherit lib; }; + + inherit (lib) + mkOption + types + assertMsg + optional + getExe + concatStringsSep + filterAttrs + listToAttrs + attrsToList + imap0 + attrNames + nameValuePair + ; + inherit (utils) naming; + + cfg = config.foundation; +in +{ + options.foundation = + let + networkSubmodule = + with types; + submodule { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = "Should this network be created?"; + }; + + default = mkOption { + type = types.bool; + default = false; + description = '' + Should this network be the default for all services + that don't have an explicit network defined? + ''; + }; + + subnet = mkOption { + type = types.nullOr types.str; + description = '' + IPv6 subnet for this network in CIDR notation. + Don't set to get a random subnet assigned to you within + the subnet defined in `defaultSubnetPrefix`. + ''; + example = "2001:d0c:123::/64"; + default = null; + }; + + driver = mkOption { + type = types.str; + default = "bridge"; + description = "Docker network driver to use"; + }; + + options = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Additional options to pass to docker network create"; + }; + + serviceGroup = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The group of the network, if it belongs to one. + This makes the network service a part of the + group target. + ''; + }; + }; + }; + in + { + networks = mkOption { + type = types.attrsOf networkSubmodule; + description = "List of service networks to create"; + default = { }; + }; + + defaultNetwork = mkOption { + type = types.nullOr types.str; + description = '' + The default network for services. + If at least one network is marked as default, you can + find it's name here. + ''; + }; + + defaultSubnetPrefix = mkOption { + type = types.str; + description = '' + Default network subnet assigned to networks without + a set subnet. + Prefix length defined by `defaultSubnetLength`. + ''; + default = "2001:d0c"; + }; + + defaultSubnetLength = mkOption { + type = types.int; + description = '' + Default network subnet length assigned to networks without + a set subnet. + This should always be the length of the `defaultSubnetPrefix` + 16. + ''; + default = 48; + }; + }; + + config = lib.mkIf (cfg.networks != { }) { + # other modules can use this to look up which network + # they can use as the default! + foundation.defaultNetwork = + let + networksLabeledDefault = attrNames (filterAttrs (name: net: net.default) cfg.networks); + defaultsCount = builtins.length networksLabeledDefault; + in + assert assertMsg (defaultsCount <= 1) "multiple service networks labeled default"; + assert assertMsg (defaultsCount != 0) "no default service network"; + builtins.head networksLabeledDefault; + + # we need these settings to allow our networks to be IPv6-enabled. + # we also ignore the default bridge, because it's setup + # is a lot more complicated... + virtualisation.docker.daemon.settings = { + experimental = true; + ipv6 = true; + ip6tables = true; + fixed-cidr-v6 = "${cfg.defaultSubnetPrefix}:255::/${toString cfg.defaultSubnetLength}"; + }; + + boot.kernel.sysctl = { + "net.ipv6.conf.all.forwarding" = 1; + "net.ipv6.conf.default.forwarding" = 1; + }; + + systemd.services = + let + subnetOffset = 100; + subnetByIndex = + i: + "${cfg.defaultSubnetPrefix}:${toString (subnetOffset + i)}::/${toString cfg.defaultSubnetLength}"; + + # this could be moved out into library functions, it's pretty useful. + # mapAttrsIndexed' :: (Int -> String -> AttrSet -> { name:: String; value :: Any; }) -> AttrSet -> AttrSet + mapAttrsIndexed' = f: attrs: listToAttrs (imap0 (i: v: f i v.name v.value) (attrsToList attrs)); + # mapAttrsIndexed :: (Int -> String -> AttrSet -> Any) -> AttrSet -> AttrSet + mapAttrsIndexed = + f: attrs: + mapAttrsIndexed' ( + i: n: v: + nameValuePair v.name (f i v.name v.value) + ) attrs; + + networkService = + index: name: network: + let + docker = getExe pkgs.docker; + options = concatStringsSep " " network.options; + subnet = if network.subnet == null then subnetByIndex index else network.subnet; + in + { + description = "Docker service network '${name}'"; + after = [ "docker.service" ]; + requires = [ "docker.service" ]; + + wantedBy = [ "multi-user.target" ]; + partOf = optional ( + network.serviceGroup != null + ) "${naming.groupTarget network.serviceGroup}.target"; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + + ExecStart = pkgs.writeShellScript "create-docker-network-${name}" '' + if ${docker} network inspect ${name} >/dev/null 2>&1; then + ${docker} network rm ${name} + fi + + ${docker} network create \ + --ipv6 \ + --subnet=${subnet} \ + --driver=${network.driver} \ + ${options} \ + ${name} + ''; + + ExecStop = pkgs.writeShellScript "delete-docker-network-${name}" '' + ${docker} network rm -f ${name} + ''; + }; + }; + + mkNetwork = index: name: network: { + name = naming.networkService name; + value = networkService index name network; + }; + in + mapAttrsIndexed' mkNetwork (filterAttrs (name: net: net.enable) cfg.networks); + }; +} 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) + ); + }; + }; +} diff --git a/modules/foundation/services/utils.nix b/modules/foundation/services/utils.nix new file mode 100644 index 0000000..c7bbcf7 --- /dev/null +++ b/modules/foundation/services/utils.nix @@ -0,0 +1,29 @@ +{ ... }: + +{ + naming = { + networkService = name: "docker-${name}-network"; + + groupTarget = group: "docker-${group}-group-root"; + + serviceService = fullName: "docker-${fullName}"; + + service = + { + 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; + }; +} |
