Files
dotfiles/lib.nix
alexion e7d7eb14e1 feat: introduce guests as nested-container definitions
Add a third auto-loaded kind beside the host and the module: the guest, a
reusable definition under guests/ that a host enables like a module and that
realizes its interior as a systemd-nspawn nested container.

Split the shared base config so a guest can stand on it.
base.nix now holds the substrate both bases share — the primary user,
home-manager, and the unstable/stable overlays.
system.nix keeps the host-only machinery, and a new guest.nix is the slim
guest-base: it imports the full modules tree, pins the interior release, and
auto-enables the toolkit bundle and SSH so any guest is workable on sight.

Give modules.ssh a guest flavor.
A host restores its host keys from secrets as before, while a guest sets
hostKeys.restore = false, names no sops files, and self-generates a host key,
so it holds no age key of its own.

The lib grows a guest helper that declares the guests.<path> namespace with an
enable and a backend field.
Only the container backend is built; microvm is a reserved value that trips a
clear build-time assertion rather than silently building nothing.

A sample guest exercises the whole path, and neogaia enables it, so the guest
interior builds through the existing nix flake check seam.
2026-07-25 13:19:07 -04:00

143 lines
3.8 KiB
Nix

{
lib,
inputs,
self,
}:
let
inherit (lib)
attrNames
filterAttrs
genAttrs
flatten
hasSuffix
mapAttrsToList
;
# Recursively collect every `.nix` file under `dir` as a flat list, for a
# module's `imports`.
collectNixFiles =
dir:
flatten (
mapAttrsToList (
name: type:
let
path = dir + "/${name}";
in
if type == "directory" then
collectNixFiles path
else if type == "regular" && hasSuffix ".nix" name then
[ path ]
else
[ ]
) (builtins.readDir dir)
);
# The special arguments every configuration is evaluated with, host and guest
# interior alike.
specialArgs = {
inherit inputs;
my = self.lib;
};
# Build one host: every module and every guest is imported unconditionally
# (inert until its `enable` flag is set), alongside chaotic, the host base,
# and the host's own directory.
mkHost =
{
hostName,
system ? "x86_64-linux",
}:
inputs.nixpkgs.lib.nixosSystem {
inherit system specialArgs;
modules =
(collectNixFiles (self + "/modules"))
++ (collectNixFiles (self + "/guests"))
++ [
inputs.chaotic.nixosModules.default
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
(self + "/system.nix")
(self + "/hosts/${hostName}")
{ networking.hostName = hostName; }
];
};
# Build a guest: a module-shaped definition whose body realizes its interior
# as a nested container standing on the guest-base, keyed by its namespace path.
# `name` is the dotted namespace under `guests.` and `interior` is an extra
# module merged into the container alongside the guest-base.
guest =
{
name,
interior ? { },
}:
{ config, lib, ... }:
let
optionPath = [ "guests" ] ++ lib.splitString "." name;
cfg = lib.getAttrFromPath optionPath config;
machineName = lib.replaceStrings [ "." ] [ "-" ] name;
in
{
options = lib.setAttrByPath optionPath {
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
backend = lib.mkOption {
type = lib.types.enum [
"container"
"microvm"
];
default = "container";
description = ''
How the guest is realized. `container` runs the guest as a
systemd-nspawn nested container. `microvm` is reserved for a future
hard-isolation backend and is not built yet.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.backend == "container";
message = ''
guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work.
'';
}
];
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
autoStart = lib.mkDefault true;
# The guest gets its own network namespace, so its services — its own
# sshd included — never contend with the host's.
privateNetwork = lib.mkDefault true;
inherit specialArgs;
config = {
imports = [
(self + "/guest.nix")
interior
];
};
};
};
};
# Discover every host (a subdirectory of `hostsDir`) and build each one.
mkHosts =
hostsDir:
let
hostNames = attrNames (filterAttrs (_name: type: type == "directory") (builtins.readDir hostsDir));
in
genAttrs hostNames (hostName: mkHost { inherit hostName; });
in
{
inherit
collectNixFiles
mkHost
mkHosts
guest
;
}