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
19 changed files with 156 additions and 108 deletions

View file

@ -217,7 +217,7 @@ lands on.
- **Admins the sync didn't make keep admin.** An admin missing from that - **Admins the sync didn't make keep admin.** An admin missing from that
file, `@swarm`, the server user `@conduit:<server_name>`, every agent's file, `@swarm`, the server user `@conduit:<server_name>`, every agent's
account, and every user id in account, and every user id in
`services.hyperhive.swarm.matrix.adminSync.ignoreUsers` are never `services.hyperhive.deploy.swarm-controller.matrixAdminSync.ignoreUsers` are never
revoked. revoked.
- **The last admin stays.** A revoke that would leave no admin in - **The last admin stays.** A revoke that would leave no admin in
`#admins` besides `@swarm` and the server user isn't sent; the `#admins` besides `@swarm` and the server user isn't sent; the

View file

@ -945,7 +945,7 @@ the subrequest denies it.
The swarm UI's header carries a single 🔗 button, visible on every route, The swarm UI's header carries a single 🔗 button, visible on every route,
opening a popover of links to other swarm-wide services. Backed by opening a popover of links to other swarm-wide services. Backed by
`GET /api/links` (swarm-controller), which serves `GET /api/links` (swarm-controller), which serves
`services.hyperhive.swarm.controller.links` (a `listOf { label, icon, url }`, `services.hyperhive.deploy.swarm-controller.links` (a `listOf { label, icon, url }`,
same shape as the per-agent `services.hyperhive.agent.dashboardLinks`). same shape as the per-agent `services.hyperhive.agent.dashboardLinks`).
Links to swarm services come from swarm-level options, so the list is the Links to swarm services come from swarm-level options, so the list is the

View file

@ -349,6 +349,18 @@ in
[ "services" "hyperhive" "c0re" "nixpkgsFlake" ] [ "services" "hyperhive" "c0re" "nixpkgsFlake" ]
[ "services" "hyperhive" "deploy" "builder" "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 # The homeserver's port and its firewall switch, removed rather than
# renamed: tuwunel listens on a unix socket only the gateway reaches, so # renamed: tuwunel listens on a unix socket only the gateway reaches, so
# there is no host port to choose or open. # 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 # This homeserver always delegates login to the swarm's authelia, as
# an OIDC relying party — matrix SSO (`m.login.sso`), offered # an OIDC relying party — matrix SSO (`m.login.sso`), offered
# alongside password login. No toggle: a homeserver in a swarm is a # 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. # granter step, on the host where that step runs.
environment.etc."hyperhive/bao-bootstrap-policy.hcl".source = ./bao-bootstrap-policy.hcl; 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 # deliberate. That option is read only by the SWARM collector, over
# loopback, so a store declared there would have metrics exactly on the # loopback, so a store declared there would have metrics exactly on the
# hosts that also run swarm-otel and nowhere else — silently, since a # 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 { links = lib.mkOption {
type = lib.types.listOf ( type = lib.types.listOf (
lib.types.submodule { 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 = { queue = {
natsUrl = lib.mkOption { natsUrl = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -662,7 +675,7 @@ in
}; };
config = lib.mkIf deployCfg.swarm-controller.enable { config = lib.mkIf deployCfg.swarm-controller.enable {
services.hyperhive.swarm.controller.links = swarmServiceLinks; services.hyperhive.deploy.swarm-controller.links = swarmServiceLinks;
users.users.swarm-controller = { users.users.swarm-controller = {
isSystemUser = true; isSystemUser = true;
@ -848,7 +861,7 @@ in
# The merged links list — see `links`' description above for who # The merged links list — see `links`' description above for who
# contributes to it. Consumed by `GET /api/links` # contributes to it. Consumed by `GET /api/links`
# (swarm-controller/src/startup.rs::load_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 # Staleness threshold for `GET /api/hives/status` — see
# `staleAfterSeconds`' description. Set unconditionally rather # `staleAfterSeconds`' description. Set unconditionally rather
# than inside `queueEnv`: it is not a queue coordinate, and # than inside `queueEnv`: it is not a queue coordinate, and
@ -858,7 +871,7 @@ in
# The accounts the operator admin sync never revokes. Whitespace- # The accounts the operator admin sync never revokes. Whitespace-
# separated: a matrix user id cannot contain a space. Consumed by # separated: a matrix user id cannot contain a space. Consumed by
# swarm-controller/src/matrix_account/operator_admin.rs. # 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). # Where `GET /api/alerts` reads alerts (swarm-controller/src/alerts.rs).
# Every swarm has one Grafana, reached by name wherever it runs. The # Every swarm has one Grafana, reached by name wherever it runs. The
# token is in the store, written by `swarm-bao-grafana-controller-token` # 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 { publishedScrapeTargets = lib.mkOption {
type = lib.types.attrsOf lib.types.str; type = lib.types.attrsOf lib.types.str;
default = { }; default = { };
@ -200,7 +148,7 @@ in
**Entries are declared swarm-wide**: from this module's `config`, **Entries are declared swarm-wide**: from this module's `config`,
unconditionally, keyed on the service's `swarm.<svc>.domain`, so 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 is not declared under the service's own `enable`: the collector
reads this list on its host and authelia's registration reads it on 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 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 slash, `http` for `https`) presents as a valid token rejected at
the target, several layers from its cause. 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 trusted because the scraper and the target share a host — that
option is loopback-and-unauthenticated by contract. An entry here option is loopback-and-unauthenticated by contract. An entry here
is trusted because it presents a credential. One shape for both is trusted because it presents a credential. One shape for both

View file

@ -18,6 +18,7 @@ let
cfg = config.services.hyperhive.swarm.otel; cfg = config.services.hyperhive.swarm.otel;
deployCfg = config.services.hyperhive.deploy; deployCfg = config.services.hyperhive.deploy;
upstream = deployCfg.swarm-otel.upstream; upstream = deployCfg.swarm-otel.upstream;
scrapeTargets = deployCfg.swarm-otel.scrapeTargets;
vmCfg = config.services.hyperhive.swarm.victoriametrics; vmCfg = config.services.hyperhive.swarm.victoriametrics;
vlCfg = config.services.hyperhive.swarm.victorialogs; vlCfg = config.services.hyperhive.swarm.victorialogs;
hyperhiveCfg = config.services.hyperhive; hyperhiveCfg = config.services.hyperhive;
@ -333,6 +334,58 @@ in
# operator's choice for this deployment, so both hang off the deployment. # operator's choice for this deployment, so both hang off the deployment.
# `enable` already lives in ./deploy.nix, which also carries the rename. # `enable` already lives in ./deploy.nix, which also carries the rename.
options.services.hyperhive.deploy.swarm-otel = { 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 { telemetryPort = lib.mkOption {
type = lib.types.port; type = lib.types.port;
default = 8889; default = 8889;
@ -498,7 +551,7 @@ in
# alive, these counters say whether it is dropping what it receives. # alive, these counters say whether it is dropping what it receives.
# Loopback is the container's own: this collector scrapes `telemetryPort` # Loopback is the container's own: this collector scrapes `telemetryPort`
# inside its netns, and nothing else reads it. # 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}"; "127.0.0.1:${toString deployCfg.swarm-otel.telemetryPort}";
# OTLP/HTTP, not a browsable UI, but the same reverse-proxy shape as # 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 # Same reason the published one is asserted: a value the grammar
# rejects surfaces as `attempt to call elemAt on null` from inside # rejects surfaces as `attempt to call elemAt on null` from inside
# the renderer, naming neither the option nor the offending value. # 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 = '' message = ''
services.hyperhive.swarm.otel.scrapeTargets has ${ services.hyperhive.deploy.swarm-otel.scrapeTargets has ${
lib.concatMapStringsSep ", " (kv: "${kv.name} = \"${kv.value}\"") ( 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>]. }, 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 message names which side each collision came from, which is
# the part a rendered-config error could not tell an operator. # the part a rendered-config error could not tell an operator.
assertion = assertion =
lib.intersectLists (lib.attrNames cfg.scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets) lib.intersectLists (lib.attrNames scrapeTargets) (lib.attrNames cfg.publishedScrapeTargets) == [ ];
== [ ];
message = '' message = ''
services.hyperhive.swarm.otel: ${ services.hyperhive.swarm.otel: ${
lib.concatMapStringsSep ", " (j: "'${j}'") ( 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 } is declared as both a loopback
scrapeTargets job and a publishedScrapeTargets job. scrapeTargets job and a publishedScrapeTargets job.
@ -1149,9 +1201,9 @@ in
# target: a `prometheus` receiver with nothing to scrape is # target: a `prometheus` receiver with nothing to scrape is
# the shape this whole issue is about, a config that renders # the shape this whole issue is about, a config that renders
# and deploys perfectly while adding no data. # and deploys perfectly while adding no data.
// lib.optionalAttrs (cfg.scrapeTargets != { } || cfg.publishedScrapeTargets != { }) { // lib.optionalAttrs (scrapeTargets != { } || cfg.publishedScrapeTargets != { }) {
prometheus.config.scrape_configs = prometheus.config.scrape_configs =
lib.mapAttrsToList loopbackScrapeConfig cfg.scrapeTargets lib.mapAttrsToList loopbackScrapeConfig scrapeTargets
# ⚠️ `++`, so the two kinds of target land in ONE list — # ⚠️ `++`, so the two kinds of target land in ONE list —
# which is exactly why a job name may not appear in both # which is exactly why a job name may not appear in both
# options. A list concatenation does not resolve a # options. A list concatenation does not resolve a
@ -1410,7 +1462,7 @@ in
# separated FOR is the credential it proves, and that is # separated FOR is the credential it proves, and that is
# the receiver's job, not the pipeline's. # the receiver's job, not the pipeline's.
++ map (n: "otlp/${n}") serviceProducerNames ++ map (n: "otlp/${n}") serviceProducerNames
++ lib.optional (cfg.scrapeTargets != { } || cfg.publishedScrapeTargets != { }) "prometheus"; ++ lib.optional (scrapeTargets != { } || cfg.publishedScrapeTargets != { }) "prometheus";
processors = [ "resource/${swarmTierName}" ]; processors = [ "resource/${swarmTierName}" ];
exporters = exporterNames; exporters = exporterNames;
}; };

View file

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

View file

@ -62,7 +62,7 @@ let
controllerPinnedMatrix = hive { controllerPinnedMatrix = hive {
deploy.swarm-controller.enable = true; deploy.swarm-controller.enable = true;
swarm.matrix.gatewayHost = "matrix.hive.t.local"; swarm.matrix.gatewayHost = "matrix.hive.t.local";
swarm.matrix.adminSync.ignoreUsers = [ deploy.swarm-controller.matrixAdminSync.ignoreUsers = [
"@keep-a:t.local" "@keep-a:t.local"
"@keep-b:t.local" "@keep-b:t.local"
]; ];

View file

@ -1594,6 +1594,15 @@ let
victorialogs.port = 19428; victorialogs.port = 19428;
forge.httpPort = 13000; forge.httpPort = 13000;
matrix.allowEncryption = true; matrix.allowEncryption = true;
controller.links = [
{
label = "Legacy";
icon = "L";
url = "https://legacy.example/";
}
];
matrix.adminSync.ignoreUsers = [ "@legacy:t.local" ];
otel.scrapeTargets.legacy = "127.0.0.1:9777";
}; };
}; };
d = old.services.hyperhive.deploy; d = old.services.hyperhive.deploy;
@ -1623,6 +1632,19 @@ let
&& d.forgejo.httpPort == 13000 && d.forgejo.httpPort == 13000
&& d.matrix.allowEncryption && d.matrix.allowEncryption
&& d.builder.nixpkgsFlake == "path:/legacy/nixpkgs" && d.builder.nixpkgsFlake == "path:/legacy/nixpkgs"
&& lib.any (l: l.label == "Legacy" && l.url == "https://legacy.example/") d.swarm-controller.links
&& lib.any (l: l.label == "Legacy") (
builtins.fromJSON old.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_LINKS
)
&& d.swarm-controller.matrixAdminSync.ignoreUsers == [ "@legacy:t.local" ]
&&
old.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS
== "@legacy:t.local"
&& d.swarm-otel.scrapeTargets.legacy == "127.0.0.1:9777"
&&
(lib.findFirst (j: j.job_name == "legacy") null (
(otelSettings old).receivers.prometheus.config.scrape_configs
)).static_configs == [ { targets = [ "127.0.0.1:9777" ]; } ]
&& &&
old.containers.swarm-builder.config.systemd.services.swarm-builder.environment.SWARM_BUILDER_NIXPKGS_FLAKE old.containers.swarm-builder.config.systemd.services.swarm-builder.environment.SWARM_BUILDER_NIXPKGS_FLAKE
== "path:/legacy/nixpkgs" == "path:/legacy/nixpkgs"
@ -1642,6 +1664,9 @@ let
"swarm.forge.httpPort" "swarm.forge.httpPort"
"swarm.matrix.allowEncryption" "swarm.matrix.allowEncryption"
"c0re.nixpkgsFlake" "c0re.nixpkgsFlake"
"swarm.controller.links"
"swarm.matrix.adminSync.ignoreUsers"
"swarm.otel.scrapeTargets"
] ]
&& !(lib.any (w: lib.hasInfix "swarm.controller.queue" w) allLocal.warnings) && !(lib.any (w: lib.hasInfix "swarm.controller.queue" w) allLocal.warnings)
&& refused old "deploy.nats.otel.telemetryPort" && refused old "deploy.nats.otel.telemetryPort"
@ -1672,7 +1697,15 @@ let
&& (locs h.swarm.nats.domain)."= /metrics".proxyPass == "http://127.0.0.1:7777/metrics" && (locs h.swarm.nats.domain)."= /metrics".proxyPass == "http://127.0.0.1:7777/metrics"
&& (locs h.swarm.victorialogs.domain)."= /metrics".proxyPass == "http://127.0.0.1:9428/metrics" && (locs h.swarm.victorialogs.domain)."= /metrics".proxyPass == "http://127.0.0.1:9428/metrics"
&& (locs h.swarm.forge.domain)."/".proxyPass == "http://127.0.0.1:3000/" && (locs h.swarm.forge.domain)."/".proxyPass == "http://127.0.0.1:3000/"
&& !allLocal.containers.hive-matrix.config.services.matrix-tuwunel.settings.global.allow_encryption; && !allLocal.containers.hive-matrix.config.services.matrix-tuwunel.settings.global.allow_encryption
&& env.SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS == ""
&& lib.any (l: l.label == "Forge" && l.url == "https://${h.swarm.forge.domain}/") (
builtins.fromJSON env.SWARM_CONTROLLER_LINKS
)
&&
(lib.findFirst (j: j.job_name == "collector") null (
(otelSettings allLocal).receivers.prometheus.config.scrape_configs
)).static_configs == [ { targets = [ "127.0.0.1:8889" ]; } ];
} }
{ {
# Each old spelling lands on the new path, with its own rename warning; # Each old spelling lands on the new path, with its own rename warning;

View file

@ -127,7 +127,7 @@ let
name = "a host running the collector, authelia, grafana and nats scrapes none of them on loopback"; name = "a host running the collector, authelia, grafana and nats scrapes none of them on loopback";
ok = ok =
let let
t = servicesHere.services.hyperhive.swarm.otel.scrapeTargets; t = servicesHere.services.hyperhive.deploy.swarm-otel.scrapeTargets;
in in
servicesHere.services.hyperhive.deploy.nats.enable servicesHere.services.hyperhive.deploy.nats.enable
&& !(t ? authelia) && !(t ? authelia)

View file

@ -300,8 +300,8 @@ let
name = "authelia and nats declare no scrape target on a host with no collector"; name = "authelia and nats declare no scrape target on a host with no collector";
ok = ok =
!(autheliaAndNats.services.hyperhive.deploy.swarm-otel.enable) !(autheliaAndNats.services.hyperhive.deploy.swarm-otel.enable)
&& !(autheliaAndNats.services.hyperhive.swarm.otel.scrapeTargets ? authelia) && !(autheliaAndNats.services.hyperhive.deploy.swarm-otel.scrapeTargets ? authelia)
&& !(autheliaAndNats.services.hyperhive.swarm.otel.scrapeTargets ? nats); && !(autheliaAndNats.services.hyperhive.deploy.swarm-otel.scrapeTargets ? nats);
} }
{ {
# The xkey and the responder's OIDC client secret are read only from # The xkey and the responder's OIDC client secret are read only from

View file

@ -100,7 +100,7 @@ in
baoWithCollector = hive { baoWithCollector = hive {
deploy.bao.enable = true; deploy.bao.enable = true;
deploy.swarm-otel.enable = true; deploy.swarm-otel.enable = true;
swarm.otel.scrapeTargets.plain = "127.0.0.1:9999"; deploy.swarm-otel.scrapeTargets.plain = "127.0.0.1:9999";
}; };
# `clientSecretFile` is what ../host-modules/otel.nix's identity assertion # `clientSecretFile` is what ../host-modules/otel.nix's identity assertion

View file

@ -81,7 +81,7 @@ let
# is a running collector with no self-scrape, not an absent one. # is a running collector with no self-scrape, not an absent one.
otelOnlyPublished = hive { otelOnlyPublished = hive {
deploy.swarm-otel.enable = true; deploy.swarm-otel.enable = true;
swarm.otel.scrapeTargets = lib.mkForce { }; deploy.swarm-otel.scrapeTargets = lib.mkForce { };
swarm.otel.publishedScrapeTargets.remote = "https://remote.t.local/metrics"; swarm.otel.publishedScrapeTargets.remote = "https://remote.t.local/metrics";
}; };
@ -175,7 +175,7 @@ let
let let
s = otelSettings otelOnlyPublished; s = otelSettings otelOnlyPublished;
in in
otelOnlyPublished.services.hyperhive.swarm.otel.scrapeTargets == { } otelOnlyPublished.services.hyperhive.deploy.swarm-otel.scrapeTargets == { }
&& otelOnlyPublished.services.hyperhive.swarm.otel.publishedScrapeTargets != { } && otelOnlyPublished.services.hyperhive.swarm.otel.publishedScrapeTargets != { }
&& (s.receivers ? prometheus) && (s.receivers ? prometheus)
&& builtins.elem "prometheus" s.service.pipelines."metrics/swarm".receivers; && builtins.elem "prometheus" s.service.pipelines."metrics/swarm".receivers;

View file

@ -167,7 +167,9 @@ let
}; };
swarmServiceLinksOf = swarmServiceLinksOf =
cfg: cfg:
lib.filter (l: swarmServiceLinkDomains ? ${l.label}) cfg.services.hyperhive.swarm.controller.links; lib.filter (
l: swarmServiceLinkDomains ? ${l.label}
) cfg.services.hyperhive.deploy.swarm-controller.links;
# Every service container at once: the services-only host plus the CI # Every service container at once: the services-only host plus the CI
# runner. The CI runner, the review classifier, the notification router, the # runner. The CI runner, the review classifier, the notification router, the

View file

@ -54,7 +54,7 @@ const SERVER_USER_LOCALPART: &str = "conduit";
const GRANTED_FILE: &str = "operator-matrix-admins.json"; const GRANTED_FILE: &str = "operator-matrix-admins.json";
/// Whitespace-separated user ids the revoke never touches, set from /// Whitespace-separated user ids the revoke never touches, set from
/// `services.hyperhive.swarm.matrix.adminSync.ignoreUsers`. /// `services.hyperhive.deploy.swarm-controller.matrixAdminSync.ignoreUsers`.
const IGNORE_USERS_ENV: &str = "SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS"; const IGNORE_USERS_ENV: &str = "SWARM_CONTROLLER_MATRIX_ADMIN_SYNC_IGNORE_USERS";
/// The localpart an SSO login by authelia user `username` lands on, or `None` /// The localpart an SSO login by authelia user `username` lands on, or `None`

View file

@ -37,7 +37,7 @@ pub(crate) async fn get_hives(State(state): State<AppState>) -> Json<Vec<HiveEnt
/// One quick link to a swarm-wide service (authelia, matrix, forge, this /// One quick link to a swarm-wide service (authelia, matrix, forge, this
/// daemon's own swagger UI, …). Deliberately generic rather than named /// daemon's own swagger UI, …). Deliberately generic rather than named
/// fields per service: each service's own nix module contributes its own /// fields per service: each service's own nix module contributes its own
/// entry to `services.hyperhive.swarm.controller.links` (same list-merge /// entry to `services.hyperhive.deploy.swarm-controller.links` (same list-merge
/// idiom `services.hyperhive.gateway.localNames` already uses), so adding /// idiom `services.hyperhive.gateway.localNames` already uses), so adding
/// a new one is a nix-only change — no new field here, no swarm-ui change. /// a new one is a nix-only change — no new field here, no swarm-ui change.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]

View file

@ -110,7 +110,7 @@ pub(crate) fn load_hives() -> Vec<HiveEntry> {
} }
/// Env var the controller's NixOS module sets from the merged /// Env var the controller's NixOS module sets from the merged
/// `services.hyperhive.swarm.controller.links` list, JSON-encoded — same /// `services.hyperhive.deploy.swarm-controller.links` list, JSON-encoded — same
/// shape/rationale as [`HIVES_ENV`]. Consumed by `GET /api/links`. /// shape/rationale as [`HIVES_ENV`]. Consumed by `GET /api/links`.
const LINKS_ENV: &str = "SWARM_CONTROLLER_LINKS"; const LINKS_ENV: &str = "SWARM_CONTROLLER_LINKS";