The swarm-nats container now runs its own otel collector, which reads the
container's journal and pushes it to the swarm collector at
https://<swarm.otel.domain>/swarm-nats, authenticated as the queue's
existing OIDC client (swarm.nats.clientId). Records carry
service.name=nats and no hive label.
The collector lives in a reusable container module,
nix/container-modules/swarm-journal-forwarder.nix, which follows
swarm-bao's forwarder: a journald receiver over /var/log/journal with
merge and a file_storage cursor, an otlphttp exporter with an
oauth2client authenticator that requests its own client id as audience,
self-metrics on a loopback telemetry port, and the client secret handed
in through LoadCredential. Unlike swarm-bao, the secret is fetched inside
the container by swarm-journal-forwarder-oidc.service, under the
container's own store identity (the swarm-nats-auth leaf, which already
holds a read grant on secret/swarm/services/swarm-nats/oidc/client).
The fetch retries until the store answers, and the collector restarts
until the file exists. A 10-minute timer re-reads the secret and restarts
the collector when it changed.
The swarm collector's authenticated producer route was hard-wired to
swarm-bao. It is now rendered from swarm.otel.serviceProducers
(internal), one receiver, oidc authenticator, gateway location and
pipeline entry per service: swarm-bao (port 4391, unchanged names) and
swarm-nats (port 4392). The reserved-fragment and port-collision
assertions cover every entry.
The queue's OIDC client registration gains its own id as audience and
RS256-signed access tokens, which the receiver verifies offline.
swarm-nats drops --link-journal=host, so its journal stays in the guest
where the forwarder reads it, as swarm-bao did in 44009dc51. The new
swarm.nats.otel.telemetryPort (8891) keeps the forwarder off the
collectors' 8888-8890 in the shared host netns, with an assertion.
module-eval-nats-authelia gains four cases: the container's forwarder
and fetch unit on the store host and on a queue-only host, the
responder role's read grant, the swarm collector's swarm-nats receiver
on a host without the queue, and the client registration.
Part of #4499
Part of #5342
140 lines
5.8 KiB
Nix
140 lines
5.8 KiB
Nix
# The swarm's message queue as every hive sees it: where it is reached and
|
||
# what it is registered as, identical on every host. What the host running it
|
||
# decides, and the container itself, are in ./swarm-nats.nix.
|
||
{
|
||
lib,
|
||
config,
|
||
...
|
||
}:
|
||
let
|
||
swarmDomain = config.services.hyperhive.swarm.domain;
|
||
# Total on a null swarm domain, for the reason ./swarm-bao-service.nix gives.
|
||
domainBase = if swarmDomain == null then "invalid" else swarmDomain;
|
||
in
|
||
{
|
||
options.services.hyperhive.swarm.nats = {
|
||
# `enable` is `services.hyperhive.deploy.nats.enable` — see
|
||
# ./deploy.nix. What the queue IS to every hive: its domain, ports
|
||
# and client id.
|
||
|
||
# ⚠️ Deliberately NO `package` option for the NATS server, anywhere —
|
||
# not here and not under `deploy.nats` either, where every other
|
||
# service's build lives. `services.nats` upstream does not expose
|
||
# one; it resolves `pkgs.nats-server` itself, so an option would be
|
||
# ignored or need an overlay to mean anything, and an option that does
|
||
# not control what it names is worse than its absence. Pin the build
|
||
# with `nixpkgs.overlays` if you need to. (`deploy.nats.authPackage`
|
||
# is a different thing: the callout responder, which IS ours.)
|
||
|
||
domain = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "nats.${domainBase}";
|
||
defaultText = lib.literalExpression ''"nats.''${services.hyperhive.swarm.domain}"'';
|
||
description = ''
|
||
Name every client reaches the queue on, as
|
||
`tls://<domain>:<port>`, and the only name its certificate carries.
|
||
A **sibling** of the swarm's other service names, for the reason
|
||
{option}`services.hyperhive.swarm.bao.domain` gives.
|
||
|
||
The host running the queue answers it through `gateway.localNames`.
|
||
Every other hive resolves it through the operator's upstream DNS,
|
||
which is where a multi-host swarm needs a record for it.
|
||
|
||
Not in {option}`services.hyperhive.swarm.serviceDomains`. The gateway
|
||
on the queue's host serves `https://<domain>/metrics` to the swarm
|
||
collector, and in self-signed mode it serves the queue's own leaf
|
||
there rather than the swarm-services one, so that host never waits
|
||
for a services leaf (./swarm-nats.nix).
|
||
'';
|
||
};
|
||
|
||
port = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 4222;
|
||
description = ''
|
||
TCP port the queue listens on. 4222 is upstream's default and
|
||
sits outside hyperhive's claimed ranges (dashboard 7000, forge
|
||
3000, every agent in 8100..8999 via FNV-1a hash).
|
||
|
||
Opened on the bridge interface, for agent containers, and on
|
||
`wg-hive` when this host is on the mesh, for other hives. Never
|
||
host-wide. TLS only: a client that does not speak it is refused.
|
||
|
||
Unlike {option}`monitorPort` and {option}`metricsPort`, the
|
||
address is not what bounds who may use this port: the queue's
|
||
`auth_callout` refuses every client it cannot identify.
|
||
'';
|
||
};
|
||
|
||
monitorPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 8222;
|
||
description = ''
|
||
Port NATS serves its **monitoring** endpoint on, bound to
|
||
loopback.
|
||
|
||
Not a metrics endpoint: the server has no Prometheus format of its
|
||
own. This serves `/varz`, `/connz`, `/routez` as JSON, and the
|
||
exporter below is what translates it — which is why enabling the
|
||
exporter without this produces a process that starts cleanly and
|
||
scrapes nothing.
|
||
|
||
⚠️ Loopback, and the exporter is the only intended reader. The
|
||
endpoint is unauthenticated and `/connz` names every connected
|
||
client, so the address it binds is the whole access control. Do
|
||
not widen it, and do not put it behind a gateway vhost expecting
|
||
that to add one.
|
||
'';
|
||
};
|
||
|
||
metricsPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 7777;
|
||
description = ''
|
||
Port the Prometheus exporter serves NATS's metrics on, bound to
|
||
loopback. The gateway's `= /metrics` location on
|
||
{option}`services.hyperhive.swarm.nats.domain` proxies it, behind
|
||
the swarm collector's authelia check.
|
||
|
||
⚠️ This and {option}`monitorPort` are two more claims on a port
|
||
space every swarm container shares — they run in this container but
|
||
`privateNetwork = false`, so a collision with any other hyperhive
|
||
service is a runtime coin toss over which process gets the port,
|
||
with nothing in any log saying so. Both defaults are upstream's
|
||
own (`nats-server` 8222, `prometheus-nats-exporter` 7777) and
|
||
neither is claimed elsewhere in this repo, checked when they were
|
||
added.
|
||
'';
|
||
};
|
||
|
||
otel.telemetryPort = lib.mkOption {
|
||
type = lib.types.port;
|
||
default = 8891;
|
||
description = ''
|
||
Loopback port the journal forwarder inside the queue's container
|
||
serves its own metrics on (refused and dropped records, exporter
|
||
failures).
|
||
|
||
⚠️ Not 8888, 8889 or 8890: the hive collector, the swarm collector
|
||
and the store's forwarder bind those
|
||
({option}`services.hyperhive.otel.telemetryPort`,
|
||
{option}`services.hyperhive.swarm.otel.telemetryPort`,
|
||
{option}`services.hyperhive.swarm.bao.otel.telemetryPort`), and this
|
||
container shares the host netns with them when co-located. The second
|
||
collector to bind a port dies with `bind: address already in use`.
|
||
'';
|
||
};
|
||
|
||
clientId = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "swarm-nats";
|
||
description = ''
|
||
OAuth2 client id the queue's authentication path identifies
|
||
itself with. The host running authelia registers it, whichever
|
||
host runs the queue, and the queue's host reads its secret out of
|
||
the swarm secret store.
|
||
'';
|
||
};
|
||
|
||
};
|
||
}
|