nix/hive-{forge,gateway}: move forge to forge.<hive-domain> sub-domain (#749, mara verdict)
mara on #749:9609: "we will go with sub domains for forge and matrix (redirected in well known in the latter case, not user visible). close / fix PRs you have open that dont match this." Reshapes the v1 sub-path (`<host>/forge/`) approach into a sub-domain vhost (`forge.<host>/`) per the mara verdict. matrix gets the same treatment in damocles's #751 follow-up. ## Why sub-domain - forgejo's default `ROOT_URL = http://<host>/` works without any `X-Forwarded-Prefix` gymnastics — sub-domain hosting is the canonical Forgejo deploy shape, matches every upstream-doc example. - Cookie / storage isolation between the dashboard and forge (XSS blast radius shrinks; a future forge XSS can't reach dashboard session). - matches the matrix-spec pattern that #751 wires up for the homeserver. ## Mechanics **forge options:** - `services.hyperhive.forge.subdomain` — nullable str, default `"forge"` → rendered sub-domain is `forge.<hive-domain>`. Set to `null` to opt out (forge stays direct on `httpPort`); set to `""` for bare-domain landing (advanced, collides with dashboard). - `services.hyperhive.forge.rootUrl` — nullable str override. When null, auto-derived: `http://<subdomain>.<hive>/` when gateway is on + subdomain set, else `http://<domain>:<httpPort>/` (direct). - **Asserts** rootUrl ends with `/` (argus 🟡 on #754: forgejo's ROOT_URL contract requires trailing slash, else emits `https://forge.example.com.user.id` shaped garbage). Asserts `subdomain != null` requires `hyperhive.domain` set. **gateway:** - New `virtualHosts."<subdomain>.<hive-domain>"` server block — separate from the `"_"` catch-all. Proxies all `/` → `http://127.0.0.1:<forge.httpPort>/` so forgejo handles requests at root (no prefix translation needed; matches the upstream-default ROOT_URL shape). - Git-tuned: `client_max_body_size 1G`, `proxy_read_timeout 1h`, `proxy_send_timeout 1h`, `proxy_buffering off`, `proxyWebsockets = true`. SSH stays direct on `cfg.sshPort`. - `networking.hosts` (when `localHostsEntry = true`) now also adds `forge.<hive-domain> -> 127.0.0.1` for the dev loop. ## Verified - `nix eval ROOT_URL` → `http://forge.test.local/` (default with gateway on) - `nix eval ROOT_URL` with `gateway.enable = false` → `http://localhost:3000/` (current direct shape preserved) - `nix eval virtualHosts attrs` → `["_", "forge.test.local"]` - `nix eval networking.hosts` with `localHostsEntry = true` → `{"127.0.0.1": ["test.local", "forge.test.local"], ...}` - bad rootUrl (no trailing /) triggers assertion at toplevel build with the spelled-out forgejo failure mode - full container toplevel builds clean (`nixos-system-hive-gateway-26.05pre-git`) ## Migration ROOT_URL change is a one-way migration on rebuild: - Existing agent `git remote origin` URLs (`http://localhost:3000/...`) **keep working** — forgejo accepts any inbound URL; the URL on the agent side is unchanged. - New clone-link copy-paste from forge UI uses `forge.<hive>/...` — operators copying clones after this lands need to go through the new sub-domain. - Direct browsing on `:3000` shows pages with `forge.<hive>` links → works if hosts entry / DNS resolves, broken otherwise. Operators should switch to `http://forge.<hive>/`. ## Out of scope - TLS termination (mara explicit on #15: no TLS v0) - SSH-over-HTTPS / wildcard cert provisioning - matrix sub-domain (damocles's #751, sibling work) Closes #749. Addresses argus 🟡 on #754.
This commit is contained in:
parent
3c2fffabdf
commit
f037056015
2 changed files with 162 additions and 4 deletions
|
|
@ -6,6 +6,31 @@
|
|||
}:
|
||||
let
|
||||
cfg = config.services.hyperhive.forge;
|
||||
gatewayCfg = config.services.hyperhive.gateway;
|
||||
hyperhiveDomain = config.services.hyperhive.domain;
|
||||
|
||||
# Sub-domain forgejo lives at when served behind the gateway (#749,
|
||||
# mara verdict at issue:9609 — sub-domain over sub-path). Defaults
|
||||
# to `forge.<hive-domain>`; set to `null` to opt out of subdomain
|
||||
# routing (forge stays direct on `cfg.httpPort`).
|
||||
subdomain =
|
||||
if cfg.subdomain == null then null else if cfg.subdomain == "" then hyperhiveDomain else "${cfg.subdomain}.${hyperhiveDomain}";
|
||||
|
||||
# ROOT_URL forgejo advertises in clone links + outbound URLs. When
|
||||
# served behind the gateway (#749), use the sub-domain so generated
|
||||
# URLs resolve cleanly through the per-subdomain server-block. When
|
||||
# direct (gateway off, or operator nulled `cfg.subdomain`), keep the
|
||||
# original port-3000 shape. Operators can override via `cfg.rootUrl`
|
||||
# for TLS / non-default gateway ports / bespoke sub-domains.
|
||||
defaultRootUrl =
|
||||
if subdomain != null && gatewayCfg.enable or false then
|
||||
let
|
||||
portSuffix = if gatewayCfg.port == 80 then "" else ":${toString gatewayCfg.port}";
|
||||
in
|
||||
"http://${subdomain}${portSuffix}/"
|
||||
else
|
||||
"http://${cfg.domain}:${toString cfg.httpPort}/";
|
||||
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
||||
in
|
||||
{
|
||||
# Private Forgejo for hyperhive agents, wrapped in a nixos-container
|
||||
|
|
@ -85,6 +110,55 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
subdomain = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = "forge";
|
||||
example = "git";
|
||||
description = ''
|
||||
Sub-domain label for the gateway vhost that serves forgejo
|
||||
(#749). The gateway adds a `server { server_name ''${subdomain}.''${hyperhive.domain}; }`
|
||||
block that proxies all `/` → `http://127.0.0.1:''${httpPort}/`.
|
||||
Forgejo's `ROOT_URL` auto-flips to
|
||||
`http://''${subdomain}.''${hyperhive.domain}/` so clone-links +
|
||||
asset references resolve cleanly through the sub-domain.
|
||||
|
||||
Defaults to `"forge"` (→ `forge.''${hyperhive.domain}`).
|
||||
Set to `null` to opt out — forge stays direct on `httpPort`,
|
||||
no gateway vhost. Set to the empty string `""` for a bare-domain
|
||||
landing (advanced: collides with the dashboard server block).
|
||||
|
||||
Requires `services.hyperhive.domain` to be set. Requires
|
||||
`services.hyperhive.gateway.enable = true` for the vhost to
|
||||
actually exist.
|
||||
|
||||
The mara-call on #749:9609 picks sub-domain over sub-path 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 via #731.
|
||||
'';
|
||||
};
|
||||
|
||||
rootUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "https://forge.example.com/";
|
||||
description = ''
|
||||
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
||||
(default), `ROOT_URL` is derived from `subdomain` + gateway
|
||||
state:
|
||||
|
||||
- gateway on + `subdomain != null` → `http://<subdomain>.<hive>/`
|
||||
(uses `services.hyperhive.gateway.port` when non-80)
|
||||
- otherwise → `http://<domain>:<httpPort>/` (direct)
|
||||
|
||||
Set this to a fully-qualified URL when running behind TLS
|
||||
termination, a non-default gateway port, or a bespoke
|
||||
sub-domain shape (e.g. `https://forge.example.com/`). Must
|
||||
end with `/` per forgejo's `ROOT_URL` contract.
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
|
|
@ -108,6 +182,33 @@ in
|
|||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
||||
message = ''
|
||||
services.hyperhive.forge.rootUrl must end with "/". forgejo's
|
||||
ROOT_URL contract requires a trailing slash for correct
|
||||
relative-link generation; without it forgejo emits URLs like
|
||||
`https://forge.example.com.user.id` instead of
|
||||
`https://forge.example.com/user.id`. Got: ${toString cfg.rootUrl}
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
cfg.subdomain == null
|
||||
|| hyperhiveDomain != null
|
||||
|| cfg.rootUrl != null;
|
||||
message = ''
|
||||
services.hyperhive.forge.subdomain = "${toString cfg.subdomain}"
|
||||
requires services.hyperhive.domain to be set (sub-domain is
|
||||
rendered as "<subdomain>.<hive-domain>"). Either set
|
||||
services.hyperhive.domain, override services.hyperhive.forge.rootUrl
|
||||
directly, or set services.hyperhive.forge.subdomain = null to opt
|
||||
out of sub-domain routing.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
containers.hive-forge = {
|
||||
autoStart = true;
|
||||
ephemeral = false;
|
||||
|
|
@ -150,7 +251,7 @@ in
|
|||
DEFAULT.APP_NAME = "HyperHive";
|
||||
server = {
|
||||
DOMAIN = cfg.domain;
|
||||
ROOT_URL = "http://${cfg.domain}:${toString cfg.httpPort}/";
|
||||
ROOT_URL = effectiveRootUrl;
|
||||
HTTP_PORT = cfg.httpPort;
|
||||
START_SSH_SERVER = true;
|
||||
SSH_PORT = cfg.sshPort;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ let
|
|||
cfg = config.services.hyperhive.gateway;
|
||||
hyperhiveDomain = config.services.hyperhive.domain;
|
||||
matrixCfg = config.services.hyperhive.matrix;
|
||||
forgeCfg = config.services.hyperhive.forge;
|
||||
|
||||
# Per-agent port table for `/agent/<name>/` routing (#15 v0). Single-
|
||||
# sourced from `cfg.agentPortsFile` (default
|
||||
|
|
@ -237,7 +238,8 @@ in
|
|||
"~*text/html" "/matrix/index.html";
|
||||
}
|
||||
'';
|
||||
virtualHosts."_" = {
|
||||
virtualHosts = {
|
||||
"_" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
|
|
@ -393,7 +395,52 @@ in
|
|||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
//
|
||||
# Forge sub-domain vhost (#749, mara verdict at issue:9609 —
|
||||
# sub-domain over sub-path). When forgejo runs behind the
|
||||
# gateway, it gets its own `server { server_name ...; }`
|
||||
# block keyed on `<forge.subdomain>.<hive-domain>`. The
|
||||
# block proxies all `/` → `http://127.0.0.1:<forge.httpPort>/`
|
||||
# so forgejo handles requests at root (default deploy shape
|
||||
# — no `ROOT_URL`-prefix translation needed).
|
||||
#
|
||||
# `client_max_body_size 1G` — git pushes + LFS uploads can
|
||||
# be large; nginx's default 1M would 413 most real commits.
|
||||
#
|
||||
# Long timeouts for big repo operations: a fresh clone of a
|
||||
# multi-GB repo can take minutes; the default 60s
|
||||
# `proxy_read_timeout` would abort mid-stream.
|
||||
#
|
||||
# `proxyWebsockets = true` keeps forgejo's live-update
|
||||
# endpoints (`/api/v1/events`) + any future websocket
|
||||
# endpoints working transparently. SSH stays direct on
|
||||
# `cfg.sshPort` (separate listener protocol, not HTTP).
|
||||
lib.optionalAttrs (
|
||||
forgeCfg.enable or false
|
||||
&& (forgeCfg.subdomain or null) != null
|
||||
&& hyperhiveDomain != null
|
||||
) {
|
||||
"${forgeCfg.subdomain}.${hyperhiveDomain}" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = cfg.port;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString forgeCfg.httpPort}/";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
proxy_buffering off;
|
||||
client_max_body_size 1G;
|
||||
proxy_read_timeout 1h;
|
||||
proxy_send_timeout 1h;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -402,8 +449,18 @@ in
|
|||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
|
||||
# `/etc/hosts` entries for local dev: the bare hive domain plus
|
||||
# any sub-domain modules (forge, matrix-via-#751) that are on.
|
||||
# All map to `127.0.0.1` since the gateway shares host netns.
|
||||
# Operators with real DNS leave `localHostsEntry = false`; this
|
||||
# is the dev-loop shortcut for `http://<hive-domain>/` +
|
||||
# `http://forge.<hive-domain>/` resolving locally.
|
||||
networking.hosts = lib.mkIf (cfg.localHostsEntry && hyperhiveDomain != null) {
|
||||
"127.0.0.1" = [ hyperhiveDomain ];
|
||||
"127.0.0.1" = [ hyperhiveDomain ]
|
||||
++ lib.optional (
|
||||
(config.services.hyperhive.forge.enable or false)
|
||||
&& (config.services.hyperhive.forge.subdomain or null) != null
|
||||
) "${config.services.hyperhive.forge.subdomain}.${hyperhiveDomain}";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue