Skip to content

Style guide

A fleet is many small modules. Each one is short when it does one job and names each value once. This page shows one small fleet in the style the docs and templates use, then the same fleet written in the style to avoid. Both give the same configuration, and a check on this page compares them.

A kind is a word of your fleet. Put the kinds in one file, apart from the data:

modules/kinds.nix
# The words of this fleet. A child names its parent in a ref field, and `pointing` spawns it.
{ yen, ... }:
{
yen.kinds.fleet = {
spawns.env = _: yen.env;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.env.spawns.host = yen.lib.pointing "env" yen.host; # (1)
yen.kinds.host = {
fields = {
env = yen.kinds.env; # (2)
ports = yen.lib.opt (yen.lib.listOf yen.lib.port) [ ]; # (3)
};
spawns.user = yen.lib.pointing "hosts" yen.user;
sinks.nixos = {
reduce = yen.lib.reduce.merge;
out =
{ scope, sinks, ... }:
{
config.${scope.name} = sinks.nixos;
};
};
};
yen.kinds.user.fields.hosts = yen.lib.listOf yen.kinds.host;
}

(1) A host names its environment in its env field, so spawns does not filter by hand. yen.lib.pointing "env" yen.host gives the hosts whose env field holds the scope, as one ref or in a list. (2) The field type is the kind itself: the field holds a ref, never a name. (3) Use the types of yen.lib. A nixpkgs composite type checks only the outer shape.

A value that two files use goes in yen.helpers. Any module reads it from its yen arg:

modules/helpers.nix
# The combinators this fleet reuses, named once in `yen.helpers`: any file reads them.
{ yen, ... }:
{
yen.helpers = {
# a predicate: a host in prod that has a user
servesPeople = yen.lib.all [
# (1)
(yen.lib.within yen.env.prod)
(yen.lib.below yen.kinds.user)
];
# a value: the NixOS firewall that opens these ports
openPorts = ports: { networking.firewall.allowedTCPPorts = ports; }; # (2)
};
}

(1) A predicate built from the combinators all, within and below: a host in prod that has a user. Name it once, and every at that needs it reads servesPeople. (2) A function that gives a NixOS value. An aspect calls it with its own data.

Each file has one let, before its attributes. A binding that needs a value of an element (a scope, a host, a tree answer) is a function of that value. The attributes below the let only assign:

modules/aspects.nix
# The aspects. One `let` at the top: named bindings, each with a comment when its name is not enough.
# The attributes below only assign them.
{ yen, ... }:
let
inherit (yen.helpers) servesPeople openPorts; # (1)
hosts.instances = yen.kinds.host; # (2) a tree request is data: name it once, here
# the names of the other hosts in the environment of host h, from the answer of the tree request
peersOf = # (3)
h: all: map (p: p.name) (builtins.filter (p: !yen.lib.same p h && yen.lib.same p.env h.env) all);
in
{
yen.aspects.firewall = {
at = yen.kinds.host;
nixos = { host, ... }: openPorts host.ports;
};
yen.aspects.ssh = {
at = servesPeople;
nixos.services.openssh.enable = true;
};
yen.aspects.peers = {
at = yen.kinds.host;
cycle =
{ tree, scope, ... }:
{
tree = [ hosts ];
nixos = [ { networking.hosts."10.0.0.1" = peersOf scope (tree.select hosts); } ]; # (4)
};
};
}

(1) Read the helpers. (2) A tree request is data, so it is a binding: the cycle asks it and selects it by the same name. (3) A function of the host and of the request’s answer, with a comment that says what it gives. (4) The cycle only calls it.

An expensive value (a nixosSystem, a stage, a graph) is computed once: pass it as an argument to the function that reads it, and do not call the function that makes it twice.

The data says which instance it means with a ref, not with a string:

modules/instances.nix
# The data: refs, never names.
{ yen, ... }:
{
yen.fleet.main = { };
yen.env = {
prod = { };
lab = { };
};
yen.host = {
web = {
env = yen.env.prod; # (1) a ref: the value is the instance, not a string
ports = [ 443 ];
};
db = {
env = yen.env.prod;
ports = [ 5432 ];
};
box.env = yen.env.lab;
};
yen.user.ana.hosts = [
# (2)
yen.host.web
yen.host.box
];
yen.flake = { out, ... }: out;
}

At (1), yen.env.prod is a ref, not the string "prod". At (2), a user lists refs to hosts. A ref has no typo to make: a wrong one is an error record.

Here is the same fleet in one file. It works, and it gives the same configuration:

before/fleet.nix
# The same fleet, in the style to avoid: names as strings, spawns by hand, a nested `let`, `with`, and a predicate
# written by hand. It gives the same flake.
{ yen, lib, ... }:
{
yen.kinds.fleet = {
spawns.env = _: yen.env;
sinks.out.reduce = yen.lib.reduce.merge;
};
yen.kinds.env.spawns.host = s: lib.filterAttrs (_: h: h.env == s.name) yen.host;
yen.kinds.host = {
fields = {
env = yen.lib.str;
ports = yen.lib.opt (yen.lib.listOf yen.lib.port) [ ];
};
spawns.user = s: lib.filterAttrs (_: u: lib.elem s.name u.hosts) yen.user;
sinks.nixos = {
reduce = yen.lib.reduce.merge;
out =
{ scope, sinks, ... }:
{
config.${scope.name} = sinks.nixos;
};
};
};
yen.kinds.user.fields.hosts = yen.lib.listOf yen.lib.str;
yen.aspects.firewall = {
at = yen.kinds.host;
nixos =
{ host, ... }:
let
ports = host.ports;
in
{
networking.firewall.allowedTCPPorts = ports;
};
};
yen.aspects.ssh = {
at = yen.lib.all [
(
s:
let
envOf =
x:
if x.parent == null then
null
else if x.parent.kind == "env" then
x.parent.name
else
envOf x.parent;
in
envOf s == "prod"
)
(yen.lib.below yen.kinds.user)
];
nixos.services.openssh.enable = true;
};
yen.aspects.peers = {
at = yen.kinds.host;
cycle =
{ tree, scope, ... }:
let
hosts.instances = yen.kinds.host;
in
{
tree = [ hosts ];
nixos = [
{
networking.hosts."10.0.0.1" =
with builtins;
map (p: p.name) (filter (p: p.name != scope.name && p.env == scope.env) (tree.select hosts));
}
];
};
};
yen.fleet.main = { };
yen.env = {
prod = { };
lab = { };
};
yen.host = {
web = {
env = "prod";
ports = [ 443 ];
};
db = {
env = "prod";
ports = [ 5432 ];
};
box.env = "lab";
};
yen.user.ana.hosts = [
"web"
"box"
];
yen.flake = { out, ... }: out;
}
result
{
box = {
networking = {
firewall = {
allowedTCPPorts = [ ];
};
hosts = {
"10.0.0.1" = [ ];
};
};
};
db = {
networking = {
firewall = {
allowedTCPPorts = [
5432
];
};
hosts = {
"10.0.0.1" = [
"web"
];
};
};
};
web = {
networking = {
firewall = {
allowedTCPPorts = [
443
];
};
hosts = {
"10.0.0.1" = [
"db"
];
};
};
services = {
openssh = {
enable = true;
};
};
};
}
Avoid Use
a name as a string: env = "prod" a ref: env = yen.env.prod, with a field typed by the kind
a spawns that filters by hand yen.lib.pointing "<field>" yen.<kind>
a predicate that walks parent by hand the combinators all, not, within and below, named once in yen.helpers
a let inside an element, an aspect or a function one let at the top of the file; a function of the value it needs
a tree request declared inside the cycle a binding in the top let
with and rec name each attribute; let bindings refer to each other without rec
the same value computed in each call compute it once and pass it as an argument
ContributeCommunitySponsor