Full service wash
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:
-
modules are (notionally, at least) functions that can change the global config, but are singletons - you can’t have an arbitrary number of “pptp” modules configured differently to each other, unless they agree between themselves on how to use
config
so that each knows not to look at attributes for the others. -
a service is just a derivation, so you can have as many services as you want and they’re independent of each other. But as a derivation it can’t affect global state - it can’t create a user, or add to the kernel config, or change the busybox applets.
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
-
There are still services inlined in rotuer.nix that need to be moved into modules
-
Likewise there are services in pkgs.liminix.networking that need the same treatment: we aim to get rid of pkgs/liminix-tools
-
more work on the actual types and less use of
str
andanything
types -
yet to do: figure out how to produce documentation from the module and service definitions and incorporate that into the manual
-
There are “glue” services in rotuer.nix that bridge one subsystem to another - for example, “acquire lan prefix”, which depends on the output of dhcp6 and controls the ipv6 address of the lan device. It’s not yet obvious to me where they should live.
-
some services (e.g. ssh, though that might change if dropbear gets privilege separation and needs a non-root user specified) have no real dependencies on any global state, but it’s still A Bit Weird to have services scattered between
config.system.service
andpkgs
, so maybe we’ll add them to thebase
module?