hyperhive/nix/module-eval/swarm-otel-identity.nix
müde dc418a5223 nix: split module-eval into per-subsystem checks
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.
2026-09-20 04:25:54 +02:00

167 lines
7.5 KiB
Nix

# `checks.module-eval-swarm-otel-identity` — see ./lib.nix for the shared
# rationale (why this suite exists, naming convention, "evaluates
# not executes").
{
pkgs,
lib,
self,
nixosSystem,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
hive
runGroup
otelSettings
;
# A collector holding no store identity at all. Unlike Grafana's mirror
# image, this is not a refused shape: the collector still receives every
# hive's telemetry with nothing to push authenticated with, which is the
# already-supported degrade `haveCollectorSecret` names above the module's
# `let`. What this fixture is for is checking the reading unit itself does
# not render, rather than rendering with an env var nothing filled in.
otelNoIdentity = hive {
deploy.swarm-otel.enable = true;
swarm.authelia.url = "https://auth.example.invalid";
deploy.forgejo.sso.clientSecretFile = "/var/lib/forgejo-oidc/by-hand.secret";
};
# authelia somewhere else, the credential delivered by hand. Whether this
# collector authenticates must follow the credential, never another
# service's placement.
#
# The `swarm.otel.clientSecretFile` below is the PRE-RENAME path. It predates
# the split and is deliberately left spelled that way: it makes this fixture
# the old-path case for that option too, so dropping its rename entry fails
# the eval here rather than only in a real operator's config.
otelRemoteAuthelia = hive {
deploy.swarm-otel.enable = true;
deploy.authelia.enable = false;
# Where that elsewhere IS. Running no IdP does not mean knowing no IdP:
# the authenticator this fixture exists to render puts this address in its
# `token_url`, so a hive with a secret and no URL has a credential it can
# present nowhere.
swarm.authelia.url = "https://auth.example.invalid";
swarm.otel.clientSecretFile = "/var/lib/swarm-otel-oidc/by-hand.secret";
};
# A collector whose ONLY scrape work is published: loopback targets forced
# empty, one published job declared. Unreachable in a real deploy today —
# the module seeds `scrapeTargets.collector` under its own `enable`, so the
# loopback set is never empty on its own — which is exactly why the arm
# below needs a fixture that takes that seeding away. `mkForce` is what
# does it, and it leaves the collector itself enabled: the state under test
# is a running collector with no self-scrape, not an absent one.
otelOnlyPublished = hive {
deploy.swarm-otel.enable = true;
swarm.otel.scrapeTargets = lib.mkForce { };
swarm.otel.publishedScrapeTargets.remote = "https://remote.t.local/metrics";
};
# Two hives in the roster, which no other fixture here has: every one of
# them declares `swarm.hives.h1` alone, so a per-hive arm written against
# one of those passes on a hardcoded literal.
otelTwoHives = hive {
deploy.swarm-otel.enable = true;
deploy.authelia.enable = true;
swarm.hives.h2.domain = "h2.t.local";
};
cases = [
{
# The collector's non-assertion, the deliberate mirror of Grafana's
# assertion two cases up: a host with no store identity is a supported,
# merely degraded shape here, so the reading unit simply does not exist
# rather than refusing the build. `haveCollectorSecret` is what the
# degrade already reads, unchanged by this slice.
name = "a collector with no store identity renders no reading unit, and is not refused";
ok =
!(otelNoIdentity.systemd.services ? swarm-bao-otel-oidc)
&& otelNoIdentity.services.hyperhive.deploy.swarm-otel.clientSecretFile == null
&& !(lib.any (a: !a.assertion) otelNoIdentity.assertions);
}
{
# The collector's half of the same split, and a different arm from the
# authenticator case below: this one reads the PATH the unit loads, so a
# reader left on a source that is non-null but wrong still fails. The
# fixture spells the option its pre-rename way, so it covers the rename
# entry at the same time.
name = "a config written against the pre-rename otel secret path still loads it as a credential";
ok =
lib.any (c: lib.hasInfix "/var/lib/swarm-otel-oidc/by-hand.secret" c)
otelRemoteAuthelia.containers.swarm-otel.config.systemd.services.opentelemetry-collector.serviceConfig.LoadCredential;
}
{
# The collector authenticates because it HOLDS a credential, not because
# authelia happens to share its host. Gating on the other service's
# placement renders a collector that pushes unauthenticated wherever
# authelia lives elsewhere — one of the supported shapes.
name = "a collector with a hand-delivered secret authenticates without authelia beside it";
ok =
let
s = otelSettings otelRemoteAuthelia;
in
(s.exporters."otlphttp/victoriametrics" ? auth)
&& builtins.elem "oauth2client/victoriametrics" s.service.extensions;
}
{
# Defining a receiver and attaching it are two separate lists, and the
# two gates were spelled differently: the receiver appeared for either
# scrape option, the pipeline only for the loopback one. A published-
# only collector therefore rendered scrape configs that reached no
# pipeline — requested, parsed, delivered nowhere, and valid enough to
# deploy. The receiver clause is what stops the arm passing for the
# wrong reason, by an empty `prometheus` never rendering at all.
name = "a published-only collector attaches its prometheus receiver to the swarm pipeline";
ok =
let
s = otelSettings otelOnlyPublished;
in
otelOnlyPublished.services.hyperhive.swarm.otel.scrapeTargets == { }
&& otelOnlyPublished.services.hyperhive.swarm.otel.publishedScrapeTargets != { }
&& (s.receivers ? prometheus)
&& builtins.elem "prometheus" s.service.pipelines."metrics/swarm".receivers;
}
{
# Read against the roster the fixture declares rather than against
# names spelled here: an arm naming `h1` passes on a single-hive
# config however the mapping is written. The length clause is what
# makes the `all` mean anything — over an empty roster it holds
# vacuously.
name = "the swarm collector routes every hive's logs, not just one";
ok =
let
p = (otelSettings otelTwoHives).service.pipelines;
hives = lib.attrNames otelTwoHives.services.hyperhive.swarm.hives;
in
lib.length hives == 2
&& lib.all (h: (p ? "logs/${h}") && p."logs/${h}".receivers == [ "otlp/${h}" ]) hives;
}
{
# The same split as the metrics case above — defining an exporter and
# naming it are two lists — plus the half one shared list cannot have:
# the metrics store's exporter renders perfectly well inside a logs
# pipeline and posts journal records at an ingest route that is not
# for them.
name = "every logs pipeline sends to the log store and to no metrics one";
ok =
let
s = otelSettings otelTwoHives;
logPipes = lib.filterAttrs (n: _: lib.hasPrefix "logs/" n) s.service.pipelines;
used = lib.unique (lib.concatMap (p: p.exporters) (lib.attrValues logPipes));
in
logPipes != { }
&& builtins.elem "otlphttp/victorialogs" used
&& !(builtins.elem "otlphttp/victoriametrics" used)
&& lib.all (e: s.exporters ? ${e}) used;
}
];
in
runGroup "swarm-otel-identity" cases