Generic batteries
The other pages of these docs write every rule by hand, so you can see how yen works. Some rules come back in
every fleet: each host reads the keys of the other hosts, each address has one owner, sections of a file come
in an order. yen.lib.batteries has them ready.
A battery never adds a word to your fleet. You write every key, with your own names. The battery is the
value. It reads only the fixed args scope and sinks, and it takes the sinks it reads as refs.
Here are the words of the example: a fleet of hosts. Each host has an ssh key and an address. The batteries know none of these names.
# Your words: a fleet of hosts. Each host has an ssh key and an address. The batteries know none of these names.{ yen, lib, ... }:let str = yen.lib.str;in{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.host.fields = { sshKey = str; addr = str; }; yen.fleet.main = { }; yen.flake = { out, ... }: out;
yen.host = { nas = { sshKey = "ssh-ed25519 AAAAnas"; addr = "10.0.0.1"; }; desk = { sshKey = "ssh-ed25519 AAAAdesk"; addr = "10.0.0.2"; }; laptop = { sshKey = "ssh-ed25519 AAAAlaptop"; addr = "10.0.0.3"; }; };}What the other hosts give
Section titled “What the other hosts give”Each host gives its key to the fleet. The fleet reduces with elements, so each key keeps the scope that gave
it. Each host then reads the keys of the others. This is the same rule as known_hosts, WireGuard peers,
Syncthing devices or remote builders, written by hand in Facts from other hosts.
# mesh: every host gives its key to the fleet; each host reads the keys of the others.{ yen, ... }:let inherit (yen.lib.batteries) mesh;in{ yen.kinds.fleet.sinks.keys.reduce = yen.lib.reduce.elements; # (1) an element knows which host gave it yen.kinds.host.sinks.known = { reduce = yen.lib.reduce.one; # (2) out = { scope, sinks, ... }: { known.${scope.name} = sinks.known; }; };
yen.aspects.key = { at = yen.kinds.host; keys = { host, ... }: host.sshKey; }; yen.aspects.known-hosts = { at = yen.kinds.host; known = mesh { # (3) from = yen.kinds.fleet.sinks.keys; build = keys: builtins.sort builtins.lessThan keys; }; };}(1) is the sink of the fleet. (2) is the sink of each host: its reduce is one, so a host has exactly one value.
At (3), mesh gives the value of the key known. It is the records of sink from, except the ones that
this scope or a scope under it gave. build makes content of them: here, a sorted list.
Each host has the keys of the other two:
{ desk = [ "ssh-ed25519 AAAAlaptop" "ssh-ed25519 AAAAnas" ]; laptop = [ "ssh-ed25519 AAAAdesk" "ssh-ed25519 AAAAnas" ]; nas = [ "ssh-ed25519 AAAAdesk" "ssh-ed25519 AAAAlaptop" ];}One owner per key
Section titled “One owner per key”# claim: each address has one owner. Two hosts with one address is an error, and the fleet has no outputs.{ yen, ... }:{ yen.kinds.fleet.sinks.addrs = { reduce = yen.lib.batteries.claim { key = addr: addr; }; # (1) out = { sinks, ... }: { addrs = builtins.attrNames sinks.addrs; }; }; yen.aspects.addr = { at = yen.kinds.host; addrs = { host, ... }: host.addr; # (2) };}At (1), claim is the reduce of the sink. Each key has one owner, and two owners is an error. At (2), every host
gives its address. When a new host takes the address of the nas, the fleet has an error and no outputs:
[ { code = "claim"; error = "claim: 10.0.0.1 has 2 owners: fleet:main/host:backup, fleet:main/host:nas"; from = "fleet:main"; sink = "addrs"; }]With rank, the highest value wins and there is no error: for example rank = claim: claim.priority, when two
virtual machines ask for one GPU.
Sections from many files
Section titled “Sections from many files”# ordered: README sections from any file, each with before and after. The battery finds one order.{ yen, ... }:{ yen.kinds.fleet.sinks.readme = { reduce = yen.lib.batteries.ordered; # (1) out = { sinks, ... }: { readme = builtins.concatStringsSep "\n" sinks.readme; }; }; yen.aspects.readme-intro = { at = yen.kinds.fleet; readme = { name = "intro"; value = "# My fleet"; }; }; yen.aspects.readme-hosts = { at = yen.kinds.fleet; readme = { name = "hosts"; after = [ "intro" ]; # (2) value = "## Hosts"; }; }; # (3) written later, in another file: it goes between the two yen.aspects.readme-usage = { at = yen.kinds.fleet; readme = { name = "usage"; after = [ "intro" ]; before = [ "hosts" ]; value = "## Usage"; }; };}At (1), ordered is the reduce of the sink. Its elements are { name; value; before; after; }, and it finds
an order that meets all of them. (2) puts a section after intro. (3) is written in another file and goes between the two:
'' # My fleet ## Usage ## Hosts''A cycle, a name that is given two times, or an unknown name in before or after is an error.
Other module systems
Section titled “Other module systems”A sink can collect modules of any module system: NixOS, flake-parts, sini/files, or plain lib.evalModules.
# eval-modules, configurations, imports: module systems other than yen's, fed by sinks.{ yen, lib, ... }:let inherit (yen.lib.batteries) eval-modules configurations imports;in{ # each host collects service settings as modules; eval-modules merges them with the module system yen.kinds.host.sinks.settings.out = configurations { # (1) from = yen.kinds.host.sinks.settings; output = "settings"; build = _: modules: (lib.evalModules { modules = [ { freeformType = lib.types.attrsOf lib.types.anything; } ] ++ modules; }).config; }; # the same with eval-modules, at the fleet: one report from every host's report module yen.kinds.fleet.sinks.report.out = eval-modules { # (2) from = yen.kinds.fleet.sinks.report; inherit lib; modules = [ { options.hosts = lib.mkOption { type = lib.types.listOf lib.types.str; }; } ]; result = e: { report = e.config; }; }; # imports: a sink of modules as one module, here for a docs sink yen.kinds.fleet.sinks.notes.out = { sinks, ... }: { notes = imports { from = yen.kinds.fleet.sinks.notes; } { inherit sinks; }; # (3) };
yen.aspects.ssh-settings = { at = yen.kinds.host; settings.ssh.port = 22; report = { host, ... }: { hosts = [ host.name ]; }; notes = "note"; }; yen.aspects.nas-settings = { at = yen.host.nas; settings.ssh.ciphers = [ "chacha20" ]; };}(1), configurations, gives one flake output per scope, <output>.<scope name>, from a builder you pass
(nixosSystem, darwinSystem, any function). (2), eval-modules, evaluates the modules with fixed modules
first. (3), imports, turns a sink of modules into one module for another sink. This is what they give:
{ report = { hosts = [ "nas" "laptop" "desk" ]; }; settings = { desk = { ssh = { port = 22; }; }; laptop = { ssh = { port = 22; }; }; nas = { ssh = { ciphers = [ "chacha20" ]; port = 22; }; }; };}Write your own
Section titled “Write your own”A battery is a plain Nix function. If none fits, write one in your fleet. Use the same shape, and it stays as reusable as these:
- Take the sinks you read as refs, such as
from = yen.kinds.fleet.sinks.keys, and read them withsinks.${from.__sink}. - Read the scope with
scope, never with an arg named after a kind. - Return a value. Do not write
yen.kinds.…oryen.aspects.…keys: those are the user’s names.
This is all of mesh:
mesh = { from, build }: { scope, sinks, ... }: build (map (e: e.value) (builtins.filter (e: !own scope e) sinks.${from.__sink}));All the generic batteries
Section titled “All the generic batteries”| Battery | Value of | Does |
|---|---|---|
mesh { from; build; } |
any key of an aspect | the records of sink from, except the ones that this scope or a scope under it gave; build makes content of them |
claim { key; rank ? ; } |
reduce of a sink |
each key has one owner; two owners is an error, unless rank picks one |
ordered |
reduce of a sink |
elements { name; value; before; after; } in an order that meets all of them |
perSystem.out { from; group; systemOf ? ; } |
out of a sink at a system scope |
the reduced sink as flake output <group>.<system> |
perSystem.inputs { inputs; system; } |
anywhere | each input read at one system: packages.<system> over legacyPackages.<system>, so (…).nixpkgs.hello |