Watch
0
0
Fork
You've already forked hyperhive
0

move links, adminSync.ignoreUsers and scrapeTargets under their service's deploy namespace

This commit is contained in:
atlas 2026-11-01 20:34:50 +01:00
commit 90272ec3c0
7 changed files with 156 additions and 108 deletions

View file

@ -349,6 +349,18 @@ in
[ "services" "hyperhive" "c0re" "nixpkgsFlake" ]
[ "services" "hyperhive" "deploy" "builder" "nixpkgsFlake" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "controller" "links" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "links" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "matrix" "adminSync" "ignoreUsers" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "matrixAdminSync" "ignoreUsers" ]
)
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "swarm" "otel" "scrapeTargets" ]
[ "services" "hyperhive" "deploy" "swarm-otel" "scrapeTargets" ]
)
# The homeserver's port and its firewall switch, removed rather than
# renamed: tuwunel listens on a unix socket only the gateway reaches, so
# there is no host port to choose or open.

View file

@ -90,18 +90,6 @@ in
'';
};
adminSync.ignoreUsers = lib.mkOption {
type = lib.types.listOf (lib.types.strMatching "@[^:[:space:]]+:[^[:space:]]+");
default = [ ];
example = [ "@mara:example.org" ];
description = ''
Full matrix user ids `swarm-controller`'s operator admin sync never
revokes homeserver admin from. The sync revokes admin only from
accounts it made admin itself, once their owner leaves authelia's
`admins` group; an account listed here keeps admin regardless.
'';
};
# This homeserver always delegates login to the swarm's authelia, as
# an OIDC relying party — matrix SSO (`m.login.sso`), offered
# alongside password login. No toggle: a homeserver in a swarm is a

View file

@ -3033,7 +3033,7 @@ in
# granter step, on the host where that step runs.
environment.etc."hyperhive/bao-bootstrap-policy.hcl".source = ./bao-bootstrap-policy.hcl;
# 🚫 No `swarm.otel.scrapeTargets.bao` entry, and its absence is
# 🚫 No `deploy.swarm-otel.scrapeTargets.bao` entry, and its absence is
# deliberate. That option is read only by the SWARM collector, over
# loopback, so a store declared there would have metrics exactly on the
# hosts that also run swarm-otel and nowhere else — silently, since a

View file

@ -391,6 +391,20 @@ in
'';
};
};
# What stays above is what the controller IS to the swarm:
# the client id it is registered under, and how stale a
# snapshot may read. What the host running it decides is here — the quick
# links its UI serves, the accounts its matrix admin sync leaves alone, which two
# builds it runs, where its socket lives, and the three credentials it reads
# off local disk. `enable` already lives in ./deploy.nix, which also carries
# the renames.
#
# ⚠️ `deploy.swarm-controller`, NOT `deploy.hive-controller`. Both exist:
# the latter is hive-c0re's namespace, where `tls.*` lives. A definition on
# the wrong one lands on a live, unrelated option rather than failing.
options.services.hyperhive.deploy.swarm-controller = {
links = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
@ -437,19 +451,18 @@ in
'';
};
};
matrixAdminSync.ignoreUsers = lib.mkOption {
type = lib.types.listOf (lib.types.strMatching "@[^:[:space:]]+:[^[:space:]]+");
default = [ ];
example = [ "@mara:example.org" ];
description = ''
Full matrix user ids the controller's operator admin sync never
revokes homeserver admin from. The sync revokes admin only from
accounts it made admin itself, once their owner leaves authelia's
`admins` group; an account listed here keeps admin regardless.
'';
};
# What stays above is what the controller IS to the swarm: the quick-links
# it publishes, the client id it is registered under, and how stale a
# snapshot may read. What the host running it decides is here — which two
# builds it runs, where its socket lives, and the three credentials it reads
# off local disk. `enable` already lives in ./deploy.nix, which also carries
# the renames.
#
# ⚠️ `deploy.swarm-controller`, NOT `deploy.hive-controller`. Both exist:
# the latter is hive-c0re's namespace, where `tls.*` lives. A definition on
# the wrong one lands on a live, unrelated option rather than failing.
options.services.hyperhive.deploy.swarm-controller = {
queue = {
natsUrl = lib.mkOption {
type = lib.types.str;
@ -662,7 +675,7 @@ in
};
config = lib.mkIf deployCfg.swarm-controller.enable {
services.hyperhive.swarm.controller.links = swarmServiceLinks;
services.hyperhive.deploy.swarm-controller.links = swarmServiceLinks;
users.users.swarm-controller = {
isSystemUser = true;
@ -848,7 +861,7 @@ in
# The merged links list — see `links`' description above for who
# contributes to it. Consumed by `GET /api/links`
# (swarm-controller/src/startup.rs::load_links).
SWARM_CONTROLLER_LINKS = builtins.toJSON cfg.links;
SWARM_CONTROLLER_LINKS = builtins.toJSON deployCfg.swarm-controller.links;
# Staleness threshold for `GET /api/hives/status` — see
# `staleAfterSeconds`' description. Set unconditionally rather
# than inside `queueEnv`: it is not a queue coordinate, and
@ -858,7 +871,7 @@ in
# The accounts the operator admin sync never revokes. Whitespace-
# separated: a matrix user id cannot contain a space. Consumed by
# swarm-controller/src/matrix_account/operator_admin.rs.
SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS = lib.concatStringsSep " " matrixCfg.adminSync.ignoreUsers;
SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS = lib.concatStringsSep " " deployCfg.swarm-controller.matrixAdminSync.ignoreUsers;
# Where `GET /api/alerts` reads alerts (swarm-controller/src/alerts.rs).
# Every swarm has one Grafana, reached by name wherever it runs. The
# token is in the store, written by `swarm-bao-grafana-controller-token`

View file

@ -138,58 +138,6 @@ in
'';
};
scrapeTargets = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''{ collector = "127.0.0.1:8889"; }'';
description = ''
Prometheus exposition endpoints this collector scrapes, as
`<job name> = "<host>:<port>"`. The collector runs in a network namespace of
its own, so `127.0.0.1` is the collector container's loopback: a target
here is a listener inside that container, such as its own telemetry.
A path and query may follow the port —
`"127.0.0.1:9100/stats?format=prometheus"` — for an
exporter that does not serve `/metrics`. Both are optional and
omitted when absent, so a bare `host:port` is scraped exactly as
before.
**A service declares its own entry, from its own module, under its
own `enable`.** Do not assemble the list here: an entry then exists
only where the service that named it runs, so a target is never
declared on a host that does not serve it. A swarm service on the
host is not reachable here, since its loopback is not the collector's:
it is a `publishedScrapeTargets` entry instead, which a collector on
any host scrapes.
⚠️ **That constrains the TARGET, not the SCRAPER, and the difference
is a silent gap.** Nothing here puts the collector on the same host.
Services are co-located by *default* — `swarm-required-services.nix`
derives their `enable` flags from one `lib.mkDefault` — not by
construction, and an operator may split them.
When they are split the target is simply never scraped: the
service's host declares an entry no local collector reads, and the
collector's host never enabled that service so has no entry at all.
No error, no eval failure, no warning.
**No assertion can catch this.** Two hosts are separate NixOS
evaluations with no shared context, so neither can see what the
other runs. A service that would be seriously wrong to lose should
say so in its own contribution, because saying it is the only
mechanism available.
Samples land in a swarm-level pipeline that stamps `swarm` and
**never** `hive`: a swarm service does not belong to a hive, and
`hive` stays a property of which authenticated receiver accepted a
push, not something a scrape can acquire.
Empty by default, in which case no scrape receiver, processor or
pipeline is emitted at all — an enabled scraper with nothing to
scrape is the inert configuration this option exists to avoid.
'';
};
publishedScrapeTargets = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
@ -200,7 +148,7 @@ in
**Entries are declared swarm-wide**: from this module's `config`,
unconditionally, keyed on the service's `swarm.<svc>.domain`, so
every host evaluates the same set. Unlike `scrapeTargets`, an entry
every host evaluates the same set. Unlike `deploy.swarm-otel.scrapeTargets`, an entry
is not declared under the service's own `enable`: the collector
reads this list on its host and authelia's registration reads it on
authelia's host, and neither need be the service's host. A swarm
@ -222,7 +170,7 @@ in
slash, `http` for `https`) presents as a valid token rejected at
the target, several layers from its cause.
⚠️ Deliberately separate from `scrapeTargets`. An entry there is
⚠️ Deliberately separate from `deploy.swarm-otel.scrapeTargets`. An entry there is
trusted because the scraper and the target share a host — that
option is loopback-and-unauthenticated by contract. An entry here
is trusted because it presents a credential. One shape for both

View file

@ -18,6 +18,7 @@ let
cfg = config.services.hyperhive.swarm.otel;
deployCfg = config.services.hyperhive.deploy;
upstream = deployCfg.swarm-otel.upstream;
scrapeTargets = deployCfg.swarm-otel.scrapeTargets;
vmCfg = config.services.hyperhive.swarm.victoriametrics;
vlCfg = config.services.hyperhive.swarm.victorialogs;
hyperhiveCfg = config.services.hyperhive;
@ -333,6 +334,58 @@ in
# operator's choice for this deployment, so both hang off the deployment.
# `enable` already lives in ./deploy.nix, which also carries the rename.
options.services.hyperhive.deploy.swarm-otel = {
scrapeTargets = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''{ collector = "127.0.0.1:8889"; }'';
description = ''
Prometheus exposition endpoints this collector scrapes, as
`<job name> = "<host>:<port>"`. The collector runs in a network namespace of
its own, so `127.0.0.1` is the collector container's loopback: a target
here is a listener inside that container, such as its own telemetry.
A path and query may follow the port —
`"127.0.0.1:9100/stats?format=prometheus"` — for an
exporter that does not serve `/metrics`. Both are optional and
omitted when absent, so a bare `host:port` is scraped exactly as
before.
**A service declares its own entry, from its own module, under its
own `enable`.** Do not assemble the list here: an entry then exists
only where the service that named it runs, so a target is never
declared on a host that does not serve it. A swarm service on the
host is not reachable here, since its loopback is not the collector's:
it is a `publishedScrapeTargets` entry instead, which a collector on
any host scrapes.
⚠️ **That constrains the TARGET, not the SCRAPER, and the difference
is a silent gap.** Nothing here puts the collector on the same host.
Services are co-located by *default* — `swarm-required-services.nix`
derives their `enable` flags from one `lib.mkDefault` — not by
construction, and an operator may split them.
When they are split the target is simply never scraped: the
service's host declares an entry no local collector reads, and the
collector's host never enabled that service so has no entry at all.
No error, no eval failure, no warning.
**No assertion can catch this.** Two hosts are separate NixOS
evaluations with no shared context, so neither can see what the
other runs. A service that would be seriously wrong to lose should
say so in its own contribution, because saying it is the only
mechanism available.
Samples land in a swarm-level pipeline that stamps `swarm` and
**never** `hive`: a swarm service does not belong to a hive, and
`hive` stays a property of which authenticated receiver accepted a
push, not something a scrape can acquire.
Empty by default, in which case no scrape receiver, processor or
pipeline is emitted at all — an enabled scraper with nothing to
scrape is the inert configuration this option exists to avoid.
'';
};
telemetryPort = lib.mkOption {
type = lib.types.port;
default = 8889;
@ -498,7 +551,7 @@ in
# alive, these counters say whether it is dropping what it receives.
# Loopback is the container's own: this collector scrapes `telemetryPort`
# inside its netns, and nothing else reads it.
services.hyperhive.swarm.otel.scrapeTargets.collector =
services.hyperhive.deploy.swarm-otel.scrapeTargets.collector =
"127.0.0.1:${toString deployCfg.swarm-otel.telemetryPort}";
# OTLP/HTTP, not a browsable UI, but the same reverse-proxy shape as
@ -935,11 +988,11 @@ in
# Same reason the published one is asserted: a value the grammar
# rejects surfaces as `attempt to call elemAt on null` from inside
# the renderer, naming neither the option nor the offending value.
assertion = lib.all (t: parseScrape t != null) (lib.attrValues cfg.scrapeTargets);
assertion = lib.all (t: parseScrape t != null) (lib.attrValues scrapeTargets);
message = ''
services.hyperhive.swarm.otel.scrapeTargets has ${
services.hyperhive.deploy.swarm-otel.scrapeTargets has ${
lib.concatMapStringsSep ", " (kv: "${kv.name} = \"${kv.value}\"") (
lib.filter (kv: parseScrape kv.value == null) (lib.attrsToList cfg.scrapeTargets)
lib.filter (kv: parseScrape kv.value == null) (lib.attrsToList scrapeTargets)
)
}, which is not of the form <host>:<port>[/<path>][?<k>=<v>].
@ -959,12 +1012,11 @@ in
# The message names which side each collision came from, which is
# the part a rendered-config error could not tell an operator.
assertion =
lib.intersectLists (lib.attrNames cfg.scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets)
== [ ];
lib.intersectLists (lib.attrNames scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets) == [ ];
message = ''
services.hyperhive.swarm.otel: ${
lib.concatMapStringsSep ", " (j: "'${j}'") (
lib.intersectLists (lib.attrNames cfg.scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets)
lib.intersectLists (lib.attrNames scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets)
)
} is declared as both a loopback
scrapeTargets job and a publishedScrapeTargets job.
@ -1149,9 +1201,9 @@ in
# target: a `prometheus` receiver with nothing to scrape is
# the shape this whole issue is about, a config that renders
# and deploys perfectly while adding no data.
// lib.optionalAttrs (cfg.scrapeTargets != { } || cfg.publishedScrapeTargets != { }) {
// lib.optionalAttrs (scrapeTargets != { } || cfg.publishedScrapeTargets != { }) {
prometheus.config.scrape_configs =
lib.mapAttrsToList loopbackScrapeConfig cfg.scrapeTargets
lib.mapAttrsToList loopbackScrapeConfig scrapeTargets
# ⚠️ `++`, so the two kinds of target land in ONE list —
# which is exactly why a job name may not appear in both
# options. A list concatenation does not resolve a
@ -1410,7 +1462,7 @@ in
# separated FOR is the credential it proves, and that is
# the receiver's job, not the pipeline's.
++ map (n: "otlp/${n}") serviceProducerNames
++ lib.optional (cfg.scrapeTargets != { } || cfg.publishedScrapeTargets != { }) "prometheus";
++ lib.optional (scrapeTargets != { } || cfg.publishedScrapeTargets != { }) "prometheus";
processors = [ "resource/${swarmTierName}" ];
exporters = exporterNames;
};

View file

@ -177,8 +177,8 @@ in
# so — unlike authelia/matrix/forge's entries — this one needs no
# host name and is never conditional on anything but this module
# being enabled at all. See
# `services.hyperhive.swarm.controller.links`'s description.
services.hyperhive.swarm.controller.links = [
# `services.hyperhive.deploy.swarm-controller.links`'s description.
services.hyperhive.deploy.swarm-controller.links = [
{
label = "API docs";
icon = "🧬";