diary @ telent

Full service wash#

Sun 23 Jul 2023 08:21:00 AM UTC

Topics: liminix

Once upon a time I wrote

With that in mind, I’ve decided to defer the redesign of modules until later down the line when I also need to do some serious thinking about services. There’s no point considering one without the other.

and today I'm pleased to say I think I have a plan. To recap the problem:

So how do we resolve this? We're taking the obvious-in-hindsight path: do both! Where a service depends on global state (e.g. firewall support depends on kernel nftables options) then instead of making that service globally accessible in pkgs, we define it in a module that specifies the required state. Including the module does not create any services in itself, but makes the service definition available in config.system.service.firewall. So you do

  imports = [
    ../modules/firewall
  ];

  services.firewall = config.system.service.firewall {
    ruleset = import ./my-firewall-ruleset.nix;
  };

If you don't add the import, there is no config.system.service.firewall, so no risk of accidentally adding the service without its prerequisite global config.

The other half of making this user-friendly is that we typeckeck the service definition parameters, using the NixOS module type system. For example, in the dnsmasq service we see

  t = {
    user = mkOption {
      type = types.str;
      default = "dnsmasq";
    };
    group = mkOption {
      type = types.str;
      default = "dnsmasq";
    };
    resolvconf = mkOption {
      type = types.nullOr liminix.lib.types.service;
      default = null;
    };
    interface = mkOption {
      type = liminix.lib.types.service;
      default = null;
    };
    upstreams = mkOption {
      type = types.listOf types.str;
      default = [];
    };
    ranges = mkOption {
      type = types.listOf types.str;
    };
    domain = mkOption {
      type = types.str;
    };
  };

(Some of these types are still a bit "loose": we could use a type for IP addresses/networks, and a type for "s6 service that describes a network interface" instead of "any s6 service". And I've been very slack about adding description fields, but they will be super-useful too when we get to the point of producing documentation from these definitions.)

As a related cleanup, we moved config.outputs to config.system.outputs, because it's not right to be spattering derivations and build products all over the "config" namespace that rightfully ought to be for configuration. (By that token config.system is still philosophically a misuse of config, but it's the only misuse of config we're going to have: everything we want to carry around in our modules is going to live under that prefix). We also now use the module type system to specify the outputs by name instead of just having a grab bag that any module can add new names to. (I am grateful to Samuel Dionne-Riel here for the description and reasoning in mobile-nixos#406 describing the Mobile NixOS switch from system.build to mobile.outputs)

Current status: this is in progress