Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ci-runner, swarm-grafana, swarm-victorialogs: forward each container's journal to the swarm collector

Each container now runs the journal forwarder the queue's and the builder's
containers run, pushing to a receiver of its own on the swarm collector. Each
authenticates as a new machine OIDC client of its own
(swarm.<service>.otel.clientId), registered by a glue file wherever authelia
runs, minted by authelia and published to the store by the secret publisher.
Grafana's browser login client is unchanged.

The fetch unit inside each container reads the client secret with the store
identity the host already holds for that service (the runner's, Grafana's,
and the log store responder's), now also handed into the container as an
nspawn system credential. Each of those three policies gains one read stanza
on its forwarder's client path. The host units that use those identities are
unchanged.

The three containers drop --link-journal=host, and the host collector stops
reading them.
This commit is contained in:
atlas 2026-10-16 10:14:57 +02:00 • committed by mara
commit c6ca9ab845
20 changed files with 810 additions and 107 deletions

View file

@ -229,6 +229,33 @@ let
autheliaUrl = toString hyperhiveCfg.swarm.authelia.url;
# The container's journal forwarder pushes to its own receiver on the swarm
# collector, the route `serviceProducers.swarm-grafana` in
# ./swarm-otel-service.nix declares, as a client of its own rather than the
# browser login's. The port suffix is elided on 443, as in ./swarm-bao.nix.
forwarderClientId = cfg.otel.clientId;
gatewayPortSuffix = lib.optionalString (
gatewayCfg.httpsPort != 443
) ":${toString gatewayCfg.httpsPort}";
forwarderEndpoint = "https://${hyperhiveCfg.swarm.otel.domain}${gatewayPortSuffix}/${cfg.machine}";
forwarderUnit = "swarm-journal-forwarder-oidc";
# In the host netns: distinct from every other collector's there, which the
# assertion below checks.
forwarderTelemetryPort = 8896;
# Grafana's store identity, handed into the container as system credentials
# (credential id → host source) for the forwarder's fetch unit. The host units
# below keep reading the same files directly.
storeFiles = lib.filterAttrs (_: source: source != null) {
grafana-bao-cert = baoDeploy.grafanaOidcClientCertFile;
grafana-bao-key = baoDeploy.grafanaOidcClientKeyFile;
grafana-bao-server-ca = baoDeploy.serverCaFile;
};
# nixos-container expands `extraFlags` unquoted, so a source path must be
# one word.
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
forwarderCredential = id: "/run/credentials/${forwarderUnit}.service/${id}";
# The three secrets below land on the host's /run (tmpfs), which the
# container bind-mounts read-only at the same path: none reaches persistent
# disk, and a reboot fetches them again. Each file is absent until its
@ -493,6 +520,39 @@ in
from that CA out of band and named here — see docs/swarm/secrets.md.
'';
}
{
assertion = lib.all oneWord (lib.attrValues storeFiles);
message = ''
Grafana's store identity paths must not contain whitespace: nspawn's
--load-credential receives them through nixos-container's unquoted
extra flags.
'';
}
{
assertion =
(hyperhiveCfg.swarm.otel.serviceProducers.${cfg.machine}.clientId or null) == forwarderClientId;
message = ''
services.hyperhive.swarm.otel.serviceProducers has no entry
"${cfg.machine}" with clientId "${forwarderClientId}", so the swarm
collector has no receiver for Grafana's journal forwarder. Declare
it in swarm-otel-service.nix.
'';
}
{
assertion =
!(lib.elem forwarderTelemetryPort [
hyperhiveCfg.otel.telemetryPort
hyperhiveCfg.swarm.otel.telemetryPort
hyperhiveCfg.swarm.bao.otel.telemetryPort
hyperhiveCfg.swarm.nats.otel.telemetryPort
]);
message = ''
Grafana's journal forwarder's telemetry port (${toString forwarderTelemetryPort})
collides with another collector's in the host netns: the hive
collector's, the swarm collector's, the store forwarder's or the
queue forwarder's.
'';
}
];
# Websockets: Grafana Live streams panel updates over one, and without
@ -555,8 +615,17 @@ in
};
# Order the container after the host CA service so the bind source below
# exists before nspawn sets the mount up.
systemd.services."container@${cfg.machine}" = caTrust.containerOrdering;
# exists before nspawn sets the mount up. nspawn also fails to start it
# when a credential source is missing, so where this host mints the leaf
# the container pulls the minting unit in and waits for it, as
# ./swarm-builder.nix's does.
systemd.services."container@${cfg.machine}" = lib.mkMerge [
caTrust.containerOrdering
{
after = [ "swarm-bao-pki.service" ];
wants = [ "swarm-bao-pki.service" ];
}
];
# The socket directory, created host-side before the container starts.
# nixos-container refuses to start when a bind source is missing, so this
@ -1036,9 +1105,9 @@ in
containers.${cfg.machine} = {
autoStart = true;
ephemeral = false;
# Journal files on the host, not inside the container: nixpkgs hardcodes
# --link-journal=try-guest, and EXTRA_NSPAWN_FLAGS expands after it.
extraFlags = [ "--link-journal=host" ];
# No `--link-journal=host`: the journal stays in the guest, where the
# container's own forwarder reads it (../container-modules/swarm-journal-forwarder.nix).
extraFlags = lib.mapAttrsToList (id: source: "--load-credential=${id}:${source}") storeFiles;
inherit privateNetwork;
# The socket directory, shared with the host so nginx can reach in.
@ -1068,9 +1137,16 @@ in
{
imports = [
../container-modules/swarm-container.nix
../container-modules/swarm-journal-forwarder.nix
(import ../container-modules/swarm-container-resolver.nix {
inherit (networkCfg) bridgeIp;
dnsConsumers = [ "grafana.service" ];
# The forwarder and its secret fetch dial the swarm collector,
# authelia and the store by name too.
dnsConsumers = [
"grafana.service"
"opentelemetry-collector.service"
"${forwarderUnit}.service"
];
})
# Grafana is Go, and `SSL_CERT_FILE` *replaces* the default store
# rather than adding to it — so it needs the system CAs and the
@ -1080,12 +1156,38 @@ in
(caTrust.trustBundle {
inherit pkgs;
name = cfg.machine;
consumers = [ "grafana" ];
consumers = [
"grafana"
# Go too, for the swarm collector's and authelia's names.
"opentelemetry-collector"
];
})
];
services.hyperhive.swarmContainer = { inherit privateNetwork; };
# Authenticated as the forwarder's own OIDC client, with Grafana's
# store identity: one identity for the whole container. It is a
# system credential, so the fetch unit loads it.
services.hyperhive.swarmJournalForwarder = {
enable = true;
serviceName = "grafana";
endpoint = forwarderEndpoint;
clientId = forwarderClientId;
tokenUrl = "${autheliaUrl}/api/oidc/token";
telemetryPort = forwarderTelemetryPort;
store = {
package = baoDeploy.package;
addr = "https://${baoCfg.domain}:${toString baoCfg.port}";
secretPath = "secret/swarm/services/${forwarderClientId}/oidc/client";
clientCertFile = forwarderCredential "grafana-bao-cert";
clientKeyFile = forwarderCredential "grafana-bao-key";
caFile =
if storeFiles ? grafana-bao-server-ca then forwarderCredential "grafana-bao-server-ca" else null;
};
};
systemd.services.${forwarderUnit}.serviceConfig.LoadCredential = lib.attrNames storeFiles;
# Grafana's `secret_key` has **no default in nixpkgs** and an
# assertion refuses the build without one — which is how the first
# version of this module broke a deploy. It signs the datasource