Bundles of the library
A bundle is a kit whose assemblies make more assemblies. One homelab assembly names the server and the nodes,
and the base, wireguard, borg, monitoring and dns assemblies follow from those two roles. See
Kits batteries for kits, assemblies and roles. You import the kits a bundle uses, then write one assembly:
# A bundle is a kit whose assemblies make more assemblies: one `homelab` assembly names the server and the nodes,# and the base, wireguard, borg, monitoring and dns assemblies follow from those two roles. Import the kits it uses.{ yen, inputs, ... }:let inherit (yen) host kit; k = inputs.yen.lib.kits; shared = import ../shared.nix yen;in{ imports = [ # (1) k.homelab k.base k.disko k.facter k.sshd k.emergency-access (k.wireguard { root = ../vars; }) k.borg k.monitoring k.dns ]; yen.host = { lb.addr = "10.0.0.1"; web.addr = "10.0.0.2"; nas.addr = "10.0.0.3"; }; yen.assembly.lab = { kit = kit.homelab; # (2) roles = { server = { at = host.nas; settings = shared // { device = "/dev/sda /dev/sdb"; layout = yen.kit.disko.layouts.zfs-mirror; # (3) }; }; node = { # (4) at = [ host.lb host.web ]; settings = shared; }; }; };}(1)imports the bundlehomelaband the kits it uses. The parts ofbaseandborgcome with them:homelablists only its own.(2)is the kit of the assembly.labis the only assembly the fleet writes.(3)is a setting of the server role: one of the layouts of the kitdisko. A role’ssettingsmay also be a function of the machine, for a device per machine.(4)is the rolenode. Its machines arelbandweb.
The fleet’s words: hosts with an address, members of assemblies, and the sinks the library kits write to.
# The fleet's words: hosts with an address, members of assemblies, and the sinks the library kits write to.{ yen, lib, ... }:let kits = yen.lib.batteries.kits; inherit (yen.lib) opt holds; attrs = yen.lib.attrs; # the fleet's assemblies and the parts their kits add (a part lives under its assembly) assemblies = yen.lib.batteries.kits.assemblies (lib.attrValues yen.assembly); # (1) # tree requests: every assembly and host, with their data assemblyTree.instances = yen.kinds.assembly; hostTree.instances = yen.kinds.host; machineOf = s: if holds yen.kinds.member s then s.parent else s;in{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks = { out.reduce = yen.lib.reduce.merge; exports.reduce = kits.exports { isMember = holds yen.kinds.member; }; generators = { reduce = yen.lib.batteries.vars.generators { inherit machineOf; }; out = yen.lib.batteries.vars.vars-plan { from = yen.kinds.fleet.sinks.generators; machines = lib.attrNames yen.host; recipientsOf = _: [ "age1admin" ]; }; }; }; }; yen.kinds.host = { fields.addr = yen.lib.str; spawns.member = kits.members { inherit assemblies holds; }; # (2) # the NixOS keys of a host, as data sinks.disks.reduce = yen.lib.reduce.merge; # kit disko gives disks here # (3) a kit's `os` keys (sshd's server) are the host's NixOS; without this sink they are dropped sinks.os.nixos = { os, ... }: { imports = os; }; sinks.nixos = { reduce = yen.lib.reduce.merge; out = { scope, sinks, ... }: { config.${scope.name} = sinks.nixos; }; }; }; yen.kinds.member = { }; yen.kinds.kit.fields = { roles = opt attrs { }; constraints = opt attrs { }; exports = opt (yen.lib.listOf yen.lib.str) [ ]; adds = opt (yen.lib.is builtins.isFunction) (_: { }); }; yen.kinds.assembly.fields = { kit = yen.kinds.kit; roles = opt attrs { }; }; # the fleet is read through tree requests, never as refs: an aspect reads instance data only through args yen.aspects.contract = { at = yen.kinds.fleet; cycle = { tree, ... }: { tree = [ assemblyTree hostTree ]; errors = [ (kits.contract { # (4) assemblies = yen.lib.batteries.kits.assemblies (tree.select assemblyTree); machines = tree.select hostTree; inherit holds; }) ]; }; }; yen.aspects.settings-typed = { at = yen.kinds.member; errors = kits.settings-typed; # (5) }; # a generator that an export carries goes to the machines of its `deploy` predicate yen.aspects.generators-from-exports = { at = yen.kinds.host; generators = yen.lib.batteries.vars.from-exports { inherit holds; }; # (6) }; yen.fleet.main = { }; yen.flake = { out, ... }: out;}(1)expands the assemblies at every depth with the batterykits.assemblies. The parts of a bundle live under its assembly, keyed by a short name (base,wg), and are not names in the fleet.(2)gives each host one member scope for each (assembly, role) it takes.(3)is thenixossink of the host. A kit’soskeys go into it.(4)iskits.contract. It refuses two assemblies with one name.(5)checks the types of the settings.(6)gives a generator that an export carries to the machines of itsdeploypredicate.
The result
Section titled “The result”The members of each machine (a part is named by its path joined by -: lab-base-disko is part disko of part base of lab):
{ lb = [ "lab-base-disko.default" "lab-base-emergency.default" "lab-base-facter.default" "lab-base-sshd.server" "lab-base.default" "lab-borg.client" "lab-monitoring.exporter" "lab-wg.peer" "lab.node" ]; nas = [ "lab-base-disko.default" "lab-base-emergency.default" "lab-base-facter.default" "lab-base-sshd.server" "lab-base.default" "lab-borg-sshd.server" "lab-borg.server" "lab-dns.default" "lab-monitoring.server" "lab-wg.controller" "lab.server" ]; web = [ "lab-base-disko.default" "lab-base-emergency.default" "lab-base-facter.default" "lab-base-sshd.server" "lab-base.default" "lab-borg.client" "lab-monitoring.exporter" "lab-wg.peer" "lab.node" ];}What the members export:
[ "sshd:lab-base-sshd:server:lb" "monitoring:lab-monitoring:exporter:lb" "wireguard:lab-wg:peer:lb" "sshd:lab-base-sshd:server:nas" "sshd:lab-borg-sshd:server:nas" "borg:lab-borg:server:nas" "wireguard:lab-wg:controller:nas" "sshd:lab-base-sshd:server:web" "monitoring:lab-monitoring:exporter:web" "wireguard:lab-wg:peer:web"]The contract
Section titled “The contract”homelab allows exactly one server. Two:
# a homelab with two servers: exactly one is allowed{ yen, ... }:{ yen.assembly.lab2 = { kit = yen.kit.homelab; roles = { server = { at = [ yen.host.nas yen.host.lb ]; settings = import ../shared.nix yen; }; node = { at = yen.host.web; settings = import ../shared.nix yen; }; }; };}[ "merge conflict at services.prometheus.scrapeConfigs" "wireguard: lb has no wg-lab2-wg.pub public key" "wireguard: nas has no wg-lab2-wg.pub public key" "wireguard: web has no wg-lab2-wg.pub public key" "kits: assembly lab2 role server: 2 machines, at most 1" "kits: assembly lab2-borg role server: 2 machines, at most 1" "kits: assembly lab2-monitoring role server: 2 machines, at most 1" "kits: assembly lab2-wg role controller: 2 machines, at most 1"]And none:
# a homelab with nodes and no machine for the server role{ yen, ... }:{ yen.assembly.lab0 = { kit = yen.kit.homelab; roles = { server = { at = _: false; settings = import ../shared.nix yen; }; node = { at = yen.host.web; settings = import ../shared.nix yen; }; }; };}[ "wireguard: web has no wg-lab0-wg.pub public key" "kits: assembly lab0 role server: 0 machines, at least 1" "kits: assembly lab0-borg role server: 0 machines, at least 1" "kits: assembly lab0-borg-sshd role server: 0 machines, at least 1" "kits: assembly lab0-monitoring role server: 0 machines, at least 1" "kits: assembly lab0-wg role controller: 0 machines, at least 1"]The bundles
Section titled “The bundles”| Bundle | Roles | Adds |
|---|---|---|
base |
default with device, layout (one of yen.kit.disko.layouts), authorizedKeys, hardware |
disko, facter, emergency-access, and sshd as server, on the same machines. No users: a user needs a name per machine |
homelab |
server (exactly 1), node; the settings of base and prefix |
base on both roles; wireguard (server controller, nodes peers); borg (server backs up); monitoring (server scrapes); dns on the server |
The templates kits-homelab and kits-base are working examples.