# The swarm's metrics UI as every hive sees it: the name it is served under, # the port its `/metrics` is re-served on, and the OIDC client it is # registered as, identical on every host. What the host running it decides, # and the container itself, are in ./swarm-grafana.nix. { lib, config, ... }: let cfg = config.services.hyperhive.swarm.grafana; swarmDomain = config.services.hyperhive.swarm.domain; # Total on a null swarm domain for the same reason every sibling module is: # the required-domain assertion in hive-network.nix should be what an # operator sees, not a coercion error from here. domainBase = if swarmDomain == null then "invalid" else swarmDomain; in { # `enable` moved to `services.hyperhive.deploy.grafana.enable` — see # ./deploy.nix. Whether this host runs the swarm's Grafana is a # deployment decision, and `swarm.*` has to be identical on every host. # Here is what the service IS to every hive: the name it is served under, # the port its `/metrics` is re-served on, and the OIDC client it is # registered as. What the host running it decides — which build it runs, # where its datasources point, which plugins are in its store path, and # the socket directory it shares with nginx — is `deploy.grafana`, in # ./swarm-grafana.nix. options.services.hyperhive.swarm.grafana = { machine = lib.mkOption { type = lib.types.str; readOnly = true; default = "swarm-grafana"; description = '' Container name. Read-only: the name appears in host paths and in `machinectl`, so it is a fact other modules may read rather than a knob. ''; }; domain = lib.mkOption { type = lib.types.str; default = "grafana.${domainBase}"; defaultText = lib.literalExpression ''"grafana.''${services.hyperhive.swarm.domain}"''; description = '' Name the gateway serves this on. A sibling of the swarm's other service names, so the swarm-services sub-CA can issue for it — see `hive-tls.nix` for why a service name being a sibling rather than a child decides which CA may sign it. ⚠️ Changing this changes the OAuth redirect URI, which authelia matches exactly. Both sides move together because both derive from this option; an operator who pins one by hand breaks the login. ''; }; metricsPort = lib.mkOption { type = lib.types.port; default = 9095; description = '' Loopback port on which the gateway's nginx re-serves Grafana's `/metrics`, and nothing else, so the swarm's collector can scrape it. ⚠️ **This is nginx's port, not Grafana's.** Grafana still claims none — see {option}`services.hyperhive.deploy.grafana.socketDir` for why that matters. A prometheus scrape target is a `host:port`, and it cannot address a unix socket; rather than undo the socket decision, the one endpoint a scraper needs gets a listener of its own. Bound to loopback and unauthenticated, which is the same posture every other entry in {option}`services.hyperhive.swarm.otel.scrapeTargets` has: those targets are trusted by *proximity* rather than by credential. Deliberately **not** the published `grafana.` vhost, which would put an authorization decision in front of a scrape. The number itself is arbitrary and free today; `state/eval-port-collisions.sh` is what keeps it that way, since a second claim on a port in this shared namespace produces no bind error and nothing in any log. ''; }; oidc = { clientId = lib.mkOption { type = lib.types.str; default = "swarm-grafana"; description = '' The authelia OIDC client id. Names the application rather than the protocol, per the convention in {option}`services.hyperhive.swarm.authelia.oidc.clients`. ''; }; redirectUri = lib.mkOption { type = lib.types.str; readOnly = true; default = "https://${cfg.domain}/login/generic_oauth"; defaultText = lib.literalExpression ''"https://''${services.hyperhive.swarm.grafana.domain}/login/generic_oauth"''; description = '' OAuth callback authelia sends the browser back to, and the URI it matches **exactly**. Read-only, like {option}`services.hyperhive.swarm.grafana.machine` and for the same reason: Grafana derives it from its own `root_url` (`/login/generic_oauth`), so it is a fact other modules may read rather than a knob. The glue that registers this client wherever authelia runs reads it from here instead of restating the format — a second spelling of it is a silently rejected login. ''; }; role = lib.mkOption { type = lib.types.enum [ "Viewer" "Editor" "Admin" ]; default = "Admin"; example = "Editor"; description = '' Grafana org role every SSO user is assigned. `Admin` by default, and that is a considered default rather than a permissive one: the login form is disabled whenever SSO is configured, so this is the *only* way anyone reaches Grafana — a `Viewer` default would produce a swarm nobody can administer. Passing authelia already means being an operator of this swarm; its user store is the small, `swarmctl`-managed one. Lower it if a swarm ever grows read-only operators, which is a one-line change here. ''; }; }; }; }