{ lib, config, pkgs, ... }: let utils = import ./utils.nix { inherit lib; }; inherit (lib) mkOption types assertMsg optional optionalString 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"; }; mtu = mkOption { type = types.nullOr types.int; default = null; example = 1400; description = '' The MTU for this network. If null, we use the Docker default. ''; }; 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} \ ${optionalString (network.mtu != null) "--opt com.docker.network.driver.mtu=${toString network.mtu}"} \ ${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); }; }