Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/ci-metrics.nix
atlas 6630a51022 trust the swarm services ca in swarm-ci-metrics
The poller's forge, authelia and collector calls go to leaves issued by
the swarm's own CA, and its reqwest/rustls roots come from SSL_CERT_FILE,
which the unit never set. Every pass since it was deployed fails
"invalid peer certificate: UnknownIssuer" before pushing, so the CI runs
dashboard has no hyperhive.ci.run.* samples (#5438).

- nix/host-modules/swarm-ci-metrics.nix: import lib/hive-ca-trust.nix and
  register a host trustBundle for the unit, as swarm-controller does; it
  adds swarm-ci-metrics-ca-bundle.service and sets SSL_CERT_FILE.
- nix/module-eval/ci-metrics.nix: a case asserting the unit's
  SSL_CERT_FILE is its bundle path when self-signed, with the bundle
  unit's ordering, and absent where the poller is off.
- docs/swarm/ci-metrics.md: how the poller trusts the swarm's CA.
2026-10-17 23:28:03 +02:00

114 lines
5 KiB
Nix

# `checks.module-eval-ci-metrics` — see ./lib.nix for the shared rationale
# (why this suite exists, naming convention, "evaluates not executes"). The CI
# run poller's unit and timer, its default, its CA trust, its collector route
# and its IdP registration; its store grants are in ./bao-grants.nix beside
# every other principal's, and its dashboard in ./grafana.nix.
#
# Every fixture is a shared one, so this cluster evaluates no host of its own.
{
pkgs,
lib,
self,
nixosSystem,
shared,
}:
let
inherit
(import ./lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
runGroup
;
bare = shared.bareWithController;
# Every swarm service on one host: the store mints the poller's leaf, so the
# poller defaults on, beside authelia, the publisher and the collector.
allLocal = shared.allLocal;
svc = allLocal.systemd.services.swarm-ci-metrics;
env = svc.environment;
clientId = allLocal.services.hyperhive.swarm.ci-metrics.clientId;
otel = allLocal.services.hyperhive.swarm.otel;
cases = [
{
# The control is the same unit and timer on the all-local host: a
# misspelled attribute path would make the absence arm pass alone.
name = "the CI run poller's unit and timer exist only where its host enables it";
ok =
!(bare.systemd.services ? swarm-ci-metrics)
&& !(bare.systemd.timers ? swarm-ci-metrics)
&& allLocal.services.hyperhive.deploy.ci-metrics.enable
&& svc.serviceConfig.Type == "oneshot"
&& !(svc.serviceConfig.RemainAfterExit or false)
&& allLocal.systemd.timers.swarm-ci-metrics.timerConfig.OnUnitInactiveSec == "5min"
&& lib.elem "timers.target" allLocal.systemd.timers.swarm-ci-metrics.wantedBy;
}
{
# The leaf is handed in as a credential, never an argument; the forge
# token and the client secret are read from the store per pass.
name = "the poller loads only its store leaf, keeps its cursor in its state directory and carries no secret in its unit";
ok =
lib.elem "bao-client-key.pem:/var/lib/swarm-bao-pki/ci-metrics-key.pem" svc.serviceConfig.LoadCredential
&& lib.elem "bao-client.pem:/var/lib/swarm-bao-pki/ci-metrics.pem" svc.serviceConfig.LoadCredential
&& env.BAO_CLIENT_KEY == "/run/credentials/swarm-ci-metrics.service/bao-client-key.pem"
&& svc.serviceConfig.DynamicUser
&& svc.serviceConfig.StateDirectory == "swarm-ci-metrics"
&& !(lib.any (n: lib.hasSuffix "TOKEN" n || lib.hasSuffix "SECRET" n) (lib.attrNames env));
}
{
# The endpoint the poller pushes to is the route the collector admits
# its client on, and that route feeds the swarm tier's metrics pipeline.
name = "the poller pushes to its own collector route, which admits its client into the metrics pipeline";
ok =
let
h = shared.baoWithCollector;
s = h.containers.swarm-otel.config.services.opentelemetry-collector.settings;
in
env.SWARM_CI_METRICS_OTLP_ENDPOINT == "https://${otel.domain}/swarm-ci-metrics"
&& env.SWARM_CI_METRICS_OIDC_CLIENT_ID == clientId
&& otel.serviceProducers.swarm-ci-metrics.clientId == clientId
&& s.receivers."otlp/swarm-ci-metrics".protocols.http.auth.authenticator == "oidc/swarm-ci-metrics"
&& s.extensions."oidc/swarm-ci-metrics".audience == clientId
&& lib.elem "otlp/swarm-ci-metrics" s.service.pipelines."metrics/${otel.producerName}".receivers
&& h.services.nginx.virtualHosts.${otel.domain}.locations ? "/swarm-ci-metrics/";
}
{
# The forge, authelia and the collector chain to the swarm's own CA;
# without the bundle every pass fails `UnknownIssuer`. hive-c0re's
# bundle on the bare host is the control that the absence arm looks up
# a unit name the module would write.
name = "the poller trusts the swarm's CA through its own bundle, which exists only where the poller does";
ok =
allLocal.services.hyperhive.gateway.useSelfSigned
&& (env.SSL_CERT_FILE or null) == "/run/swarm-ci-metrics-ca/trust-bundle.pem"
&& lib.elem "swarm-ci-metrics-ca-bundle.service" svc.requires
&& lib.elem "hive-tls-ca.service" (
allLocal.systemd.services.swarm-ci-metrics-ca-bundle.requires or [ ]
)
&& bare.systemd.services ? hive-c0re-ca-bundle
&& !(bare.systemd.services ? swarm-ci-metrics-ca-bundle);
}
{
name = "the poller's client is registered as a machine, and the publisher carries its secret";
ok =
let
c = lib.findFirst (
c: c.id == clientId
) null allLocal.services.hyperhive.swarm.authelia.oidc.clients;
in
c != null
&& c.kind == "machine"
&& c.audience == [ clientId ]
&& lib.hasInfix "secret/swarm/services/${clientId}/oidc/client" (
allLocal.containers.swarm-authelia.config.systemd.services.swarm-secret-publish.script
);
}
];
in
runGroup "ci-metrics" cases