deploy: split the forge's host decisions out of swarm.forge
`swarm.*` is what a hive needs to be a *client* of the swarm. For the forge that is what it IS from any hive's point of view: its package, the names and ports it answers on, the URLs it advertises, and the client id it is registered under. How it is served, what it mirrors and where its host-local secrets sit are decisions of the machine running it, so behindGateway, openFirewall, mirrors, sso.clientSecretFile and hostSwarmControllerTokenFile move to `deploy.forgejo.*`. Unlike the wireguard mesh this SPLITS a module rather than relocating a whole namespace. `sso` splits with it: `clientId` stays because it must match the id in authelia's register, while the secret beside it is a path on one host. Moving the whole `sso` block for symmetry with `ci` was considered and rejected on exactly that asymmetry. Declared in hive-forge/default.nix under the `deploy.*` path, following swarm-victorialogs.nix; deploy.nix carries only the renames. `mirrors` renames in one entry rather than one per field — it is a single option of a list-of-submodule type, so the rename carries its whole value, where `ci` needed five because it is a plain attrset of options. Readers outside the module: hive-ci.nix binds `deploy.forgejo` for its behindGateway assertion; swarm-authelia.nix and swarm-controller.nix read theirs off the `deployCfg` they already bind. hivectl's `open` printed `services.hyperhive.forge.behindGateway` in an operator-facing hint — a path that never existed, missing `swarm.` — and hive-c0re's state_snapshot doc comment carried the same defect; both now name the new path. The rendered docs put the two halves on separate pages, so the five descriptions of staying options that explain themselves in terms of `behindGateway` now qualify it in full. module-eval gains a forge case configured entirely through the old paths, asserting the rendered firewall ports and the mirror env var c0re seeds from: the new paths evaluate fine without the shims, so dropping them reads as a clean tree. All five old paths are defined in the fixture, so removing any single shim entry fails the eval rather than only the two the assertion reads.
This commit is contained in:
parent
368f5d82aa
commit
1db2ac26a7
13 changed files with 211 additions and 122 deletions
|
|
@ -75,7 +75,7 @@ let
|
|||
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
||||
# shapes.
|
||||
defaultRootUrl =
|
||||
if cfg.behindGateway then
|
||||
if deployCfg.forgejo.behindGateway then
|
||||
let
|
||||
portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}";
|
||||
in
|
||||
|
|
@ -98,9 +98,9 @@ let
|
|||
# operator hasn't already declared that dest themselves (else CI-on +
|
||||
# an explicit `actions/checkout` entry would duplicate it).
|
||||
effectiveMirrors =
|
||||
cfg.mirrors
|
||||
deployCfg.forgejo.mirrors
|
||||
++ lib.optional (
|
||||
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
||||
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) deployCfg.forgejo.mirrors)
|
||||
) actionCheckoutMirror;
|
||||
in
|
||||
{
|
||||
|
|
@ -133,7 +133,7 @@ in
|
|||
Removed rather than defaulted to true so a config that turned it
|
||||
OFF fails here, where the line is, instead of silently gaining a
|
||||
login provider on the next rebuild. Drop the line; if it was
|
||||
false, set services.hyperhive.swarm.forge.sso.clientSecretFile and
|
||||
false, set services.hyperhive.deploy.forgejo.sso.clientSecretFile and
|
||||
services.hyperhive.swarm.authelia.url as the assertions describe.
|
||||
'')
|
||||
];
|
||||
|
|
@ -178,7 +178,7 @@ in
|
|||
description = ''
|
||||
Public hostname for the forge. Doubles as both the forgejo
|
||||
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
||||
gateway vhost server-name when `behindGateway = true`
|
||||
gateway vhost server-name when `deploy.forgejo.behindGateway = true`
|
||||
(sub-domain routing — see `docs/networking/gateway.md`).
|
||||
|
||||
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
||||
|
|
@ -201,7 +201,10 @@ in
|
|||
publicUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default =
|
||||
if config.services.hyperhive.enable && cfg.behindGateway then "https://${cfg.domain}" else null;
|
||||
if config.services.hyperhive.enable && deployCfg.forgejo.behindGateway then
|
||||
"https://${cfg.domain}"
|
||||
else
|
||||
null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if behindGateway then "https://''${domain}" else null
|
||||
'';
|
||||
|
|
@ -212,7 +215,7 @@ in
|
|||
the approval-queue's "review PR on forge" link) — sourced into
|
||||
every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`.
|
||||
|
||||
Defaults to `https://''${cfg.domain}` when `behindGateway =
|
||||
Defaults to `https://''${cfg.domain}` when `deploy.forgejo.behindGateway =
|
||||
true` (the gateway vhost is genuinely reachable at that URL)
|
||||
and `null` otherwise. When `null`, the dashboard **hides**
|
||||
forge links rather than guessing one — see
|
||||
|
|
@ -221,7 +224,7 @@ in
|
|||
container port is only an accident away from wrong on any
|
||||
deployment that isn't plain localhost).
|
||||
|
||||
**Set this explicitly if `behindGateway = false`** and the
|
||||
**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
|
||||
|
|
@ -244,6 +247,60 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
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 `cfg.domain` + 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
|
||||
`gateway.tls.certDir` / ACME is set), so the forge is always
|
||||
advertised over https. A non-canonical `gateway.httpsPort` is
|
||||
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 for a genuinely bespoke shape (e.g. an external reverse
|
||||
proxy on a different host/path). Must end with `/` per forgejo's
|
||||
`ROOT_URL` contract.
|
||||
'';
|
||||
};
|
||||
|
||||
# The swarm's authelia is always registered as an OpenID Connect
|
||||
# login source here — there is no toggle, for the same reason the
|
||||
# forge itself has none.
|
||||
#
|
||||
# **Additive, never exclusive.** Forgejo keeps its local password
|
||||
# database and gains an extra "sign in with" button; this does not
|
||||
# disable local login. Deliberate: an identity provider that can take
|
||||
# the forge offline when it hiccups is a worse forge than one with
|
||||
# two ways in — which is also what makes always-on safe.
|
||||
sso = {
|
||||
clientId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "forgejo";
|
||||
description = ''
|
||||
OAuth2 client id this forge identifies itself with. Must match
|
||||
the `id` of the corresponding entry in
|
||||
`services.hyperhive.swarm.authelia.oidc.clients`.
|
||||
'';
|
||||
};
|
||||
# The secret half is a path on the host that runs the forge, so it
|
||||
# lives under `deploy.forgejo.sso` — see the block below.
|
||||
};
|
||||
};
|
||||
|
||||
# What stays above is what the forge IS from any hive's point of view: its
|
||||
# package, the names and ports it answers on, the URLs it advertises, and
|
||||
# the client id it is registered under. What lives here is what the host
|
||||
# running it decides — how it is served, what it mirrors, and where its
|
||||
# host-local secrets sit. Same rule as ./swarm-victorialogs.nix, and the
|
||||
# renames are in ./deploy.nix with the rest.
|
||||
options.services.hyperhive.deploy.forgejo = {
|
||||
behindGateway = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.services.hyperhive.enable;
|
||||
|
|
@ -274,29 +331,6 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
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 `cfg.domain` + gateway
|
||||
state, including the scheme:
|
||||
|
||||
- `behindGateway = true` → `https://''${cfg.domain}/`. The gateway
|
||||
always terminates TLS (self-signed is the implicit floor when no
|
||||
`gateway.tls.certDir` / ACME is set), so the forge is always
|
||||
advertised over https. A non-canonical `gateway.httpsPort` is
|
||||
appended as `:<port>`.
|
||||
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
||||
|
||||
The TLS scheme is derived automatically now, so you only need to
|
||||
set this for a genuinely bespoke shape (e.g. an external reverse
|
||||
proxy on a different host/path). Must end with `/` per forgejo's
|
||||
`ROOT_URL` contract.
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
|
|
@ -312,8 +346,8 @@ in
|
|||
|
||||
**Breaking change**: this used to default to `true`. If you
|
||||
relied on the old default for external reach, add
|
||||
`services.hyperhive.swarm.forge.openFirewall = true;` to your host
|
||||
config before rebuilding.
|
||||
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your
|
||||
host config before rebuilding.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -360,45 +394,27 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# The swarm's authelia is always registered as an OpenID Connect
|
||||
# login source here — there is no toggle, for the same reason the
|
||||
# forge itself has none.
|
||||
#
|
||||
# **Additive, never exclusive.** Forgejo keeps its local password
|
||||
# database and gains an extra "sign in with" button; this does not
|
||||
# disable local login. Deliberate: an identity provider that can take
|
||||
# the forge offline when it hiccups is a worse forge than one with
|
||||
# two ways in — which is also what makes always-on safe.
|
||||
sso = {
|
||||
clientId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "forgejo";
|
||||
description = ''
|
||||
OAuth2 client id this forge identifies itself with. Must match
|
||||
the `id` of the corresponding entry in
|
||||
`services.hyperhive.swarm.authelia.oidc.clients`.
|
||||
'';
|
||||
};
|
||||
# The other half of the SSO pair: the client *id* is swarm-wide (it has
|
||||
# to match authelia's register), the secret is a path on this host. The
|
||||
# design note for the login source itself is with the id.
|
||||
sso.clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/hyperhive/forge-oidc-secret";
|
||||
description = ''
|
||||
Path **inside the forge container** holding the client
|
||||
secret's plaintext.
|
||||
|
||||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/hyperhive/forge-oidc-secret";
|
||||
description = ''
|
||||
Path **inside the forge container** holding the client
|
||||
secret's plaintext.
|
||||
A path, never a value: an OIDC client secret has two holders
|
||||
in two containers (authelia keeps a hash, this forge needs the
|
||||
plaintext), and a literal written here would be rendered into
|
||||
the world-readable nix store.
|
||||
|
||||
A path, never a value: an OIDC client secret has two holders
|
||||
in two containers (authelia keeps a hash, this forge needs the
|
||||
plaintext), and a literal written here would be rendered into
|
||||
the world-readable nix store.
|
||||
|
||||
Required when `enable` is set — deliberately no fallback. A
|
||||
forge that boots with SSO half-configured presents as a login
|
||||
button that always fails, which is harder to diagnose than an
|
||||
eval error.
|
||||
'';
|
||||
};
|
||||
Required when `enable` is set — deliberately no fallback. A
|
||||
forge that boots with SSO half-configured presents as a login
|
||||
button that always fails, which is harder to diagnose than an
|
||||
eval error.
|
||||
'';
|
||||
};
|
||||
|
||||
hostSwarmControllerTokenFile = lib.mkOption {
|
||||
|
|
@ -443,13 +459,13 @@ in
|
|||
# Both halves are gated on `behindGateway`: with it off the operator
|
||||
# fronts forgejo themselves, so this hive must neither claim the
|
||||
# vhost nor answer DNS for it.
|
||||
services.hyperhive.gateway.localNames = lib.optional cfg.behindGateway cfg.domain;
|
||||
services.hyperhive.gateway.localNames = lib.optional deployCfg.forgejo.behindGateway cfg.domain;
|
||||
|
||||
# This swarm-ui quick-links entry, same `behindGateway` guard as the
|
||||
# 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 = lib.optional cfg.behindGateway {
|
||||
services.hyperhive.swarm.controller.links = lib.optional deployCfg.forgejo.behindGateway {
|
||||
label = "Forge";
|
||||
icon = "⚒";
|
||||
url = "https://${cfg.domain}/";
|
||||
|
|
@ -466,15 +482,17 @@ in
|
|||
# what authelia compares against — this string agreeing with the
|
||||
# `location` block above it is the whole mechanism. A near miss is a
|
||||
# correctly minted token refused at the target.
|
||||
services.hyperhive.swarm.otel.publishedScrapeTargets = lib.optionalAttrs cfg.behindGateway {
|
||||
forgejo = "https://${cfg.domain}/metrics";
|
||||
};
|
||||
services.hyperhive.swarm.otel.publishedScrapeTargets =
|
||||
lib.optionalAttrs deployCfg.forgejo.behindGateway
|
||||
{
|
||||
forgejo = "https://${cfg.domain}/metrics";
|
||||
};
|
||||
|
||||
# `server_name = forge.domain`, proxies all `/` → forgejo. Tuned for
|
||||
# git: `client_max_body_size 1G`, `proxy_read_timeout 1h` (multi-GB
|
||||
# clones). SSH stays direct on `forge.sshPort`. See
|
||||
# `docs/networking/gateway.md`.
|
||||
services.nginx.virtualHosts = lib.optionalAttrs cfg.behindGateway {
|
||||
services.nginx.virtualHosts = lib.optionalAttrs deployCfg.forgejo.behindGateway {
|
||||
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
||||
listen = gatewayCfg.lib.listen;
|
||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||
|
|
@ -543,10 +561,10 @@ in
|
|||
{
|
||||
# Fail at EVAL, not at boot. The alternative failure is a login
|
||||
# button that always 401s, three layers from the missing file.
|
||||
assertion = cfg.sso.clientSecretFile != null;
|
||||
assertion = deployCfg.forgejo.sso.clientSecretFile != null;
|
||||
message = ''
|
||||
The forge's SSO login source requires
|
||||
services.hyperhive.swarm.forge.sso.clientSecretFile — the path
|
||||
services.hyperhive.deploy.forgejo.sso.clientSecretFile — the path
|
||||
(inside the forge container) holding the OIDC client secret's
|
||||
plaintext.
|
||||
|
||||
|
|
@ -600,7 +618,7 @@ in
|
|||
# splits on the single slash to create the org + repo.
|
||||
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
|
||||
message = ''
|
||||
Every services.hyperhive.swarm.forge.mirrors[].dest must be exactly
|
||||
Every services.hyperhive.deploy.forgejo.mirrors[].dest must be exactly
|
||||
"<owner>/<repo>" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors}
|
||||
'';
|
||||
}
|
||||
|
|
@ -618,7 +636,7 @@ in
|
|||
])
|
||||
) effectiveMirrors;
|
||||
message = ''
|
||||
services.hyperhive.swarm.forge.mirrors[].dest must not place a mirror
|
||||
services.hyperhive.deploy.forgejo.mirrors[].dest must not place a mirror
|
||||
in a hive-c0re-managed org (config / shared / agents / core) —
|
||||
those are provisioned by hive-c0re and a mirror there would
|
||||
collide. Use a dedicated org (e.g. "actions/checkout").
|
||||
|
|
@ -771,7 +789,7 @@ in
|
|||
# scraper at the gateway, so a second credential system per
|
||||
# service would buy nothing and would be the one that stops
|
||||
# getting rotated.
|
||||
metrics.ENABLED = cfg.behindGateway;
|
||||
metrics.ENABLED = deployCfg.forgejo.behindGateway;
|
||||
# The two per-dimension breakdowns, on the same condition as
|
||||
# the endpoint itself: `gitea_issues_by_label{label=…}` and
|
||||
# `gitea_issues_by_repository{repository=…}`. Off by default
|
||||
|
|
@ -790,8 +808,8 @@ in
|
|||
# forge that grew to thousands of repos would want this
|
||||
# revisited — that is a real trigger, unlike a time-based one:
|
||||
# `count(gitea_issues_by_repository)` answers it directly.
|
||||
metrics.ENABLED_ISSUE_BY_LABEL = cfg.behindGateway;
|
||||
metrics.ENABLED_ISSUE_BY_REPOSITORY = cfg.behindGateway;
|
||||
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway;
|
||||
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.behindGateway;
|
||||
# Repo migrations / pull-mirrors fetch from the source
|
||||
# URL *inside* Forgejo. hyperhive code is synced from
|
||||
# `localhost` (and the host LAN), which Forgejo's
|
||||
|
|
@ -1025,9 +1043,9 @@ in
|
|||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
|
||||
secret=$(cat ${lib.escapeShellArg deployCfg.forgejo.sso.clientSecretFile})
|
||||
if [ -z "$secret" ]; then
|
||||
echo "empty OIDC client secret at ${cfg.sso.clientSecretFile}" >&2
|
||||
echo "empty OIDC client secret at ${deployCfg.forgejo.sso.clientSecretFile}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -1204,7 +1222,7 @@ in
|
|||
|
||||
# Same case, same reasoning: this host minted the secret, so it can
|
||||
# say where the forge will find it.
|
||||
services.hyperhive.swarm.forge.sso.clientSecretFile = lib.mkIf ssoLocal (
|
||||
services.hyperhive.deploy.forgejo.sso.clientSecretFile = lib.mkIf ssoLocal (
|
||||
lib.mkDefault forgeSecretPath
|
||||
);
|
||||
|
||||
|
|
@ -1298,7 +1316,7 @@ in
|
|||
set -euo pipefail
|
||||
|
||||
src=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${swarmControllerTokenPath}"}
|
||||
dst=${lib.escapeShellArg cfg.hostSwarmControllerTokenFile}
|
||||
dst=${lib.escapeShellArg deployCfg.forgejo.hostSwarmControllerTokenFile}
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
[ -s "$src" ] && break
|
||||
|
|
@ -1313,7 +1331,7 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||
networking.firewall = lib.mkIf deployCfg.forgejo.openFirewall {
|
||||
allowedTCPPorts = [
|
||||
cfg.httpPort
|
||||
cfg.sshPort
|
||||
|
|
|
|||
Loading…
Reference in a new issue