tuwunel 1.9.4 exports no metrics. The host collector gets an httpcheck receiver on the gateway's /_matrix/client/versions (new services.hyperhive.otel.httpChecks, declared by hive-matrix), and a timer in the hive-matrix container pushes the database directory's size through the forwarder's OTLP port, as the attic cache-size unit does. The size step dereferences the state directory's symlink and pushes nothing for a result under 4096 bytes.
242 lines
11 KiB
Nix
242 lines
11 KiB
Nix
# `checks.module-eval-matrix-core` — see ./lib.nix for the shared
|
|
# rationale (why this suite exists, naming convention, "evaluates
|
|
# not executes").
|
|
{
|
|
pkgs,
|
|
lib,
|
|
self,
|
|
nixosSystem,
|
|
shared,
|
|
}:
|
|
let
|
|
inherit
|
|
(import ./lib.nix {
|
|
inherit
|
|
pkgs
|
|
lib
|
|
self
|
|
nixosSystem
|
|
;
|
|
})
|
|
hive
|
|
hiveWithController
|
|
runGroup
|
|
;
|
|
|
|
# A deliberately odd `maxRequestSize`, so the number the assertion looks for
|
|
# cannot have come from a default or from another module's literal.
|
|
matrixBodyCap = hive {
|
|
deploy.singleHostSwarm = true;
|
|
deploy.matrix.maxRequestSize = 99000000;
|
|
};
|
|
|
|
# The same hive with the identity taken away, which separates "a homeserver
|
|
# is deployed" from "this host can authenticate to the store".
|
|
inherit (shared) matrixNoBaoIdentity;
|
|
|
|
# A hive host that does not run the homeserver: its hive-c0re reaches the
|
|
# homeserver across the network.
|
|
matrixElsewhere = shared.bareWithController;
|
|
|
|
# A homeserver whose server name was pinned to the hive domain, on the
|
|
# controller's host, with no homeserver of its own: the discovery answer
|
|
# sits on the hive domain and needs neither the homeserver nor the apex.
|
|
matrixPinned = hiveWithController {
|
|
swarm.matrix.serverName = "h1.t.local";
|
|
deploy.swarm-controller.enable = true;
|
|
deploy.swarm-ui.package = pkgs.emptyDirectory;
|
|
};
|
|
|
|
cases = [
|
|
{
|
|
# Two limits governed a matrix upload and nothing kept them in agreement,
|
|
# so raising the documented one past the gateway's hardcoded cap changed
|
|
# nothing. The second clause is the control: the old directive is gone, so
|
|
# a pass means the value is derived rather than that `hasInfix` matched
|
|
# something incidental. It targets the whole directive rather than the
|
|
# bare size, because the rendered config carries the comment above it and
|
|
# a prose mention of the old value would defeat a looser arm.
|
|
name = "the matrix gateway's body cap is derived from maxRequestSize, not a literal";
|
|
ok =
|
|
let
|
|
c = matrixBodyCap.services.nginx.virtualHosts."chat.t.local".locations."/_matrix/".extraConfig;
|
|
in
|
|
lib.hasInfix "client_max_body_size 100048576;" c && !(lib.hasInfix "client_max_body_size 50M" c);
|
|
}
|
|
{
|
|
# Absence arm. A hive with no client identity gets no
|
|
# store environment at all — the daemon reports a queue it cannot serve
|
|
# rather than a handshake it cannot explain.
|
|
name = "a hive with no client identity gives hive-c0re no store environment";
|
|
ok =
|
|
let
|
|
s = matrixNoBaoIdentity.systemd.services;
|
|
in
|
|
s ? hive-c0re
|
|
&& !(s.hive-c0re.environment ? BAO_ADDR)
|
|
&& !(lib.any (c: lib.hasPrefix "bao-" c) s.hive-c0re.serviceConfig.LoadCredential);
|
|
}
|
|
{
|
|
# The homeserver's OIDC secret has one route, the store, under
|
|
# matrix-ctl's leaf, landing where `clientSecretFile` points: the host's
|
|
# /run (tmpfs), which the container sees read-only. No unit copies it out
|
|
# of authelia's tree, even on a host that runs authelia, and nothing
|
|
# writes it to persistent disk.
|
|
name = "the homeserver reads its OIDC secret from the store under matrix-ctl's leaf";
|
|
ok =
|
|
let
|
|
s = matrixBodyCap.systemd.services;
|
|
u = s.swarm-bao-matrix-oidc;
|
|
m = matrixBodyCap.services.hyperhive.deploy.matrix;
|
|
bind = matrixBodyCap.containers.hive-matrix.bindMounts."/run/hive-matrix-oidc";
|
|
tmpfiles = matrixBodyCap.systemd.tmpfiles.rules;
|
|
in
|
|
matrixBodyCap.services.hyperhive.deploy.authelia.enable
|
|
&& u.environment.BAO_CLIENT_CERT == m.ctlBaoClientCertFile
|
|
&& u.environment.BAO_CLIENT_KEY == m.ctlBaoClientKeyFile
|
|
&& m.sso.clientSecretFile == "/run/hive-matrix-oidc/tuwunel.secret"
|
|
&& lib.hasInfix "bao kv get -field=value secret/swarm/services/tuwunel/oidc/client" u.script
|
|
&& lib.hasInfix "atomic_write_secret 0400 \"\" /run/hive-matrix-oidc/tuwunel.secret" u.script
|
|
&& !(lib.hasInfix "/var/lib" u.script)
|
|
&& !(lib.hasInfix "nixos-containers" u.script)
|
|
# The container sees the directory the unit writes, and cannot write it.
|
|
&& bind.hostPath == "/run/hive-matrix-oidc"
|
|
&& bind.isReadOnly
|
|
# tmpfiles keeps the bind source and the credential source in place and
|
|
# removes the copy an earlier version left on persistent disk.
|
|
&& lib.elem "d /run/hive-matrix-oidc 0700 root root -" tmpfiles
|
|
&& lib.elem "f /run/hive-matrix-oidc/tuwunel.secret 0400 root root -" tmpfiles
|
|
&& lib.elem "r /var/lib/nixos-containers/hive-matrix/var/lib/tuwunel-oidc/tuwunel.secret" tmpfiles
|
|
&& lib.hasInfix "refresh_consumer hive-matrix tuwunel.service" u.script
|
|
# An unpublished or empty secret fails the unit, so `storeRetry` retries it.
|
|
&& !(lib.hasInfix "exit 0" u.script)
|
|
&& lib.elem "container@hive-matrix.service" u.before
|
|
&& !(s ? hive-matrix-oidc-secret);
|
|
}
|
|
{
|
|
# No matrix-ctl leaf is a homeserver with no OIDC secret, which tuwunel
|
|
# refuses to start on. The unit still renders, and fails naming the
|
|
# options.
|
|
name = "a homeserver host with no matrix-ctl leaf renders a secret fetch that fails naming the options";
|
|
ok =
|
|
let
|
|
u = matrixNoBaoIdentity.systemd.services.swarm-bao-matrix-oidc;
|
|
in
|
|
!(u.environment ? BAO_CLIENT_CERT)
|
|
&& lib.hasInfix "deploy.matrix.ctlBaoClientCertFile" u.script
|
|
&& lib.hasInfix "exit 1" u.script
|
|
&& !(lib.hasInfix "bao login" u.script);
|
|
}
|
|
{
|
|
# The legacy redirect reaches the hive domain's vhost from a glue
|
|
# module, merged into the vhost hive-c0re declares, and only where the
|
|
# homeserver runs. The matrix-elsewhere arm reads the same vhost, which
|
|
# still exists there with its redirect, so its absence of the redirect
|
|
# cannot pass on a misspelled name. Under the default server name (the
|
|
# swarm domain) the hive domain answers no discovery on either host.
|
|
# The fixture's gateway is on a non-default TLS port, which none of the
|
|
# URLs carry.
|
|
name = "an unpinned hive domain's vhost carries the matrix redirect where the homeserver runs, and no discovery";
|
|
ok =
|
|
let
|
|
here = matrixNoBaoIdentity.services.nginx.virtualHosts."h1.t.local".locations;
|
|
elsewhere = matrixElsewhere.services.nginx.virtualHosts."h1.t.local".locations;
|
|
in
|
|
matrixNoBaoIdentity.services.hyperhive.gateway.httpsPort == 8443
|
|
&& lib.hasInfix "rewrite ^/matrix/(.*)$ https://chat.t.local/$1 permanent;" (
|
|
here."/matrix/".extraConfig or ""
|
|
)
|
|
&& lib.hasInfix "return 301 https://h1.hives.t.local$request_uri;" here."/".extraConfig
|
|
&& !(here ? "= /.well-known/matrix/client")
|
|
&& !(here ? "= /.well-known/matrix/server")
|
|
&& elsewhere ? "/"
|
|
&& !(elsewhere ? "/matrix/")
|
|
&& !(elsewhere ? "= /.well-known/matrix/client");
|
|
}
|
|
{
|
|
# A homeserver pinned to the hive domain keeps its discovery on that
|
|
# vhost, even though the host runs no homeserver, and not on the apex, which is a
|
|
# different server name. The apex arm is read on a vhost that
|
|
# exists (its `/` and `/api/` are there), so it cannot pass on a
|
|
# misspelled name.
|
|
name = "a server name pinned to the hive domain is answered on the hive domain's vhost and not on the apex";
|
|
ok =
|
|
let
|
|
onHive = matrixPinned.services.nginx.virtualHosts."h1.t.local".locations;
|
|
apex = matrixPinned.services.nginx.virtualHosts."t.local".locations;
|
|
in
|
|
!matrixPinned.services.hyperhive.deploy.matrix.enable
|
|
&& lib.hasInfix ''{"m.homeserver":{"base_url":"https://chat.t.local"}}'' (
|
|
onHive."= /.well-known/matrix/client".extraConfig or ""
|
|
)
|
|
&& lib.hasInfix ''{"m.server":"chat.t.local:443"}'' (
|
|
onHive."= /.well-known/matrix/server".extraConfig or ""
|
|
)
|
|
&& onHive ? "/"
|
|
&& apex ? "/"
|
|
&& apex ? "/api/"
|
|
&& !(apex ? "= /.well-known/matrix/client")
|
|
&& !(apex ? "= /.well-known/matrix/server");
|
|
}
|
|
{
|
|
# tuwunel has no metrics of its own, so the hive collector requests
|
|
# `/_matrix/client/versions` through the gateway. The URL carries no
|
|
# port, like the rest of this module's URLs. The elsewhere host is the
|
|
# control: the same lookups there find no check, and the receiver stays
|
|
# out of its pipeline.
|
|
name = "the hive collector requests the homeserver's versions endpoint through the gateway, only where the homeserver runs";
|
|
ok =
|
|
let
|
|
url = "https://chat.t.local/_matrix/client/versions";
|
|
settings = m: m.services.opentelemetry-collector.settings;
|
|
pipeline = m: (settings m).service.pipelines.metrics.receivers;
|
|
in
|
|
matrixNoBaoIdentity.services.hyperhive.otel.httpChecks == [ url ]
|
|
&&
|
|
(settings matrixNoBaoIdentity).receivers.httpcheck.targets == [
|
|
{
|
|
endpoint = url;
|
|
method = "GET";
|
|
}
|
|
]
|
|
&& lib.elem "httpcheck" (pipeline matrixNoBaoIdentity)
|
|
&& matrixElsewhere.services.hyperhive.otel.httpChecks == [ ]
|
|
&& !((settings matrixElsewhere).receivers ? httpcheck)
|
|
&& !(lib.elem "httpcheck" (pipeline matrixElsewhere))
|
|
# Presence control for the two lookups above.
|
|
&& (settings matrixElsewhere).receivers ? hostmetrics
|
|
&& lib.elem "hostmetrics" (pipeline matrixElsewhere);
|
|
}
|
|
{
|
|
# The size is read from the database directory, as the dynamic user the
|
|
# homeserver runs as, and pushed to the forwarder's OTLP port. Without a
|
|
# store identity the container has no forwarder, so there is nothing to
|
|
# push to and neither unit appears; the hive with the identity is the
|
|
# control that the same lookups find them.
|
|
name = "the homeserver container pushes its database size through the forwarder, and only with a forwarder";
|
|
ok =
|
|
let
|
|
inner = m: m.containers.hive-matrix.config;
|
|
here = inner matrixBodyCap;
|
|
without = inner matrixNoBaoIdentity;
|
|
svc = here.systemd.services.hive-matrix-database-size;
|
|
f = here.services.hyperhive.swarmJournalForwarder;
|
|
in
|
|
svc.serviceConfig.Type == "oneshot"
|
|
&& svc.serviceConfig.DynamicUser
|
|
&& svc.serviceConfig.User == "tuwunel"
|
|
&& svc.serviceConfig.StateDirectory == here.services.matrix-tuwunel.stateDirectory
|
|
&& lib.hasInfix "du --summarize --bytes --dereference-args /var/lib/tuwunel" svc.script
|
|
&& lib.hasInfix "-lt 4096" svc.script
|
|
&& lib.hasInfix "hyperhive.matrix.database_bytes" svc.script
|
|
&& lib.hasInfix "127.0.0.1:${toString f.otlpMetricsPort}/v1/metrics" svc.script
|
|
&& f.otlpMetricsPort != null
|
|
&& here.systemd.timers.hive-matrix-database-size.timerConfig.OnUnitActiveSec == "5min"
|
|
&& !(without.systemd.services ? hive-matrix-database-size)
|
|
&& !(without.systemd.timers ? hive-matrix-database-size)
|
|
# Presence control: the container's units are found by the same lookup.
|
|
&& without.systemd.services ? tuwunel;
|
|
}
|
|
];
|
|
in
|
|
runGroup "matrix-core" cases
|