forge: always behind the gateway; drop behindGateway
The forge always sits behind the gateway, so `deploy.forgejo.behindGateway` (and its `swarm.forge.behindGateway` rename alias) is removed and its true-branch behaviour is now unconditional within `deploy.forgejo.enable`: https ROOT_URL on the gateway's httpsPort, the forge vhost and local DNS name, the swarm-ui quick link, the published metrics scrape target, forgejo metrics, the authelia `/metrics` rule, and `publicUrl` defaulting to `https://<forge.domain>`. Removed with it: the direct-port `http://<domain>:<httpPort>/` ROOT_URL branch, the hive-ci assertion that the option is true, the core-toggle cases that only exercised the false branch (the services-leaf case reads `bare`, which never enabled the forge either). `hivectl open forge` now points at `swarm.forge.publicUrl`, which can still be set to null. Refs #4885
This commit is contained in:
parent
a40c0026cf
commit
ac592a5d23
13 changed files with 89 additions and 213 deletions
|
|
@ -10,7 +10,7 @@ This host's nginx fronts the hyperhive web surfaces running on it — next to hi
|
||||||
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
|
| `<hive>/agent/<name>/` | `_` | per-agent harness (UDS or TCP) | `agents.conf` (runtime-generated) |
|
||||||
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
|
| `<hive>/.well-known/matrix/{client,server}` | `_` | inline JSON (no upstream) | `matrix.enable && domain != null` |
|
||||||
| `<hive>/matrix/` (deprecated) | `_` | 301 → `chat.<swarm>/` | `matrix.gui.enable` |
|
| `<hive>/matrix/` (deprecated) | `_` | 301 → `chat.<swarm>/` | `matrix.gui.enable` |
|
||||||
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo.behindGateway` |
|
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo` |
|
||||||
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
|
| `chat.<swarm>/_matrix/*` | `chat.<swarm>` | tuwunel (`8008`) | `matrix.gatewayHost != null` |
|
||||||
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
|
| `chat.<swarm>/` | `chat.<swarm>` | fluffychat-web static | `matrix.gui.enable` |
|
||||||
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
| `chat.<swarm>/config.json` | `chat.<swarm>` | inline JSON (FluffyChat boot config) | `matrix.gui.enable && domain != null` |
|
||||||
|
|
@ -66,7 +66,7 @@ Each location carries a duplicated `auth_basic` block (separate locations don't
|
||||||
`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`:
|
`services.hyperhive.gateway.localHostsEntry = true` adds entries to the host's `/etc/hosts`:
|
||||||
|
|
||||||
- `<hive-domain>` → `127.0.0.1`
|
- `<hive-domain>` → `127.0.0.1`
|
||||||
- `forge.<swarm>` → `127.0.0.1` (when deploy.forgejo.behindGateway)
|
- `forge.<swarm>` → `127.0.0.1` (when deploy.forgejo)
|
||||||
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
|
- `chat.<swarm>` → `127.0.0.1` (when matrix.gatewayHost set)
|
||||||
- `auth.<swarm>` → `127.0.0.1` (when deploy.authelia)
|
- `auth.<swarm>` → `127.0.0.1` (when deploy.authelia)
|
||||||
|
|
||||||
|
|
@ -165,9 +165,8 @@ true; the `false` branch stays as a defensive fallback for the
|
||||||
env being unset. Three render sites
|
env being unset. Three render sites
|
||||||
flip together: the primary agent-name link, the favicon fetch
|
flip together: the primary agent-name link, the favicon fetch
|
||||||
(`<url>/icon`), and the nav-strip `container`-kind links from
|
(`<url>/icon`), and the nav-strip `container`-kind links from
|
||||||
`DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links still
|
`DashboardState.links` (`GET /api/dashboard-state`). `forge`-kind nav-strip links
|
||||||
resolve against `http://<host>:3000` (separate sub-domain transition
|
resolve against `forge_public_url`, and the dashboard hides them when it's unset; `external`-kind links are
|
||||||
tracked by `deploy.forgejo.behindGateway`); `external`-kind links are
|
|
||||||
already absolute. See `docs/web-ui/dashboard.md::Container row` for the
|
already absolute. See `docs/web-ui/dashboard.md::Container row` for the
|
||||||
frontend-side derivation.
|
frontend-side derivation.
|
||||||
|
|
||||||
|
|
@ -385,9 +384,8 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
|
||||||
- External git clients that push/pull via SSH directly to the host.
|
- External git clients that push/pull via SSH directly to the host.
|
||||||
<!-- vale write-good.Passive = YES -->
|
<!-- vale write-good.Passive = YES -->
|
||||||
|
|
||||||
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does
|
Forgejo's gateway vhost doesn't need `openFirewall` — the gateway's own
|
||||||
not need `openFirewall` — the gateway's own `openFirewall` option covers
|
`openFirewall` option covers that path.
|
||||||
that path.
|
|
||||||
|
|
||||||
### `rootUrl` override
|
### `rootUrl` override
|
||||||
|
|
||||||
|
|
@ -396,17 +394,9 @@ services.hyperhive.swarm.forge.rootUrl = "https://forge.example.com/";
|
||||||
```
|
```
|
||||||
|
|
||||||
`rootUrl` (default **null**) overrides the Forgejo `ROOT_URL` that's
|
`rootUrl` (default **null**) overrides the Forgejo `ROOT_URL` that's
|
||||||
autoderived from `forge.domain` + gateway state. The autoderivation
|
autoderived as `https://<forge.domain>/`, with `:<gateway.httpsPort>`
|
||||||
covers most cases:
|
appended when that port isn't 443. The gateway always terminates TLS, so
|
||||||
|
the forge is always advertised over `https://`. Set `rootUrl` explicitly when
|
||||||
| Shape | Autoderived `ROOT_URL` |
|
|
||||||
|---|---|
|
|
||||||
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
|
|
||||||
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
|
||||||
|
|
||||||
The gateway always terminates TLS, so the `behindGateway = true` case is
|
|
||||||
always advertised over `https://`; only the direct (`behindGateway =
|
|
||||||
false`) shape stays `http://`. Set `rootUrl` explicitly when
|
|
||||||
`forge.domain` resolves differently from the public URL, or for a
|
`forge.domain` resolves differently from the public URL, or for a
|
||||||
genuinely bespoke shape (for example an external reverse proxy on a different
|
genuinely bespoke shape (for example an external reverse proxy on a different
|
||||||
host/path). Must end with `/` (Forgejo requirement; an assertion
|
host/path). Must end with `/` (Forgejo requirement; an assertion
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ build can't hold the runner's single slot indefinitely).
|
||||||
|
|
||||||
## Container design
|
## Container design
|
||||||
|
|
||||||
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `deploy.forgejo.behindGateway = true`.
|
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI.
|
||||||
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
|
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
|
||||||
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
|
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
|
||||||
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.
|
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,6 @@ note, not an error.
|
||||||
|
|
||||||
A surface has no URL when it isn't browser-reachable: `home` needs
|
A surface has no URL when it isn't browser-reachable: `home` needs
|
||||||
`services.hyperhive.domain`; `forge` needs
|
`services.hyperhive.domain`; `forge` needs
|
||||||
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
|
`services.hyperhive.swarm.forge.publicUrl` (set by default); `matrix` needs
|
||||||
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command
|
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command
|
||||||
exits with a hint naming the option to set.
|
exits with a hint naming the option to set.
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,7 @@ pub(super) struct StateSnapshot {
|
||||||
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
|
/// `"https://forge.pr1ma.darkest.space"`). Sourced from the
|
||||||
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
|
/// `HIVE_FORGE_PUBLIC_URL` env var, which the c0re NixOS module
|
||||||
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
|
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
|
||||||
/// the gateway vhost URL when `deploy.forgejo.behindGateway = true`,
|
/// the gateway vhost URL). `None` when absent — the frontend **hides**
|
||||||
/// `null` otherwise). `None` when absent — the frontend **hides**
|
|
||||||
/// forge links rather than guessing `http://<hostname>:3000`,
|
/// forge links rather than guessing `http://<hostname>:3000`,
|
||||||
/// which is only right by accident on deployments that aren't
|
/// which is only right by accident on deployments that aren't
|
||||||
/// plain localhost.
|
/// plain localhost.
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,7 @@ impl LifecycleScope {
|
||||||
/// This hive's canonical domain plus the browser-facing URLs for its
|
/// This hive's canonical domain plus the browser-facing URLs for its
|
||||||
/// web surfaces — the `Urls` request result. Every field is `None` when
|
/// web surfaces — the `Urls` request result. Every field is `None` when
|
||||||
/// the corresponding surface can't be reached from a browser (domain
|
/// the corresponding surface can't be reached from a browser (domain
|
||||||
/// unset, forge not behind the gateway, matrix GUI disabled), so the CLI
|
/// unset, forge `publicUrl` null, matrix GUI disabled), so the CLI
|
||||||
/// can give a precise hint instead of opening a dead link.
|
/// can give a precise hint instead of opening a dead link.
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct HiveUrls {
|
pub struct HiveUrls {
|
||||||
|
|
@ -461,8 +461,8 @@ pub struct HiveUrls {
|
||||||
/// Operator dashboard root (`https://<domain>/`).
|
/// Operator dashboard root (`https://<domain>/`).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — only the
|
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — `None` when
|
||||||
/// behind-gateway public URL; `None` on direct-port forge deploys.
|
/// `swarm.forge.publicUrl` is `null`.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub forge: Option<String>,
|
pub forge: Option<String>,
|
||||||
/// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI
|
/// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
|
||||||
),
|
),
|
||||||
OpenTarget::Forge => (
|
OpenTarget::Forge => (
|
||||||
urls.forge,
|
urls.forge,
|
||||||
"the public forge URL needs `services.hyperhive.deploy.forgejo.behindGateway = true`",
|
"the public forge URL needs `services.hyperhive.swarm.forge.publicUrl` to be set",
|
||||||
),
|
),
|
||||||
OpenTarget::Matrix => (
|
OpenTarget::Matrix => (
|
||||||
urls.matrix,
|
urls.matrix,
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ in
|
||||||
# The rest of the forge split. What stays under `swarm.forge` is what the
|
# The rest of the forge split. What stays under `swarm.forge` is what the
|
||||||
# forge IS from any hive's point of view — the names and ports it answers
|
# forge IS from any hive's point of view — the names and ports it answers
|
||||||
# on, the URLs it advertises, the client id it is registered under; these
|
# on, the URLs it advertises, the client id it is registered under; these
|
||||||
# five are what the host running it decides. Its package moved as well,
|
# four are what the host running it decides. Its package moved as well,
|
||||||
# further down with the other `*.package` moves. `sso` splits
|
# further down with the other `*.package` moves. `sso` splits
|
||||||
# because its two halves are different facts: the client id must match the
|
# because its two halves are different facts: the client id must match the
|
||||||
# entry in authelia's register, the secret is a path on this machine.
|
# entry in authelia's register, the secret is a path on this machine.
|
||||||
|
|
@ -218,10 +218,6 @@ in
|
||||||
# option of a list-of-submodule type, so the rename carries its whole
|
# option of a list-of-submodule type, so the rename carries its whole
|
||||||
# value. The `ci` block above needs five because it is a plain attrset of
|
# value. The `ci` block above needs five because it is a plain attrset of
|
||||||
# separate options, which is the case with no parent path to rename.
|
# separate options, which is the case with no parent path to rename.
|
||||||
(lib.mkRenamedOptionModule
|
|
||||||
[ "services" "hyperhive" "swarm" "forge" "behindGateway" ]
|
|
||||||
[ "services" "hyperhive" "deploy" "forgejo" "behindGateway" ]
|
|
||||||
)
|
|
||||||
(lib.mkRenamedOptionModule
|
(lib.mkRenamedOptionModule
|
||||||
[ "services" "hyperhive" "swarm" "forge" "openFirewall" ]
|
[ "services" "hyperhive" "swarm" "forge" "openFirewall" ]
|
||||||
[ "services" "hyperhive" "deploy" "forgejo" "openFirewall" ]
|
[ "services" "hyperhive" "deploy" "forgejo" "openFirewall" ]
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,8 @@ in
|
||||||
# breaks the moment the operator's browser hostname isn't the forge
|
# breaks the moment the operator's browser hostname isn't the forge
|
||||||
# host, e.g. through the gateway or a reverse proxy). Sourced from
|
# host, e.g. through the gateway or a reverse proxy). Sourced from
|
||||||
# `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the
|
# `services.hyperhive.swarm.forge.publicUrl`, which itself defaults to the
|
||||||
# gateway vhost URL when `deploy.forgejo.behindGateway = true` and `null`
|
# gateway vhost URL — see that option's doc for the "hide, don't guess"
|
||||||
# otherwise — see that option's doc for the "hide, don't guess" rationale.
|
# rationale.
|
||||||
# Absent here whenever `publicUrl` is `null`; the dashboard hides
|
# Absent here whenever `publicUrl` is `null`; the dashboard hides
|
||||||
# forge links rather than emitting one it can't justify.
|
# forge links rather than emitting one it can't justify.
|
||||||
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
|
HIVE_FORGE_PUBLIC_URL = config.services.hyperhive.swarm.forge.publicUrl;
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,7 @@ let
|
||||||
# Private network namespace, attached to the hive bridge so the
|
# Private network namespace, attached to the hive bridge so the
|
||||||
# runner reaches the forge via the gateway — and cannot reach
|
# runner reaches the forge via the gateway — and cannot reach
|
||||||
# host-loopback (127.0.0.1:7000 dashboard, raw forge port, etc.).
|
# host-loopback (127.0.0.1:7000 dashboard, raw forge port, etc.).
|
||||||
# Requires `deploy.forgejo.behindGateway = true` (asserted in the
|
# See docs/networking/network.md.
|
||||||
# config block below). See docs/networking/network.md.
|
|
||||||
privateNetwork = true;
|
privateNetwork = true;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -161,22 +160,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# `deploy.forgejo.behindGateway = true` (the default) is required because
|
|
||||||
# the CI container uses private networking and reaches the forge through
|
|
||||||
# the gateway vhost. Without the gateway vhost there is no HTTP
|
|
||||||
# listener for `forgeCfg.domain` on the bridge that the runner can
|
|
||||||
# connect to.
|
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
|
||||||
assertion = forgeDeployCfg.behindGateway;
|
|
||||||
message = ''
|
|
||||||
services.hyperhive.deploy.forgejo.ci.enable requires
|
|
||||||
services.hyperhive.deploy.forgejo.behindGateway = true.
|
|
||||||
The CI container runs with a private network namespace and
|
|
||||||
reaches the forge through the gateway vhost on the bridge IP.
|
|
||||||
Set behindGateway = true (it is the default).
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
# The runner reaches the forge through THIS host's gateway, and
|
# The runner reaches the forge through THIS host's gateway, and
|
||||||
# hive-c0re registers it through the local forge container; neither
|
# hive-c0re registers it through the local forge container; neither
|
||||||
|
|
|
||||||
|
|
@ -58,26 +58,20 @@ let
|
||||||
|
|
||||||
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
|
||||||
|
|
||||||
# ROOT_URL forgejo advertises in clone links + outbound URLs. When
|
# ROOT_URL forgejo advertises in clone links + outbound URLs.
|
||||||
# served behind the gateway, `cfg.domain` doubles as both the
|
# `cfg.domain` doubles as both the forgejo `DOMAIN` setting AND the
|
||||||
# forgejo `DOMAIN` setting AND the gateway vhost server-name, so
|
# gateway vhost server-name, so ROOT_URL just uses it directly. The
|
||||||
# ROOT_URL just uses it directly. The gateway always terminates TLS
|
# gateway always terminates TLS (self-signed is the implicit floor when
|
||||||
# (self-signed is the implicit floor when neither `tls.certDir` nor
|
# neither `tls.certDir` nor ACME is configured), so the forge is always
|
||||||
# ACME is configured), so behind the gateway the forge is always
|
# advertised over `https` on `httpsPort` — the canonical 443 elides the
|
||||||
# advertised over `https` on `httpsPort` — the canonical 443 elides
|
# port suffix. Operators can still override via `cfg.rootUrl` for
|
||||||
# the port suffix. When direct (`behindGateway = false`), keep the
|
# bespoke shapes. Both bindings are duplicated in ./service.nix, which
|
||||||
# host:httpPort shape so direct browser access still produces correct
|
# builds `sso.redirectUri` from them; keep the two equal.
|
||||||
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
|
||||||
# shapes. Both bindings are duplicated in ./service.nix, which builds
|
|
||||||
# `sso.redirectUri` from them; keep the two equal.
|
|
||||||
defaultRootUrl =
|
defaultRootUrl =
|
||||||
if deployCfg.forgejo.behindGateway then
|
|
||||||
let
|
let
|
||||||
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
||||||
in
|
in
|
||||||
"https://${cfg.domain}${portSuffix}/"
|
"https://${cfg.domain}${portSuffix}/";
|
||||||
else
|
|
||||||
"http://${cfg.domain}:${toString cfg.httpPort}/";
|
|
||||||
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
||||||
|
|
||||||
# When CI is enabled, the runner needs `actions/checkout` resolvable
|
# When CI is enabled, the runner needs `actions/checkout` resolvable
|
||||||
|
|
@ -136,35 +130,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
behindGateway = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
Serve forgejo through the hive-gateway nginx as a sub-domain
|
|
||||||
vhost (`server_name = cfg.domain`) instead of directly on
|
|
||||||
`httpPort` (sub-domain routing — see `docs/networking/gateway.md`).
|
|
||||||
|
|
||||||
When `true`:
|
|
||||||
- The gateway adds a `server { server_name = ''${cfg.domain}; }`
|
|
||||||
block that proxies all `/` → `http://127.0.0.1:''${httpPort}/`.
|
|
||||||
- Forgejo's `ROOT_URL` flips to `http(s)://''${cfg.domain}/`
|
|
||||||
(sub-domain root, no port suffix when gateway is on 80).
|
|
||||||
- `gateway.localHostsEntry = true` extends `/etc/hosts` to
|
|
||||||
include `cfg.domain → 127.0.0.1` for local dev.
|
|
||||||
|
|
||||||
Defaults to `true` (the gateway always runs alongside
|
|
||||||
hyperhive, so forge auto-routes through it). Set `false`
|
|
||||||
explicitly to keep forge on the direct port even though the
|
|
||||||
gateway is running (e.g. an external git client that doesn't
|
|
||||||
traverse the gateway).
|
|
||||||
|
|
||||||
Sub-domain routing is the preferred shape for forge + matrix
|
|
||||||
(both are external standard apps with sub-domain-native config
|
|
||||||
defaults). Per-agent UIs stay on sub-path (`/agent/<name>/`)
|
|
||||||
because they're hyperhive-internal + already base-path-aware.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
openFirewall = lib.mkOption {
|
openFirewall = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
|
@ -281,41 +246,30 @@ in
|
||||||
# the gateway supplies the primitives (`lib.listen`, `lib.tlsFor`,
|
# the gateway supplies the primitives (`lib.listen`, `lib.tlsFor`,
|
||||||
# `lib.securityHeaders`) and never needs to know this service by
|
# `lib.securityHeaders`) and never needs to know this service by
|
||||||
# name.
|
# name.
|
||||||
#
|
services.hyperhive.gateway.localNames = [ cfg.domain ];
|
||||||
# Both halves are gated on `behindGateway`: with it off the operator
|
services.hyperhive.gateway.enable = lib.mkDefault true;
|
||||||
# fronts forgejo themselves, so this hive must neither claim the
|
|
||||||
# vhost nor answer DNS for it.
|
|
||||||
services.hyperhive.gateway.localNames = lib.optional deployCfg.forgejo.behindGateway cfg.domain;
|
|
||||||
services.hyperhive.gateway.enable = lib.mkIf deployCfg.forgejo.behindGateway (lib.mkDefault true);
|
|
||||||
|
|
||||||
# Not conditional on `behindGateway`: the forge container resolves the
|
# The forge container resolves the rest of the hive through dnsmasq.
|
||||||
# rest of the hive through dnsmasq whoever fronts it.
|
|
||||||
services.hyperhive.gateway.dns.enable = lib.mkDefault true;
|
services.hyperhive.gateway.dns.enable = lib.mkDefault true;
|
||||||
|
|
||||||
# This swarm-ui quick-links entry, same `behindGateway` guard as the
|
# This swarm-ui quick-links entry. See
|
||||||
# vhost/DNS name above — with it off, this host doesn't actually
|
|
||||||
# serve `cfg.domain`, so linking to it would be dead. See
|
|
||||||
# `services.hyperhive.swarm.controller.links`'s description.
|
# `services.hyperhive.swarm.controller.links`'s description.
|
||||||
services.hyperhive.swarm.controller.links = lib.optional deployCfg.forgejo.behindGateway {
|
services.hyperhive.swarm.controller.links = [
|
||||||
|
{
|
||||||
label = "Forge";
|
label = "Forge";
|
||||||
icon = "⚒";
|
icon = "⚒";
|
||||||
url = "https://${cfg.domain}/";
|
url = "https://${cfg.domain}/";
|
||||||
};
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# The metrics endpoint, declared once: the collector both scrapes this
|
# The metrics endpoint, declared once: the collector both scrapes this
|
||||||
# URL and derives from it the audience its token is minted for. Same
|
# URL and derives from it the audience its token is minted for.
|
||||||
# `behindGateway` guard, and for a stronger reason than the two above:
|
|
||||||
# with it off there is no `= /metrics` location and no `auth_request`
|
|
||||||
# in front of it, so the URL this names does not exist to be scraped
|
|
||||||
# or authorised.
|
|
||||||
#
|
#
|
||||||
# ⚠️ Written as the exact URL a collector requests, because that is
|
# ⚠️ Written as the exact URL a collector requests, because that is
|
||||||
# what authelia compares against — this string agreeing with the
|
# what authelia compares against — this string agreeing with the
|
||||||
# `location` block above it is the whole mechanism. A near miss is a
|
# `location` block above it is the whole mechanism. A near miss is a
|
||||||
# correctly minted token refused at the target.
|
# correctly minted token refused at the target.
|
||||||
services.hyperhive.swarm.otel.publishedScrapeTargets =
|
services.hyperhive.swarm.otel.publishedScrapeTargets = {
|
||||||
lib.optionalAttrs deployCfg.forgejo.behindGateway
|
|
||||||
{
|
|
||||||
forgejo = "https://${cfg.domain}/metrics";
|
forgejo = "https://${cfg.domain}/metrics";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -323,7 +277,7 @@ 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 = lib.optionalAttrs deployCfg.forgejo.behindGateway {
|
services.nginx.virtualHosts = {
|
||||||
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
||||||
listen = gatewayCfg.lib.listen;
|
listen = gatewayCfg.lib.listen;
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||||
|
|
@ -600,21 +554,17 @@ in
|
||||||
DEFAULT_PRIVATE = "private";
|
DEFAULT_PRIVATE = "private";
|
||||||
};
|
};
|
||||||
# Not an option: a swarm-integrated, auto-deployed forge
|
# Not an option: a swarm-integrated, auto-deployed forge
|
||||||
# always has metrics. Tied to `behindGateway` because that
|
# always has metrics, behind the protected `= /metrics`
|
||||||
# IS the swarm-integrated shape — it is the condition under
|
# location on its gateway vhost.
|
||||||
# which the protected `= /metrics` location below exists.
|
|
||||||
# Serving the endpoint without that location would put it on
|
|
||||||
# a listener `openFirewall` can expose, with nothing in
|
|
||||||
# front of it.
|
|
||||||
#
|
#
|
||||||
# No `TOKEN` here on purpose. Forgejo can guard this itself
|
# No `TOKEN` here on purpose. Forgejo can guard this itself
|
||||||
# with a static bearer, but the swarm authenticates the
|
# with a static bearer, but the swarm authenticates the
|
||||||
# scraper at the gateway, so a second credential system per
|
# scraper at the gateway, so a second credential system per
|
||||||
# service would buy nothing and would be the one that stops
|
# service would buy nothing and would be the one that stops
|
||||||
# getting rotated.
|
# getting rotated.
|
||||||
metrics.ENABLED = deployCfg.forgejo.behindGateway;
|
metrics.ENABLED = true;
|
||||||
# The two per-dimension breakdowns, on the same condition as
|
# The two per-dimension breakdowns, alongside the endpoint
|
||||||
# the endpoint itself: `gitea_issues_by_label{label=…}` and
|
# itself: `gitea_issues_by_label{label=…}` and
|
||||||
# `gitea_issues_by_repository{repository=…}`. Off by default
|
# `gitea_issues_by_repository{repository=…}`. Off by default
|
||||||
# upstream because they are the only metrics here whose series
|
# upstream because they are the only metrics here whose series
|
||||||
# count grows with the CONTENT of the forge rather than with
|
# count grows with the CONTENT of the forge rather than with
|
||||||
|
|
@ -631,8 +581,8 @@ in
|
||||||
# forge that grew to thousands of repos would want this
|
# forge that grew to thousands of repos would want this
|
||||||
# revisited — that is a real trigger, unlike a time-based one:
|
# revisited — that is a real trigger, unlike a time-based one:
|
||||||
# `count(gitea_issues_by_repository)` answers it directly.
|
# `count(gitea_issues_by_repository)` answers it directly.
|
||||||
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway;
|
metrics.ENABLED_ISSUE_BY_LABEL = true;
|
||||||
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.behindGateway;
|
metrics.ENABLED_ISSUE_BY_REPOSITORY = true;
|
||||||
# Repo migrations / pull-mirrors fetch from the source
|
# Repo migrations / pull-mirrors fetch from the source
|
||||||
# URL *inside* Forgejo. hyperhive code is synced from
|
# URL *inside* Forgejo. hyperhive code is synced from
|
||||||
# `localhost` (and the host LAN), which Forgejo's
|
# `localhost` (and the host LAN), which Forgejo's
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ let
|
||||||
cfg = config.services.hyperhive.swarm.forge;
|
cfg = config.services.hyperhive.swarm.forge;
|
||||||
gatewayCfg = config.services.hyperhive.gateway;
|
gatewayCfg = config.services.hyperhive.gateway;
|
||||||
swarmDomain = config.services.hyperhive.swarm.domain;
|
swarmDomain = config.services.hyperhive.swarm.domain;
|
||||||
deployCfg = config.services.hyperhive.deploy;
|
|
||||||
|
|
||||||
# Forgejo's name for the login source. Duplicated in ./default.nix, which
|
# Forgejo's name for the login source. Duplicated in ./default.nix, which
|
||||||
# registers the source under it.
|
# registers the source under it.
|
||||||
|
|
@ -24,13 +23,10 @@ let
|
||||||
# Forgejo's `ROOT_URL`, duplicated from ./default.nix, which documents its
|
# Forgejo's `ROOT_URL`, duplicated from ./default.nix, which documents its
|
||||||
# shape.
|
# shape.
|
||||||
defaultRootUrl =
|
defaultRootUrl =
|
||||||
if deployCfg.forgejo.behindGateway then
|
|
||||||
let
|
let
|
||||||
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
||||||
in
|
in
|
||||||
"https://${cfg.domain}${portSuffix}/"
|
"https://${cfg.domain}${portSuffix}/";
|
||||||
else
|
|
||||||
"http://${cfg.domain}:${toString cfg.httpPort}/";
|
|
||||||
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -94,8 +90,8 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Public hostname for the forge. Doubles as both the forgejo
|
Public hostname for the forge. Doubles as both the forgejo
|
||||||
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
||||||
gateway vhost server-name when `deploy.forgejo.behindGateway = true`
|
gateway vhost server-name (sub-domain routing — see
|
||||||
(sub-domain routing — see `docs/networking/gateway.md`).
|
`docs/networking/gateway.md`).
|
||||||
|
|
||||||
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
||||||
swarm's domain, not this hive's, because a swarm runs **one**
|
swarm's domain, not this hive's, because a swarm runs **one**
|
||||||
|
|
@ -116,10 +112,8 @@ in
|
||||||
|
|
||||||
publicUrl = lib.mkOption {
|
publicUrl = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = if deployCfg.forgejo.behindGateway then "https://${cfg.domain}" else null;
|
default = "https://${cfg.domain}";
|
||||||
defaultText = lib.literalExpression ''
|
defaultText = lib.literalExpression ''"https://''${domain}"'';
|
||||||
if behindGateway then "https://''${domain}" else null
|
|
||||||
'';
|
|
||||||
example = "https://forge.example.com";
|
example = "https://forge.example.com";
|
||||||
description = ''
|
description = ''
|
||||||
Browser-facing forge URL the dashboard uses to build clickable
|
Browser-facing forge URL the dashboard uses to build clickable
|
||||||
|
|
@ -127,20 +121,12 @@ in
|
||||||
the approval-queue's "review PR on forge" link) — sourced into
|
the approval-queue's "review PR on forge" link) — sourced into
|
||||||
every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`.
|
every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`.
|
||||||
|
|
||||||
Defaults to `https://''${cfg.domain}` when `deploy.forgejo.behindGateway =
|
Defaults to `https://''${cfg.domain}`, the gateway vhost. When
|
||||||
true` (the gateway vhost is genuinely reachable at that URL)
|
`null`, the dashboard **hides** forge links rather than
|
||||||
and `null` otherwise. When `null`, the dashboard **hides**
|
guessing one — see `docs/web-ui/dashboard.md::H0M3 page` for
|
||||||
forge links rather than guessing one — see
|
the rationale (a link built from the operator's own browser
|
||||||
`docs/web-ui/dashboard.md::H0M3 page` for the rationale (a
|
hostname + a container port is only an accident away from
|
||||||
link built from the operator's own browser hostname + a
|
wrong on any deployment that isn't plain localhost).
|
||||||
container port is only an accident away from wrong on any
|
|
||||||
deployment that isn't plain localhost).
|
|
||||||
|
|
||||||
**Set this explicitly if `deploy.forgejo.behindGateway = false`** and the
|
|
||||||
forge is still reachable at a stable URL you want linked from
|
|
||||||
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
|
|
||||||
for an all-LAN deployment) — leaving it unset there means the
|
|
||||||
dashboard's forge links are simply absent, not broken.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -150,20 +136,15 @@ in
|
||||||
example = "https://forge.example.com/";
|
example = "https://forge.example.com/";
|
||||||
description = ''
|
description = ''
|
||||||
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
||||||
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
|
(default), `ROOT_URL` is `https://''${cfg.domain}/`. The gateway
|
||||||
state, including the scheme:
|
|
||||||
|
|
||||||
- `deploy.forgejo.behindGateway = true` → `https://''${cfg.domain}/`. The gateway
|
|
||||||
always terminates TLS (self-signed is the implicit floor when no
|
always terminates TLS (self-signed is the implicit floor when no
|
||||||
`gateway.tls.certDir` / ACME is set), so the forge is always
|
`gateway.tls.certDir` / ACME is set), so the forge is always
|
||||||
advertised over https. A non-canonical `gateway.httpsPort` is
|
advertised over https. A non-canonical `gateway.httpsPort` is
|
||||||
appended as `:<port>`.
|
appended as `:<port>`.
|
||||||
- `deploy.forgejo.behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
|
||||||
|
|
||||||
The TLS scheme is derived automatically now, so you only need to
|
Set this only for a genuinely bespoke shape (e.g. an external
|
||||||
set this for a genuinely bespoke shape (e.g. an external reverse
|
reverse proxy on a different host/path). Must end with `/` per
|
||||||
proxy on a different host/path). Must end with `/` per forgejo's
|
forgejo's `ROOT_URL` contract.
|
||||||
`ROOT_URL` contract.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -202,11 +183,10 @@ in
|
||||||
format.
|
format.
|
||||||
|
|
||||||
⚠️ With {option}`services.hyperhive.swarm.forge.rootUrl` unset,
|
⚠️ With {option}`services.hyperhive.swarm.forge.rootUrl` unset,
|
||||||
`ROOT_URL` follows
|
`ROOT_URL` follows the gateway's `httpsPort`, which is
|
||||||
{option}`services.hyperhive.deploy.forgejo.behindGateway` and
|
per-host. An authelia host that is not the forge's host renders
|
||||||
the gateway's `httpsPort`, which are per-host. An authelia host
|
the forge's callback only if the two agree on it; set `rootUrl`
|
||||||
that is not the forge's host renders the forge's callback only
|
if they do not.
|
||||||
if the two agree on them; set `rootUrl` if they do not.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
# The secret half is a path on the host that runs the forge, so it
|
# The secret half is a path on the host that runs the forge, so it
|
||||||
|
|
|
||||||
|
|
@ -1060,7 +1060,7 @@ in
|
||||||
# Denied or client-scoped depending on whether a
|
# Denied or client-scoped depending on whether a
|
||||||
# collector is registered — see `metricsRule` above,
|
# collector is registered — see `metricsRule` above,
|
||||||
# which is where the reasoning for both halves lives.
|
# which is where the reasoning for both halves lives.
|
||||||
lib.optional deployCfg.forgejo.behindGateway metricsRule
|
[ metricsRule ]
|
||||||
# Also guarded on the domain being set: without it a null
|
# Also guarded on the domain being set: without it a null
|
||||||
# apex would render a rule matching the string "null".
|
# apex would render a rule matching the string "null".
|
||||||
++ lib.optional (deployCfg.swarm-ui.enable && swarmDomain != null) {
|
++ lib.optional (deployCfg.swarm-ui.enable && swarmDomain != null) {
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,6 @@ let
|
||||||
deploy.forgejo.ci.enable = true;
|
deploy.forgejo.ci.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# A hive whose one gateway-published swarm service sits on another host:
|
|
||||||
# every swarm name still configured, not one of them served here.
|
|
||||||
forgeElsewhere = hive { deploy.forgejo.behindGateway = false; };
|
|
||||||
|
|
||||||
# A priority collision is a property of the *option*, not
|
# A priority collision is a property of the *option*, not
|
||||||
# of the merged value's interior — nix throws the moment the value is
|
# of the merged value's interior — nix throws the moment the value is
|
||||||
# demanded at all, so `seq`-ing each `serviceConfig` value to WHNF is
|
# demanded at all, so `seq`-ing each `serviceConfig` value to WHNF is
|
||||||
|
|
@ -72,31 +68,12 @@ let
|
||||||
builtins.foldl' (acc: v: builtins.seq v acc) true vals;
|
builtins.foldl' (acc: v: builtins.seq v acc) true vals;
|
||||||
cases = [
|
cases = [
|
||||||
{
|
{
|
||||||
# Both halves matter. The equality is the "no longer consults the central
|
# The domain is the stub's swarm domain, which both fixtures share.
|
||||||
# toggle" half; the literal is the "and still renders what it always
|
name = "the forge's publicUrl default does not consult the central toggle";
|
||||||
# did" half, which an equality on its own would let drift to `false` in
|
|
||||||
# lockstep.
|
|
||||||
name = "the forge's behindGateway default is true regardless of the central toggle";
|
|
||||||
ok =
|
|
||||||
bare.services.hyperhive.deploy.forgejo.behindGateway == true
|
|
||||||
&& centralToggleOff.services.hyperhive.deploy.forgejo.behindGateway == true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
# Downstream of the one above — publicUrl reads `behindGateway`, so it
|
|
||||||
# tracked the central toggle transitively as well as directly. The domain
|
|
||||||
# is the stub's swarm domain, which both fixtures share.
|
|
||||||
name = "the forge's publicUrl default follows behindGateway alone, not the central toggle";
|
|
||||||
ok =
|
ok =
|
||||||
bare.services.hyperhive.swarm.forge.publicUrl == "https://forge.t.local"
|
bare.services.hyperhive.swarm.forge.publicUrl == "https://forge.t.local"
|
||||||
&& centralToggleOff.services.hyperhive.swarm.forge.publicUrl == "https://forge.t.local";
|
&& centralToggleOff.services.hyperhive.swarm.forge.publicUrl == "https://forge.t.local";
|
||||||
}
|
}
|
||||||
{
|
|
||||||
# And that it still tracks `behindGateway` at all: without this arm the
|
|
||||||
# case above passes just as well for a default hardcoded to the URL.
|
|
||||||
name = "the forge's publicUrl default is still null with behindGateway off";
|
|
||||||
ok =
|
|
||||||
(hive { deploy.forgejo.behindGateway = false; }).services.hyperhive.swarm.forge.publicUrl == null;
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
# The controller's token path follows where the forge runs
|
# The controller's token path follows where the forge runs
|
||||||
# (./forge-placement.nix), never the central toggle: a forge host with
|
# (./forge-placement.nix), never the central toggle: a forge host with
|
||||||
|
|
@ -226,9 +203,9 @@ let
|
||||||
# vhosts are the hive leaf's, which this host still signs.
|
# vhosts are the hive leaf's, which this host still signs.
|
||||||
name = "a host fronting none of the swarm's service names requests no services leaf";
|
name = "a host fronting none of the swarm's service names requests no services leaf";
|
||||||
ok =
|
ok =
|
||||||
forgeElsewhere.services.hyperhive.swarm.localServiceDomains == [ ]
|
bare.services.hyperhive.swarm.localServiceDomains == [ ]
|
||||||
&& forgeElsewhere.services.hyperhive.swarm.serviceDomains != [ ]
|
&& bare.services.hyperhive.swarm.serviceDomains != [ ]
|
||||||
&& lib.hasInfix "want_svc=0" forgeElsewhere.systemd.services.swarm-services-cert.script;
|
&& lib.hasInfix "want_svc=0" bare.systemd.services.swarm-services-cert.script;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# nixos asserts when a vhost declares both, so this is also a
|
# nixos asserts when a vhost declares both, so this is also a
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue