Watch
0
0
Fork
You've already forked hyperhive
0

gateway: service modules declare their vhost under gateway.vhosts

Ten service modules (forge, matrix chat, authelia, grafana, otel, nats,
swarm-ui, victorialogs, victoriametrics, bao UI) wrote
services.nginx.virtualHosts themselves, each applying the gateway's
listen set, per-name TLS attrs and security headers by hand. They now
declare services.hyperhive.gateway.vhosts.<name> with only their own
locations and options; ./declared-vhosts.nix renders it into the nginx
vhost with listen, TLS and headers added.

An eval assertion refuses a declared vhost that sets listen, addSSL,
onlySSL, enableACME, useACMEHost, sslCertificate or sslCertificateKey,
and one declared while the gateway is off. nats' own leaf is the new
`certificate` field. The rendered nginx vhosts are unchanged.
This commit is contained in:
atlas 2026-10-16 10:20:13 +02:00 • committed by mara
commit ddb583cc22
17 changed files with 652 additions and 513 deletions

View file

@ -22,6 +22,8 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
| `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host | | `<queue domain>/metrics` | the queue exporter's `/metrics` on loopback, behind an authelia `auth_request`; every other path, `/` included, returns `403`, so `/varz` and `/connz` are never reachable | `swarm-nats.nix`, the queue's host |
| `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) | | `grafana.<swarm>`, `metrics.<swarm>`, `logs.<swarm>`, `otel.<swarm>`, `bao.<swarm>` | the matching swarm service | that service's module → [`swarm/services.md`](../swarm/services.md) |
A service module declares its vhost as `services.hyperhive.gateway.vhosts.<name>`: the locations, `forceSSL` and any `extraConfig` it needs. The gateway renders it into `services.nginx.virtualHosts.<name>` and adds the `listen` set, the certificate for that name and the [security headers](#security-headers) (ahead of the vhost's own `extraConfig`), and drops `addSSL` from a vhost that sets `forceSSL`. The eval fails for a vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`, `useACMEHost`, `sslCertificate` or `sslCertificateKey` itself, and for any vhost declared while `gateway.enable` is false. A name with a certificate from outside the gateway's leaves (the queue's) sets `certificate = { cert; key; }` instead.
**The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows: **The hive's own vhosts**: the dashboard's, named `<name>.hives.<swarm>` (`swarm.dashboardDomain`), and one named for the hive domain. They exist only on a host that runs a hive (`deploy.hive-controller.enable`); a host without a hive has none of these rows:
| URL | upstream | when | | URL | upstream | when |

View file

@ -261,9 +261,8 @@ in
# This service's own gateway surface: the vhost that fronts it and # This service's own gateway surface: the vhost that fronts it and
# the name the hive resolver answers for. Declared here rather than # the name the hive resolver answers for. Declared here rather than
# in the gateway so the forge's public face lives with the forge — # in the gateway so the forge's public face lives with the forge —
# the gateway supplies the primitives (`lib.listen`, `lib.tlsFor`, # the gateway adds `listen`, the certificate and the security headers
# `lib.securityHeaders`) and never needs to know this service by # (`gateway.vhosts`) and never needs to know this service by name.
# name.
services.hyperhive.gateway.localNames = [ cfg.domain ]; services.hyperhive.gateway.localNames = [ cfg.domain ];
services.hyperhive.gateway.enable = lib.mkDefault true; services.hyperhive.gateway.enable = lib.mkDefault true;
@ -274,10 +273,8 @@ in
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB # git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
# clones). SSH stays direct on `forge.sshPort`. See # clones). SSH stays direct on `forge.sshPort`. See
# `docs/networking/gateway.md`. # `docs/networking/gateway.md`.
services.nginx.virtualHosts = { services.hyperhive.gateway.vhosts = {
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { "${cfg.domain}" = {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = { locations = {
"/" = { "/" = {
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/"; proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";

View file

@ -0,0 +1,85 @@
# Renders `services.hyperhive.gateway.vhosts` into
# `services.nginx.virtualHosts`: each declared vhost keeps what its module
# wrote and gets the gateway's `listen`, the TLS attributes for its name and
# the security headers.
#
# Its own module rather than part of ./default.nix because the
# "declared while the gateway is off" assertion has to hold when
# `gateway.enable` is false, where ./default.nix defines nothing.
{ lib, config, ... }:
let
cfg = config.services.hyperhive.gateway;
# What the gateway owns on a vhost. A declared vhost naming one of these
# fails the eval.
gatewayOwned = [
"listen"
"addSSL"
"onlySSL"
"enableACME"
"useACMEHost"
"sslCertificate"
"sslCertificateKey"
];
offenders = lib.concatMap (
name: map (attr: "${name}.${attr}") (lib.filter (a: cfg.vhosts.${name} ? ${a}) gatewayOwned)
) (lib.attrNames cfg.vhosts);
render =
name: declared:
let
own = removeAttrs declared [
"_module"
"certificate"
];
base =
if declared.certificate != null then
{
sslCertificate = declared.certificate.cert;
sslCertificateKey = declared.certificate.key;
}
else
cfg.lib.tlsFor name;
# nixos asserts on a vhost declaring both `addSSL` and `forceSSL`.
tls = if own.forceSSL or false then removeAttrs base [ "addSSL" ] else base;
in
tls
// own
// {
listen = cfg.lib.listen;
extraConfig = cfg.lib.securityHeaders + (own.extraConfig or "");
};
in
{
config = lib.mkMerge [
{
assertions = [
{
assertion = cfg.enable || cfg.vhosts == { };
message = ''
services.hyperhive.gateway.vhosts declares ${lib.concatStringsSep ", " (lib.attrNames cfg.vhosts)}
while services.hyperhive.gateway.enable is false, so nothing
would serve them. Enable the gateway, or drop the service that
declares the vhost.
'';
}
{
assertion = offenders == [ ];
message = ''
services.hyperhive.gateway.vhosts sets attributes the gateway owns:
${lib.concatStringsSep ", " offenders}
The gateway sets the listen set and the TLS attributes of every
vhost it fronts. Declare only the vhost's own locations and
options; to serve a certificate the gateway does not pick, set
`certificate` on the vhost.
'';
}
];
}
(lib.mkIf cfg.enable {
services.nginx.virtualHosts = lib.mapAttrs render cfg.vhosts;
})
];
}

View file

@ -6,7 +6,8 @@
# namespace of its own. # namespace of its own.
# Full vhost map + discovery flow + design rationale in # Full vhost map + discovery flow + design rationale in
# `docs/networking/gateway.md`. Layout: ./options.nix (option declarations), # `docs/networking/gateway.md`. Layout: ./options.nix (option declarations),
# ./vhosts.nix (the nginx virtual-host tree), ./error-pages.nix # ./vhosts.nix (the gateway's own nginx virtual hosts), ./declared-vhosts.nix
# (renders the vhosts service modules declare under `gateway.vhosts`), ./error-pages.nix
# (styled static pages), ./dns.nix + ./dnsmasq.nix (the resolver, which # (styled static pages), ./dns.nix + ./dnsmasq.nix (the resolver, which
# has an enable of its own because a host can need hive names to resolve # has an enable of its own because a host can need hive names to resolve
# without serving a vhost). # without serving a vhost).
@ -134,6 +135,7 @@ in
imports = [ imports = [
./options.nix ./options.nix
./dns.nix ./dns.nix
./declared-vhosts.nix
]; ];
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {

View file

@ -136,6 +136,61 @@ in
''; '';
}; };
vhosts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
# Every other attribute is an nginx vhost option, passed through
# to `services.nginx.virtualHosts.<name>` as written.
freeformType = lib.types.attrsOf lib.types.anything;
options.certificate = lib.mkOption {
type = lib.types.nullOr (
lib.types.submodule {
options = {
cert = lib.mkOption {
type = lib.types.str;
description = "Path of the certificate nginx serves for this name.";
};
key = lib.mkOption {
type = lib.types.str;
description = "Path of the private key for `cert`. A path, never a value.";
};
};
}
);
default = null;
description = ''
A certificate pair to serve instead of the one the gateway
picks for this name (`gateway.lib.tlsFor`). For a name whose
certificate is issued outside the gateway's own leaves.
'';
};
}
);
default = { };
internal = true;
description = ''
The nginx vhosts that sit behind this gateway, by server name. A
service module declares its own — locations, `forceSSL`, extra
config — and the gateway renders it into
`services.nginx.virtualHosts.<name>` with the parts it owns:
- `listen`, the gateway's port pair;
- the TLS attributes for that name (`gateway.lib.tlsFor`, or
`certificate` below), with `addSSL` dropped when the vhost sets
`forceSSL`;
- `securityHeaders`, placed ahead of the vhost's own `extraConfig`.
A vhost that sets `listen`, `addSSL`, `onlySSL`, `enableACME`,
`useACMEHost`, `sslCertificate` or `sslCertificateKey` itself fails
the eval: those are the gateway's, and a service that sets them
can bind a port or present a certificate the rest of the gateway
does not.
A vhost declared while the gateway is off fails the eval too: the
name would otherwise be silently unserved.
'';
};
lib = { lib = {
listen = lib.mkOption { listen = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.raw); type = lib.types.listOf (lib.types.attrsOf lib.types.raw);
@ -144,11 +199,10 @@ in
description = '' description = ''
Read-only: the `listen` set every vhost in front of this Read-only: the `listen` set every vhost in front of this
gateway shares (plain http on `port`, TLS on `httpsPort`). gateway shares (plain http on `port`, TLS on `httpsPort`).
Published so a service module can declare its own vhost Applied to every vhost declared under `vhosts`, and read by
without restating the port pair — a vhost that binds a the gateway's own vhosts, so no vhost binds a different
different set is reachable on a port the gateway does not set — one that did would be reachable on a port the gateway
consider its own, which is the kind of drift nobody notices does not consider its own.
until one name behaves differently from the rest.
''; '';
}; };
@ -163,8 +217,8 @@ in
service's: a swarm service's name can sit outside this hive's service's: a swarm service's name can sit outside this hive's
domain, and the hive leaf's role refuses it, so that domain, and the hive leaf's role refuses it, so that
vhost must serve the swarm-services leaf while everything else vhost must serve the swarm-services leaf while everything else
keeps the hive leaf. A service module calls this instead of keeps the hive leaf. The gateway calls this for every vhost it
deciding — deciding is how the vhost and the cert stop fronts — a vhost choosing its own is how the vhost and the cert stop
agreeing. agreeing.
''; '';
}; };

View file

@ -2,18 +2,17 @@
# attrs, and the security headers every vhost in front of this gateway # attrs, and the security headers every vhost in front of this gateway
# needs. # needs.
# #
# Split out of ./vhosts.nix so it has one home and two consumers. Today # Split out of ./vhosts.nix so it has one home and several consumers.
# only ./vhosts.nix reads it; it is published as # ./vhosts.nix and ./declared-vhosts.nix (which renders the vhosts service
# `services.hyperhive.gateway.lib` (see ./options.nix) so a service # modules declare under `gateway.vhosts`) read it through
# module can declare its OWN vhost without the gateway having to know # `services.hyperhive.gateway.lib` (see ./options.nix) rather than calling
# that service by name. `vhosts.nix` reads the published value rather # this file directly — one source, not a copy that agrees by inspection. A
# than calling this file directly — one source, not a copy that agrees # service module reads `verifiedProxyTo`, `errorPages` and `securityHeaders`
# by inspection. # from there for the locations it writes.
# #
# Pure function of the gateway's config + resolved cert paths; returns # Pure function of the gateway's config + resolved cert paths; returns
# an attrset, evaluates no options itself. Everything here is *gateway* # an attrset, evaluates no options itself. Everything here is *gateway*
# knowledge — which port pair to bind, which issuer covers a name — and # knowledge — which port pair to bind, which issuer covers a name.
# stays here even once the service vhosts move out to their own modules.
{ {
lib, lib,
cfg, # services.hyperhive.gateway cfg, # services.hyperhive.gateway

View file

@ -7,10 +7,9 @@
# dashboard's name. # dashboard's name.
# #
# Scope: a swarm service declares its own vhost and dns name in its own # Scope: a swarm service declares its own vhost and dns name in its own
# module, built from the kit this file also consumes # module, as `services.hyperhive.gateway.vhosts.<name>` (rendered by
# (`services.hyperhive.gateway.lib`, ./vhost-lib.nix). That keeps a # ./declared-vhosts.nix). That keeps a service's gateway surface next to
# service's gateway surface next to the service, and keeps this file to # the service, and keeps this file to the surface the hive itself serves.
# the surface the hive itself serves.
# #
# Pure function — called from ./default.nix with the outer-scope config # Pure function — called from ./default.nix with the outer-scope config
# values as arguments; returns `{ virtualHosts }`. # values as arguments; returns `{ virtualHosts }`.

View file

@ -477,10 +477,8 @@ in
# be served at the *server name*, which is the hive domain — it is # be served at the *server name*, which is the hive domain — it is
# the hive answering "where is my homeserver", not the homeserver # the hive answering "where is my homeserver", not the homeserver
# answering for itself. # answering for itself.
services.nginx.virtualHosts = lib.optionalAttrs (cfg.gatewayHost != null) { services.hyperhive.gateway.vhosts = lib.optionalAttrs (cfg.gatewayHost != null) {
"${cfg.gatewayHost}" = (gatewayCfg.lib.tlsFor cfg.gatewayHost) // { "${cfg.gatewayHost}" = {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = { locations = {
"/_matrix/" = { "/_matrix/" = {
# No URI after the socket, so tuwunel gets the request URI as the # No URI after the socket, so tuwunel gets the request URI as the

View file

@ -800,9 +800,7 @@ in
# #
# ⚠️ And deliberately NO `auth_request` here: it would put the login # ⚠️ And deliberately NO `auth_request` here: it would put the login
# page behind the login it serves. # page behind the login it serves.
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { services.hyperhive.gateway.vhosts."${cfg.domain}" = {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations."/" = { locations."/" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}/"; proxyPass = "http://127.0.0.1:${toString cfg.port}/";
proxyWebsockets = true; proxyWebsockets = true;

View file

@ -2716,14 +2716,9 @@ in
# netns), so no port opens anywhere. `auth_request` only proves a # netns), so no port opens anywhere. `auth_request` only proves a
# session; restricting it to `admins` is ./swarm-authelia.nix's rule # session; restricting it to `admins` is ./swarm-authelia.nix's rule
# for this name. `forceSSL` because authelia answers a plain-http # for this name. `forceSSL` because authelia answers a plain-http
# subrequest with 400, and `removeAttrs` because nixos refuses # subrequest with 400.
# `addSSL` beside it. services.hyperhive.gateway.vhosts.${cfg.ui.domain} = {
services.nginx.virtualHosts.${cfg.ui.domain} =
(builtins.removeAttrs (hyperhiveCfg.gateway.lib.tlsFor cfg.ui.domain) [ "addSSL" ])
// {
forceSSL = true; forceSSL = true;
listen = hyperhiveCfg.gateway.lib.listen;
extraConfig = hyperhiveCfg.gateway.lib.securityHeaders;
locations = { locations = {
"/" = { "/" = {
proxyPass = "http://127.0.0.1:${toString baoDeploy.uiProxyPort}"; proxyPass = "http://127.0.0.1:${toString baoDeploy.uiProxyPort}";

View file

@ -558,9 +558,7 @@ in
# Websockets: Grafana Live streams panel updates over one, and without # Websockets: Grafana Live streams panel updates over one, and without
# the upgrade headers dashboards load and then never refresh — which # the upgrade headers dashboards load and then never refresh — which
# reads as stale data rather than as a proxy fault. # reads as stale data rather than as a proxy fault.
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { services.hyperhive.gateway.vhosts."${cfg.domain}" = {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
# Grafana serves its Prometheus exposition to anyone, signed in or not # Grafana serves its Prometheus exposition to anyone, signed in or not
# (`metrics.enabled` below, no `basic_auth_*`), so this location's auth # (`metrics.enabled` below, no `basic_auth_*`), so this location's auth
# is the only gate on it. `=` is enough: nginx matches the decoded, # is the only gate on it. `=` is enough: nginx matches the decoded,

View file

@ -465,28 +465,19 @@ in
# nothing else on this name: the monitoring endpoint (`/varz`, `/connz`) # nothing else on this name: the monitoring endpoint (`/varz`, `/connz`)
# is never proxied, and every other path is 403. # is never proxied, and every other path is 403.
# #
# ⚠️ Not `gateway.lib.tlsFor`, and the name is not in # ⚠️ In self-signed mode this vhost serves the queue's own leaf
# `swarm.serviceDomains`. In self-signed mode this vhost serves the # (`certificate`; issued for exactly this name, `server_flag` set), not
# queue's own leaf (issued for exactly this name, `server_flag` set), so # the gateway's pick for the name, and the name is not in
# a host whose only swarm service is the queue never fronts a services # `swarm.serviceDomains`.
# name and its gateway does not wait for the swarm-services leaf.
# #
# `forceSSL` for the reason ./swarm-victorialogs.nix gives: authelia # `forceSSL` for the reason ./swarm-victorialogs.nix gives: authelia
# answers a plain-http auth subrequest with 400. # answers a plain-http auth subrequest with 400.
services.nginx.virtualHosts."${cfg.domain}" = services.hyperhive.gateway.vhosts."${cfg.domain}" = {
( certificate = lib.mkIf gatewayServesQueueLeaf {
if gatewayServesQueueLeaf then cert = "${gatewayTlsDir}/cert.pem";
{ key = "${gatewayTlsDir}/key.pem";
sslCertificate = "${gatewayTlsDir}/cert.pem"; };
sslCertificateKey = "${gatewayTlsDir}/key.pem";
}
else
builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ]
)
// {
forceSSL = true; forceSSL = true;
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = { locations = {
"/".return = "403"; "/".return = "403";

View file

@ -358,9 +358,7 @@ in
# every sibling swarm service: TLS terminates here, then plain http to # every sibling swarm service: TLS terminates here, then plain http to
# the co-located container over loopback (shared netns, like the store # the co-located container over loopback (shared netns, like the store
# this collector writes to). # this collector writes to).
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // { services.hyperhive.gateway.vhosts."${cfg.domain}" = {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = locations =
# One name for the whole collector, and the hive is a path under # One name for the whole collector, and the hive is a path under
# it. The alternative — a vhost per hive — needs a certificate, # it. The alternative — a vhost per hive — needs a certificate,

View file

@ -179,13 +179,9 @@ in
# on. Serving forge or matrix over http is merely insecure rather # on. Serving forge or matrix over http is merely insecure rather
# than broken, so they keep `addSSL` and the asymmetry stays local # than broken, so they keep `addSSL` and the asymmetry stays local
# to the vhost whose correctness depends on the scheme. # to the vhost whose correctness depends on the scheme.
# `removeAttrs` because nixos asserts on a vhost declaring both. # The gateway drops `addSSL` from a vhost that sets `forceSSL`.
services.nginx.virtualHosts."${apex}" = services.hyperhive.gateway.vhosts."${apex}" = {
(builtins.removeAttrs (gatewayCfg.lib.tlsFor apex) [ "addSSL" ])
// {
forceSSL = true; forceSSL = true;
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = { locations = {
"/" = { "/" = {
root = "${deployCfg.swarm-controller.ui.package}"; root = "${deployCfg.swarm-controller.ui.package}";

View file

@ -313,17 +313,13 @@ in
# Authenticated front door onto the loopback-only store — see the # Authenticated front door onto the loopback-only store — see the
# file-top comment for why this is safe to add without touching the # file-top comment for why this is safe to add without touching the
# store's own (still unauthenticated, still loopback) listener at all. # store's own (still unauthenticated, still loopback) listener at all.
# `removeAttrs`/`forceSSL`: same asymmetry `swarm-ui.nix` documents — # `forceSSL`: same asymmetry `swarm-ui.nix` documents —
# authelia answers a plain-http auth subrequest with 400, which # authelia answers a plain-http auth subrequest with 400, which
# `auth_request` cannot read as anything but a broken check, so this # `auth_request` cannot read as anything but a broken check, so this
# vhost needs `forceSSL` rather than the `addSSL` every unauthenticated # vhost needs `forceSSL` rather than the `addSSL` every unauthenticated
# sibling vhost uses. # sibling vhost uses.
services.nginx.virtualHosts."${cfg.domain}" = services.hyperhive.gateway.vhosts."${cfg.domain}" = {
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
// {
forceSSL = true; forceSSL = true;
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = { locations = {
"/" = { "/" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}/"; proxyPass = "http://127.0.0.1:${toString cfg.port}/";

View file

@ -252,15 +252,11 @@ in
} }
''; '';
# `removeAttrs`/`forceSSL`: authelia answers a plain-http auth subrequest # `forceSSL`: authelia answers a plain-http auth subrequest
# with 400, which `auth_request` cannot read as anything but a broken # with 400, which `auth_request` cannot read as anything but a broken
# check. Same as ./swarm-victorialogs.nix's vhost. # check. Same as ./swarm-victorialogs.nix's vhost.
services.nginx.virtualHosts."${cfg.domain}" = services.hyperhive.gateway.vhosts."${cfg.domain}" = {
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
// {
forceSSL = true; forceSSL = true;
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;
locations = locations =
readLocations readLocations
// uiLocations // uiLocations

View file

@ -88,6 +88,8 @@ let
deploy.grafana.plugins = [ ]; deploy.grafana.plugins = [ ];
deploy.grafana.package = pkgs.emptyDirectory; deploy.grafana.package = pkgs.emptyDirectory;
swarm.authelia.url = "https://auth.example.invalid"; swarm.authelia.url = "https://auth.example.invalid";
# Declares a vhost that binds its own `listen`, which the gateway refuses.
gateway.vhosts."own-listen.t.local".listen = [ ];
}; };
# Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its # Did ./host-modules/swarm-grafana.nix refuse this host, and for which of its
@ -479,6 +481,39 @@ let
&& builtins.length selectors == 12 && builtins.length selectors == 12
&& lib.all (e: lib.hasInfix "\"service.name\"=\"${stamped.value}\"" e) selectors; && lib.all (e: lib.hasInfix "\"service.name\"=\"${stamped.value}\"" e) selectors;
} }
{
# A module declares its vhost under `gateway.vhosts` and the gateway adds
# what it owns: the listen set, the cert for the name and the security
# headers ahead of the module's own `extraConfig`.
name = "grafana's vhost is rendered with the gateway's listen set, TLS and security headers";
ok =
let
gw = grafanaOldPath.services.hyperhive.gateway;
v =
grafanaOldPath.services.nginx.virtualHosts.${grafanaOldPath.services.hyperhive.swarm.grafana.domain};
# nginx's `listen` entries come back with defaults filled in
# (`extraParameters`, `proxyProtocol`), so compare the fields set.
listenOf = map (l: {
inherit (l) addr port;
ssl = l.ssl or false;
});
in
listenOf v.listen == listenOf gw.lib.listen
&& lib.hasPrefix gw.lib.securityHeaders v.extraConfig
&& v.addSSL
&& !(v ? certificate);
}
{
# A vhost that sets `listen` itself would bind a port the gateway does
# not consider its own, so the eval refuses it and names the vhost.
name = "a gateway vhost that sets its own listen is refused";
ok = lib.any (
a:
!a.assertion
&& lib.hasInfix "sets attributes the gateway owns" a.message
&& lib.hasInfix "own-listen.t.local.listen" a.message
) grafanaNoIdentity.assertions;
}
]; ];
in in
runGroup "grafana" cases runGroup "grafana" cases