The single module-eval derivation forced ~62 full nixosSystem fixtures live at once to compute its cases list: 10.6GB peak RSS / 5m25s to evaluate, by far the dominant cost in nix flake check. Splits it into 21 independent checks.module-eval-* derivations (1-7 fixtures each) sharing builders/helpers via module-eval/lib.nix, so no single derivation needs more than a handful of fixtures live at once. A few cases spanning two clusters carry a small duplicated fixture rather than threading shared state through lib.nix.
202 lines
8.3 KiB
Nix
202 lines
8.3 KiB
Nix
# `checks.module-eval-*` — the flake checks that cover **nix**.
|
|
#
|
|
# Split from one monolithic `module-eval` derivation into small
|
|
# independent checks, one per subsystem cluster, so that evaluating any
|
|
# single derivation only has to hold a handful of full `nixosSystem`
|
|
# fixtures live at once instead of all of them together — the original
|
|
# combined ~62 fixtures into one derivation and measured 10.6GB peak RSS /
|
|
# 5m25s to evaluate. Every other check in ./checks.nix is a Rust
|
|
# derivation, so a `.nix`-only diff moves no hash and `nix flake check`
|
|
# goes green **without evaluating what changed**; these derivations' hash
|
|
# is a function of their cases' results, so a change that flips a
|
|
# property rebuilds the owning check and fails naming it.
|
|
#
|
|
# Anything expressible as a module `assertion` **should be one instead** —
|
|
# an assertion fires at deploy time for a real operator, not only in CI.
|
|
# What cannot is the **absence class**: "a hive that hasn't opted in
|
|
# renders what it did before", "this unit does not exist unless X" — claims
|
|
# about the rendered config rather than about a config being invalid.
|
|
#
|
|
# ⚠️ **Name a case for the PROPERTY it defends, never the ticket that
|
|
# prompted it**; a case named after a ticket has the ticket's lifetime.
|
|
#
|
|
# ⚠️ **This evaluates, it does not execute** — a command line, a request or
|
|
# a certificate needs something that *runs* it. A case needing a rendered
|
|
# file must stub what it drags in (`deploy.swarm-ui.package = pkgs.emptyDirectory`).
|
|
#
|
|
# `hive`/`agent`/`agentWith` builders, the shared cross-group helpers, and
|
|
# the `runGroup` derivation builder live in ./lib.nix; each file below
|
|
# defines only the fixtures and cases its own cluster needs.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
}:
|
|
let
|
|
# Stub host, same shape ./docs/default.nix already uses: enough for a
|
|
# `nixosSystem` to evaluate, nothing that pulls a real disk or
|
|
# bootloader in.
|
|
hive =
|
|
extra:
|
|
(nixosSystem {
|
|
system = pkgs.stdenv.hostPlatform.system;
|
|
modules = [
|
|
self.nixosModules.default
|
|
{
|
|
fileSystems."/" = {
|
|
device = "/dev/null";
|
|
fsType = "tmpfs";
|
|
};
|
|
boot.loader.grub.enable = false;
|
|
system.stateVersion = "25.11";
|
|
# `recursiveUpdate`, not `//`: a plain `//` only merges the
|
|
# *top-level* keys of `extra` in, so an `extra` that touches a
|
|
# nested attr under an existing top-level key (e.g. `swarm.*`)
|
|
# silently drops every sibling under that key instead of merging
|
|
# into it — the same class of bug as the `services.hyperhive`
|
|
# double-nesting mistake this stub already had to dodge once,
|
|
# just one level down. `recursiveUpdate` merges nested attrsets
|
|
# all the way down instead.
|
|
services.hyperhive = lib.recursiveUpdate {
|
|
enable = true;
|
|
hiveName = "h1";
|
|
swarm.domain = "t.local";
|
|
swarm.hives.h1.domain = "h1.t.local";
|
|
} extra;
|
|
}
|
|
];
|
|
}).config;
|
|
|
|
# The other half of the tree. `nix/agent-modules/` is evaluated by nothing
|
|
# else in this suite — every fixture above is a host — so a rendered
|
|
# container config was only ever read by a real deploy. Same entry point
|
|
# the meta flake hands a container, so what this evaluates is what an
|
|
# agent gets.
|
|
#
|
|
# Takes a whole module rather than a settings attrset, so a fixture can
|
|
# reach either spelling of the agent tier. `user.name` is the one option
|
|
# with no usable default, set here at its real path and at `mkDefault` so a
|
|
# fixture naming its own agent still wins.
|
|
agentWith =
|
|
module:
|
|
(nixosSystem {
|
|
system = pkgs.stdenv.hostPlatform.system;
|
|
modules = [
|
|
self.nixosModules.agent-base
|
|
{
|
|
fileSystems."/" = {
|
|
device = "/dev/null";
|
|
fsType = "tmpfs";
|
|
};
|
|
boot.loader.grub.enable = false;
|
|
system.stateVersion = "25.11";
|
|
services.hyperhive.agent.user.name = lib.mkDefault "a1";
|
|
}
|
|
module
|
|
];
|
|
}).config;
|
|
|
|
# Note `hyperhive`, not `services.hyperhive`: the agent tier's options moved
|
|
# under `services.hyperhive.agent`, and ../agent-modules/renamed-options.nix
|
|
# keeps the top-level spelling reaching them. ⚠️ That shim covers the
|
|
# options that existed when the tier moved and nothing since, so a fixture
|
|
# for an option added afterwards has to go through [`agentWith`] and name
|
|
# the real path.
|
|
agent = extra: agentWith { hyperhive = extra; };
|
|
|
|
baoNames = machine: machine.services.hyperhive.gateway.localNames;
|
|
|
|
# What nginx is handed for its `stream {}` block. Deliberately not
|
|
# `virtualHosts`: a vhost is the terminating shape ./host-modules/
|
|
# swarm-bao.nix's header refuses, and this is the passthrough that is not.
|
|
baoStream = machine: machine.services.nginx.streamConfig;
|
|
|
|
# The bridge-interface firewall — the list `network.exposeHostPorts` merges
|
|
# into, and the only place a port is opened for agents. The host's own
|
|
# `allowedTCPPorts` is a different list and a different exposure.
|
|
bridgePorts =
|
|
machine:
|
|
machine.networking.firewall.interfaces.${machine.services.hyperhive.network.bridgeName}.allowedTCPPorts;
|
|
|
|
# Same list, on a host that may not declare the interface at all: the
|
|
# `[ 80 443 ]` block is what creates the attr, and it is off where the hive
|
|
# is. Reading it through `bridgePorts` would throw rather than report an
|
|
# empty exposure, which is precisely the state the cases below assert.
|
|
bridgePortsOrNone =
|
|
machine:
|
|
(machine.networking.firewall.interfaces.${machine.services.hyperhive.network.bridgeName} or {
|
|
allowedTCPPorts = [ ];
|
|
}
|
|
).allowedTCPPorts;
|
|
|
|
# The config file openbao parses, not the nix that produces it: a setting it
|
|
# requires is absent here without anything in the module system minding, so
|
|
# the daemon's own startup is otherwise the first reader.
|
|
baoSettings = machine: machine.containers.swarm-bao.config.services.openbao.settings;
|
|
|
|
otelSettings =
|
|
machine: machine.containers.swarm-otel.config.services.opentelemetry-collector.settings;
|
|
|
|
agentHarness = machine: machine.systemd.services.hive-agent;
|
|
|
|
# The enables that switch owns. ⚠️ `otel` is the per-hive collector's own
|
|
# option and is NOT under `deploy` — spelled at the wrong path it would be
|
|
# undeclared rather than false, and a roster that quietly loses a member is
|
|
# what the count guard in the cases below exists to catch. Its membership
|
|
# here is deliberate and was the fix for a gap, not an oversight: the hive
|
|
# tier lands wherever the swarm services do.
|
|
swarmServiceEnables =
|
|
machine:
|
|
let
|
|
h = machine.services.hyperhive;
|
|
in
|
|
{
|
|
matrix = h.deploy.matrix.enable;
|
|
otel = h.otel.enable;
|
|
authelia = h.deploy.authelia.enable;
|
|
nats = h.deploy.nats.enable;
|
|
swarm-otel = h.deploy.swarm-otel.enable;
|
|
victoriametrics = h.deploy.victoriametrics.enable;
|
|
grafana = h.deploy.grafana.enable;
|
|
victorialogs = h.deploy.victorialogs.enable;
|
|
bao = h.deploy.bao.enable;
|
|
};
|
|
runGroup =
|
|
name: cases:
|
|
let
|
|
bad = builtins.filter (c: !c.ok) cases;
|
|
# Escaped, because a name is prose and prose contains apostrophes. Hand-quoting
|
|
# broke the builder mid-report on the first such name that failed — and only
|
|
# ever on failure, so every green run agreed the reporter was fine.
|
|
report = lib.concatMapStringsSep "\n" (
|
|
c: " echo ${lib.escapeShellArg "FAILED: ${c.name}"} >&2"
|
|
) bad;
|
|
in
|
|
# The results are embedded in the builder text on purpose: that is what
|
|
# makes this derivation's hash depend on them, so a nix-only change that
|
|
# flips a case cannot be answered from cache.
|
|
pkgs.runCommand "hyperhive-module-eval-${name}" { } ''
|
|
${report}
|
|
${
|
|
if bad == [ ] then
|
|
"echo '${toString (builtins.length cases)} module properties hold in ${name}' && touch $out"
|
|
else
|
|
"echo 'module-eval-${name}: ${toString (builtins.length bad)} of ${toString (builtins.length cases)} properties broke' >&2 && exit 1"
|
|
}
|
|
'';
|
|
in
|
|
{
|
|
inherit hive;
|
|
inherit agent;
|
|
inherit agentWith;
|
|
inherit baoNames;
|
|
inherit baoStream;
|
|
inherit bridgePorts;
|
|
inherit bridgePortsOrNone;
|
|
inherit baoSettings;
|
|
inherit otelSettings;
|
|
inherit agentHarness;
|
|
inherit swarmServiceEnables;
|
|
inherit runGroup;
|
|
}
|