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
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
Private Forgejo instance running in a nixos-container, used as the
|
Private Forgejo instance running in a nixos-container, used as the
|
||||||
swarm's persistent code-collaboration surface (issues, PRs, reviews,
|
swarm's persistent code-collaboration surface (issues, PRs, reviews,
|
||||||
attachments). Configured via `services.hyperhive.swarm.forge.*`. Container
|
attachments). Configured via `services.hyperhive.swarm.forge.*` (what the
|
||||||
|
forge IS to every hive — its package, names, ports, URLs) plus
|
||||||
|
`services.hyperhive.deploy.forgejo.*` (what the host running it decides —
|
||||||
|
how it is served, what it mirrors, where its host-local secrets sit).
|
||||||
|
Container
|
||||||
shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL
|
shape, ROOT_URL / sub-domain routing, and operator-vs-in-cluster URL
|
||||||
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the
|
handling live in [`docs/networking/gateway.md`](../networking/gateway.md); this file owns the
|
||||||
per-agent integration story and the notification pump that wakes
|
per-agent integration story and the notification pump that wakes
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Single nginx in front of every hyperhive web surface. Runs on the **host**, next
|
||||||
| `<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`) | `forge.behindGateway` |
|
| `forge.<swarm>/` | `forge.<swarm>` | forgejo (`3000`) | `deploy.forgejo.behindGateway` |
|
||||||
| `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` |
|
||||||
|
|
@ -62,7 +62,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 forge.behindGateway)
|
- `forge.<swarm>` → `127.0.0.1` (when deploy.forgejo.behindGateway)
|
||||||
- `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)
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ 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 still
|
||||||
resolve against `http://<host>:3000` (separate sub-domain transition
|
resolve against `http://<host>:3000` (separate sub-domain transition
|
||||||
tracked by `forge.behindGateway`); `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.
|
||||||
|
|
||||||
|
|
@ -333,10 +333,12 @@ container.
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.swarm.forge = {
|
services.hyperhive.swarm.forge = {
|
||||||
httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range
|
httpPort = 3000; # default — HTTP listener; outside hyperhive's 7000/8100-8999 range
|
||||||
sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh
|
sshPort = 2222; # default — git-over-SSH; kept off 22 so it doesn't collide with the host openssh
|
||||||
openFirewall = false; # default — expose httpPort + sshPort to the host firewall
|
|
||||||
};
|
};
|
||||||
|
# Which ports the forge answers on is swarm-wide; whether THIS host opens
|
||||||
|
# them in its firewall is a deployment decision, so it lives under deploy.*.
|
||||||
|
services.hyperhive.deploy.forgejo.openFirewall = false; # default
|
||||||
```
|
```
|
||||||
|
|
||||||
`httpPort` (default **3000**) is the port Forgejo's HTTP server binds to.
|
`httpPort` (default **3000**) is the port Forgejo's HTTP server binds to.
|
||||||
|
|
@ -357,7 +359,7 @@ the bridge), not the raw port, so no firewall hole is needed. Flip to
|
||||||
(not behind the gateway).
|
(not behind the gateway).
|
||||||
- External git clients that push/pull via SSH directly to the host.
|
- External git clients that push/pull via SSH directly to the host.
|
||||||
|
|
||||||
Forgejo served through the gateway (`forge.behindGateway = true`) does
|
Forgejo served through the gateway (`deploy.forgejo.behindGateway = true`) does
|
||||||
not need `openFirewall` — the gateway's own `openFirewall` option covers
|
not need `openFirewall` — the gateway's own `openFirewall` option covers
|
||||||
that path.
|
that path.
|
||||||
|
|
||||||
|
|
@ -373,8 +375,8 @@ covers most cases:
|
||||||
|
|
||||||
| Shape | Auto-derived `ROOT_URL` |
|
| Shape | Auto-derived `ROOT_URL` |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
|
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (port suffix omitted when `gateway.httpsPort == 443`) |
|
||||||
| `behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
|
||||||
|
|
||||||
The gateway always terminates TLS, so the `behindGateway = true` case is
|
The gateway always terminates TLS, so the `behindGateway = true` case is
|
||||||
always advertised over `https://`; only the direct (`behindGateway =
|
always advertised over `https://`; only the direct (`behindGateway =
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,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 cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `forge.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 cannot 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`.
|
||||||
- **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.
|
||||||
|
|
@ -150,10 +150,10 @@ forge provisioning sweep (orgs, agent accounts, etc.), so mirror
|
||||||
seeding lives in the same place rather than a separate host-side unit.
|
seeding lives in the same place rather than a separate host-side unit.
|
||||||
|
|
||||||
**General-purpose mirrors**: you can pre-seed any external repo as a
|
**General-purpose mirrors**: you can pre-seed any external repo as a
|
||||||
pull-mirror via `services.hyperhive.swarm.forge.mirrors`:
|
pull-mirror via `services.hyperhive.deploy.forgejo.mirrors`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
services.hyperhive.swarm.forge.mirrors = [
|
services.hyperhive.deploy.forgejo.mirrors = [
|
||||||
{ upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }
|
{ upstream = "https://github.com/actions/checkout"; dest = "actions/checkout"; }
|
||||||
{ upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; }
|
{ upstream = "https://github.com/example/tool"; dest = "mirrors/tool"; }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,6 @@ 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.swarm.forge.behindGateway = true`; `matrix` needs
|
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
|
||||||
`services.hyperhive.swarm.matrix.gui.enable = true`. In those cases the command
|
`services.hyperhive.swarm.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,8 @@ pub(super) struct StateSnapshot {
|
||||||
/// Public URL of the forge (e.g.
|
/// Public URL of the forge (e.g.
|
||||||
/// `"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.forge.publicUrl` (defaults to
|
/// sets from `services.hyperhive.swarm.forge.publicUrl` (defaults to
|
||||||
/// the gateway vhost URL when `forge.behindGateway = true`,
|
/// the gateway vhost URL when `deploy.forgejo.behindGateway = true`,
|
||||||
/// `null` otherwise). `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
|
||||||
|
|
|
||||||
|
|
@ -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.forge.behindGateway = true`",
|
"the public forge URL needs `services.hyperhive.deploy.forgejo.behindGateway = true`",
|
||||||
),
|
),
|
||||||
OpenTarget::Matrix => (
|
OpenTarget::Matrix => (
|
||||||
urls.matrix,
|
urls.matrix,
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,38 @@ in
|
||||||
[ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ]
|
[ "services" "hyperhive" "deploy" "forgejo" "ci" "package" ]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The rest of the forge split. What stays under `swarm.forge` 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, the client id it is registered
|
||||||
|
# under; these five are what the host running it decides. `sso` splits
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# `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. 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.
|
||||||
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "services" "hyperhive" "swarm" "forge" "behindGateway" ]
|
||||||
|
[ "services" "hyperhive" "deploy" "forgejo" "behindGateway" ]
|
||||||
|
)
|
||||||
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "services" "hyperhive" "swarm" "forge" "openFirewall" ]
|
||||||
|
[ "services" "hyperhive" "deploy" "forgejo" "openFirewall" ]
|
||||||
|
)
|
||||||
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "services" "hyperhive" "swarm" "forge" "mirrors" ]
|
||||||
|
[ "services" "hyperhive" "deploy" "forgejo" "mirrors" ]
|
||||||
|
)
|
||||||
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "services" "hyperhive" "swarm" "forge" "sso" "clientSecretFile" ]
|
||||||
|
[ "services" "hyperhive" "deploy" "forgejo" "sso" "clientSecretFile" ]
|
||||||
|
)
|
||||||
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "services" "hyperhive" "swarm" "forge" "hostSwarmControllerTokenFile" ]
|
||||||
|
[ "services" "hyperhive" "deploy" "forgejo" "hostSwarmControllerTokenFile" ]
|
||||||
|
)
|
||||||
|
|
||||||
# Retention is read only where the container is defined, so it is a
|
# Retention is read only where the container is defined, so it is a
|
||||||
# decision of the host running the store rather than something the swarm
|
# decision of the host running the store rather than something the swarm
|
||||||
# agrees on. The two stores keep everything else — package, domain, port
|
# agrees on. The two stores keep everything else — package, domain, port
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,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 `behindGateway = true` and `null` otherwise
|
# gateway vhost URL when `deploy.forgejo.behindGateway = true` and `null`
|
||||||
# — see that option's doc for the "hide, don't guess" rationale.
|
# otherwise — see that option's doc for the "hide, don't guess" 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;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
let
|
let
|
||||||
cfg = config.services.hyperhive.deploy.forgejo.ci;
|
cfg = config.services.hyperhive.deploy.forgejo.ci;
|
||||||
forgeCfg = config.services.hyperhive.swarm.forge;
|
forgeCfg = config.services.hyperhive.swarm.forge;
|
||||||
|
forgeDeployCfg = config.services.hyperhive.deploy.forgejo;
|
||||||
gatewayCfg = config.services.hyperhive.gateway;
|
gatewayCfg = config.services.hyperhive.gateway;
|
||||||
networkCfg = config.services.hyperhive.network;
|
networkCfg = config.services.hyperhive.network;
|
||||||
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
|
tlsCfg = config.services.hyperhive.deploy.hive-controller.tls;
|
||||||
|
|
@ -79,8 +80,8 @@ in
|
||||||
example = true;
|
example = true;
|
||||||
description = ''
|
description = ''
|
||||||
Run a Forgejo Actions runner in a `hive-ci` nixos-container.
|
Run a Forgejo Actions runner in a `hive-ci` nixos-container.
|
||||||
Grouped under `services.hyperhive.swarm.forge` because the runner is
|
Grouped under `services.hyperhive.deploy.forgejo` because the runner
|
||||||
tightly coupled to the forge instance it registers against.
|
is tightly coupled to the forge instance it registers against.
|
||||||
Disabled by default; the internal forge it registers against is
|
Disabled by default; the internal forge it registers against is
|
||||||
always present (mandatory), so enabling this is all that's needed.
|
always present (mandatory), so enabling this is all that's needed.
|
||||||
|
|
||||||
|
|
@ -153,17 +154,17 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# `forge.behindGateway = true` (the default) is required because the
|
# `deploy.forgejo.behindGateway = true` (the default) is required because
|
||||||
# CI container uses private networking and reaches the forge through
|
# the CI container uses private networking and reaches the forge through
|
||||||
# the gateway vhost. Without the gateway vhost there is no HTTP
|
# the gateway vhost. Without the gateway vhost there is no HTTP
|
||||||
# listener for `forgeCfg.domain` on the bridge that the runner can
|
# listener for `forgeCfg.domain` on the bridge that the runner can
|
||||||
# connect to.
|
# connect to.
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = forgeCfg.behindGateway;
|
assertion = forgeDeployCfg.behindGateway;
|
||||||
message = ''
|
message = ''
|
||||||
services.hyperhive.deploy.forgejo.ci.enable requires
|
services.hyperhive.deploy.forgejo.ci.enable requires
|
||||||
services.hyperhive.swarm.forge.behindGateway = true.
|
services.hyperhive.deploy.forgejo.behindGateway = true.
|
||||||
The CI container runs with a private network namespace and
|
The CI container runs with a private network namespace and
|
||||||
reaches the forge through the gateway vhost on the bridge IP.
|
reaches the forge through the gateway vhost on the bridge IP.
|
||||||
Set behindGateway = true (it defaults to true alongside
|
Set behindGateway = true (it defaults to true alongside
|
||||||
|
|
@ -216,8 +217,8 @@ in
|
||||||
# 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 `forge.behindGateway = true` (asserted in the options
|
# Requires `deploy.forgejo.behindGateway = true` (asserted in the
|
||||||
# block above). See docs/networking/network.md.
|
# options block above). See docs/networking/network.md.
|
||||||
privateNetwork = true;
|
privateNetwork = true;
|
||||||
hostBridge = networkCfg.bridgeName;
|
hostBridge = networkCfg.bridgeName;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ let
|
||||||
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
# links. Operators can still override via `cfg.rootUrl` for bespoke
|
||||||
# shapes.
|
# shapes.
|
||||||
defaultRootUrl =
|
defaultRootUrl =
|
||||||
if cfg.behindGateway then
|
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
|
||||||
|
|
@ -98,9 +98,9 @@ let
|
||||||
# operator hasn't already declared that dest themselves (else CI-on +
|
# operator hasn't already declared that dest themselves (else CI-on +
|
||||||
# an explicit `actions/checkout` entry would duplicate it).
|
# an explicit `actions/checkout` entry would duplicate it).
|
||||||
effectiveMirrors =
|
effectiveMirrors =
|
||||||
cfg.mirrors
|
deployCfg.forgejo.mirrors
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) deployCfg.forgejo.mirrors)
|
||||||
) actionCheckoutMirror;
|
) actionCheckoutMirror;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -133,7 +133,7 @@ in
|
||||||
Removed rather than defaulted to true so a config that turned it
|
Removed rather than defaulted to true so a config that turned it
|
||||||
OFF fails here, where the line is, instead of silently gaining a
|
OFF fails here, where the line is, instead of silently gaining a
|
||||||
login provider on the next rebuild. Drop the line; if it was
|
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.
|
services.hyperhive.swarm.authelia.url as the assertions describe.
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
@ -178,7 +178,7 @@ 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 `behindGateway = true`
|
gateway vhost server-name when `deploy.forgejo.behindGateway = true`
|
||||||
(sub-domain routing — see `docs/networking/gateway.md`).
|
(sub-domain routing — see `docs/networking/gateway.md`).
|
||||||
|
|
||||||
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
Defaults to `forge.''${services.hyperhive.swarm.domain}` — the
|
||||||
|
|
@ -201,7 +201,10 @@ in
|
||||||
publicUrl = lib.mkOption {
|
publicUrl = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default =
|
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 ''
|
defaultText = lib.literalExpression ''
|
||||||
if behindGateway then "https://''${domain}" else null
|
if behindGateway then "https://''${domain}" else null
|
||||||
'';
|
'';
|
||||||
|
|
@ -212,7 +215,7 @@ 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 `behindGateway =
|
Defaults to `https://''${cfg.domain}` when `deploy.forgejo.behindGateway =
|
||||||
true` (the gateway vhost is genuinely reachable at that URL)
|
true` (the gateway vhost is genuinely reachable at that URL)
|
||||||
and `null` otherwise. When `null`, the dashboard **hides**
|
and `null` otherwise. When `null`, the dashboard **hides**
|
||||||
forge links rather than guessing one — see
|
forge links rather than guessing one — see
|
||||||
|
|
@ -221,7 +224,7 @@ in
|
||||||
container port is only an accident away from wrong on any
|
container port is only an accident away from wrong on any
|
||||||
deployment that isn't plain localhost).
|
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
|
forge is still reachable at a stable URL you want linked from
|
||||||
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
|
the dashboard (e.g. `http://<lan-host>:''${toString cfg.httpPort}`
|
||||||
for an all-LAN deployment) — leaving it unset there means the
|
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 {
|
behindGateway = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = config.services.hyperhive.enable;
|
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 {
|
openFirewall = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
|
|
@ -312,8 +346,8 @@ in
|
||||||
|
|
||||||
**Breaking change**: this used to default to `true`. If you
|
**Breaking change**: this used to default to `true`. If you
|
||||||
relied on the old default for external reach, add
|
relied on the old default for external reach, add
|
||||||
`services.hyperhive.swarm.forge.openFirewall = true;` to your host
|
`services.hyperhive.deploy.forgejo.openFirewall = true;` to your
|
||||||
config before rebuilding.
|
host config before rebuilding.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -360,45 +394,27 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# The swarm's authelia is always registered as an OpenID Connect
|
# The other half of the SSO pair: the client *id* is swarm-wide (it has
|
||||||
# login source here — there is no toggle, for the same reason the
|
# to match authelia's register), the secret is a path on this host. The
|
||||||
# forge itself has none.
|
# design note for the login source itself is with the id.
|
||||||
#
|
sso.clientSecretFile = lib.mkOption {
|
||||||
# **Additive, never exclusive.** Forgejo keeps its local password
|
type = lib.types.nullOr lib.types.str;
|
||||||
# database and gains an extra "sign in with" button; this does not
|
default = null;
|
||||||
# disable local login. Deliberate: an identity provider that can take
|
example = "/var/lib/hyperhive/forge-oidc-secret";
|
||||||
# the forge offline when it hiccups is a worse forge than one with
|
description = ''
|
||||||
# two ways in — which is also what makes always-on safe.
|
Path **inside the forge container** holding the client
|
||||||
sso = {
|
secret's plaintext.
|
||||||
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`.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
clientSecretFile = lib.mkOption {
|
A path, never a value: an OIDC client secret has two holders
|
||||||
type = lib.types.nullOr lib.types.str;
|
in two containers (authelia keeps a hash, this forge needs the
|
||||||
default = null;
|
plaintext), and a literal written here would be rendered into
|
||||||
example = "/var/lib/hyperhive/forge-oidc-secret";
|
the world-readable nix store.
|
||||||
description = ''
|
|
||||||
Path **inside the forge container** holding the client
|
|
||||||
secret's plaintext.
|
|
||||||
|
|
||||||
A path, never a value: an OIDC client secret has two holders
|
Required when `enable` is set — deliberately no fallback. A
|
||||||
in two containers (authelia keeps a hash, this forge needs the
|
forge that boots with SSO half-configured presents as a login
|
||||||
plaintext), and a literal written here would be rendered into
|
button that always fails, which is harder to diagnose than an
|
||||||
the world-readable nix store.
|
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 {
|
hostSwarmControllerTokenFile = lib.mkOption {
|
||||||
|
|
@ -443,13 +459,13 @@ in
|
||||||
# Both halves are gated on `behindGateway`: with it off the operator
|
# Both halves are gated on `behindGateway`: with it off the operator
|
||||||
# fronts forgejo themselves, so this hive must neither claim the
|
# fronts forgejo themselves, so this hive must neither claim the
|
||||||
# vhost nor answer DNS for it.
|
# 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
|
# This swarm-ui quick-links entry, same `behindGateway` guard as the
|
||||||
# vhost/DNS name above — with it off, this host doesn't actually
|
# vhost/DNS name above — with it off, this host doesn't actually
|
||||||
# serve `cfg.domain`, so linking to it would be dead. See
|
# 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 cfg.behindGateway {
|
services.hyperhive.swarm.controller.links = lib.optional deployCfg.forgejo.behindGateway {
|
||||||
label = "Forge";
|
label = "Forge";
|
||||||
icon = "⚒";
|
icon = "⚒";
|
||||||
url = "https://${cfg.domain}/";
|
url = "https://${cfg.domain}/";
|
||||||
|
|
@ -466,15 +482,17 @@ in
|
||||||
# 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 = lib.optionalAttrs cfg.behindGateway {
|
services.hyperhive.swarm.otel.publishedScrapeTargets =
|
||||||
forgejo = "https://${cfg.domain}/metrics";
|
lib.optionalAttrs deployCfg.forgejo.behindGateway
|
||||||
};
|
{
|
||||||
|
forgejo = "https://${cfg.domain}/metrics";
|
||||||
|
};
|
||||||
|
|
||||||
# `server_name = forge.domain`, proxies all `/` → forgejo. Tuned for
|
# `server_name = forge.domain`, proxies all `/` → forgejo. Tuned for
|
||||||
# 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 cfg.behindGateway {
|
services.nginx.virtualHosts = lib.optionalAttrs deployCfg.forgejo.behindGateway {
|
||||||
"${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;
|
||||||
|
|
@ -543,10 +561,10 @@ in
|
||||||
{
|
{
|
||||||
# Fail at EVAL, not at boot. The alternative failure is a login
|
# Fail at EVAL, not at boot. The alternative failure is a login
|
||||||
# button that always 401s, three layers from the missing file.
|
# button that always 401s, three layers from the missing file.
|
||||||
assertion = cfg.sso.clientSecretFile != null;
|
assertion = deployCfg.forgejo.sso.clientSecretFile != null;
|
||||||
message = ''
|
message = ''
|
||||||
The forge's SSO login source requires
|
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
|
(inside the forge container) holding the OIDC client secret's
|
||||||
plaintext.
|
plaintext.
|
||||||
|
|
||||||
|
|
@ -600,7 +618,7 @@ in
|
||||||
# splits on the single slash to create the org + repo.
|
# splits on the single slash to create the org + repo.
|
||||||
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
|
assertion = lib.all (m: lib.length (lib.splitString "/" m.dest) == 2) effectiveMirrors;
|
||||||
message = ''
|
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}
|
"<owner>/<repo>" (one slash). Got: ${lib.concatMapStringsSep ", " (m: m.dest) effectiveMirrors}
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
@ -618,7 +636,7 @@ in
|
||||||
])
|
])
|
||||||
) effectiveMirrors;
|
) effectiveMirrors;
|
||||||
message = ''
|
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) —
|
in a hive-c0re-managed org (config / shared / agents / core) —
|
||||||
those are provisioned by hive-c0re and a mirror there would
|
those are provisioned by hive-c0re and a mirror there would
|
||||||
collide. Use a dedicated org (e.g. "actions/checkout").
|
collide. Use a dedicated org (e.g. "actions/checkout").
|
||||||
|
|
@ -771,7 +789,7 @@ in
|
||||||
# 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 = cfg.behindGateway;
|
metrics.ENABLED = deployCfg.forgejo.behindGateway;
|
||||||
# The two per-dimension breakdowns, on the same condition as
|
# The two per-dimension breakdowns, on the same condition as
|
||||||
# the endpoint itself: `gitea_issues_by_label{label=…}` and
|
# the endpoint itself: `gitea_issues_by_label{label=…}` and
|
||||||
# `gitea_issues_by_repository{repository=…}`. Off by default
|
# `gitea_issues_by_repository{repository=…}`. Off by default
|
||||||
|
|
@ -790,8 +808,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 = cfg.behindGateway;
|
metrics.ENABLED_ISSUE_BY_LABEL = deployCfg.forgejo.behindGateway;
|
||||||
metrics.ENABLED_ISSUE_BY_REPOSITORY = cfg.behindGateway;
|
metrics.ENABLED_ISSUE_BY_REPOSITORY = deployCfg.forgejo.behindGateway;
|
||||||
# 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
|
||||||
|
|
@ -1025,9 +1043,9 @@ in
|
||||||
script = ''
|
script = ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
secret=$(cat ${lib.escapeShellArg cfg.sso.clientSecretFile})
|
secret=$(cat ${lib.escapeShellArg deployCfg.forgejo.sso.clientSecretFile})
|
||||||
if [ -z "$secret" ]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -1204,7 +1222,7 @@ in
|
||||||
|
|
||||||
# Same case, same reasoning: this host minted the secret, so it can
|
# Same case, same reasoning: this host minted the secret, so it can
|
||||||
# say where the forge will find it.
|
# 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
|
lib.mkDefault forgeSecretPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1298,7 +1316,7 @@ in
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
src=${lib.escapeShellArg "/var/lib/nixos-containers/hive-forge${swarmControllerTokenPath}"}
|
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
|
for _ in $(seq 1 60); do
|
||||||
[ -s "$src" ] && break
|
[ -s "$src" ] && break
|
||||||
|
|
@ -1313,7 +1331,7 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
networking.firewall = lib.mkIf deployCfg.forgejo.openFirewall {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
cfg.httpPort
|
cfg.httpPort
|
||||||
cfg.sshPort
|
cfg.sshPort
|
||||||
|
|
|
||||||
|
|
@ -1361,7 +1361,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 forgeCfg.behindGateway metricsRule
|
lib.optional deployCfg.forgejo.behindGateway 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) {
|
||||||
|
|
|
||||||
|
|
@ -413,8 +413,8 @@ in
|
||||||
|
|
||||||
forgeTokenFile = lib.mkOption {
|
forgeTokenFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
# `services.hyperhive.swarm.forge` has no `enable` of its own to
|
# The forge has no `enable` of its own to check — neither half of
|
||||||
# check — the module activates on the general
|
# its split namespace carries one — the module activates on the general
|
||||||
# `config.services.hyperhive.enable` instead (see
|
# `config.services.hyperhive.enable` instead (see
|
||||||
# `hive-forge/default.nix`'s own `config = lib.mkIf
|
# `hive-forge/default.nix`'s own `config = lib.mkIf
|
||||||
# config.services.hyperhive.enable { ... }`), so that's the
|
# config.services.hyperhive.enable { ... }`), so that's the
|
||||||
|
|
@ -423,7 +423,8 @@ in
|
||||||
# that turns swarm-controller on — caught in review, not by
|
# that turns swarm-controller on — caught in review, not by
|
||||||
# `nix flake check` (nothing in its checked combinations forced
|
# `nix flake check` (nothing in its checked combinations forced
|
||||||
# this particular default to actually evaluate).
|
# this particular default to actually evaluate).
|
||||||
default = if config.services.hyperhive.enable then forgeCfg.hostSwarmControllerTokenFile else null;
|
default =
|
||||||
|
if config.services.hyperhive.enable then deployCfg.forgejo.hostSwarmControllerTokenFile else null;
|
||||||
defaultText = lib.literalExpression ''
|
defaultText = lib.literalExpression ''
|
||||||
forge's own `hostSwarmControllerTokenFile` when this host runs
|
forge's own `hostSwarmControllerTokenFile` when this host runs
|
||||||
hyperhive at all (forge has no separate enable), else null
|
hyperhive at all (forge has no separate enable), else null
|
||||||
|
|
@ -440,7 +441,7 @@ in
|
||||||
`hive-forge/default.nix`, it has no `enable` of its own).
|
`hive-forge/default.nix`, it has no `enable` of its own).
|
||||||
Override explicitly if forge's actual token file ends up
|
Override explicitly if forge's actual token file ends up
|
||||||
somewhere else — copy it out of forge's
|
somewhere else — copy it out of forge's
|
||||||
{option}`services.hyperhive.swarm.forge.hostSwarmControllerTokenFile`
|
{option}`services.hyperhive.deploy.forgejo.hostSwarmControllerTokenFile`
|
||||||
with whatever secret management this deployment already uses,
|
with whatever secret management this deployment already uses,
|
||||||
the same shape `swarm.nix`'s `clientSecretFile` documents for
|
the same shape `swarm.nix`'s `clientSecretFile` documents for
|
||||||
its own cross-host case. `null` means no forge access — the
|
its own cross-host case. `null` means no forge access — the
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,23 @@ let
|
||||||
swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key";
|
swarm.wireguard.privateKeyFile = "/etc/wireguard/hive.key";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Same shape for the forge, which SPLIT rather than moving whole: the five
|
||||||
|
# host-side options are set here through their pre-rename paths while the
|
||||||
|
# rest of `swarm.forge` stays put. All five are defined so that dropping any
|
||||||
|
# single shim entry fails the eval, not just the two the assertion reads.
|
||||||
|
forgeOldPath = hive {
|
||||||
|
swarm.forge.behindGateway = true;
|
||||||
|
swarm.forge.openFirewall = true;
|
||||||
|
swarm.forge.hostSwarmControllerTokenFile = "/etc/forge/sc.token";
|
||||||
|
swarm.forge.sso.clientSecretFile = "/etc/forge/oidc.secret";
|
||||||
|
swarm.forge.mirrors = [
|
||||||
|
{
|
||||||
|
upstream = "https://example.invalid/tool";
|
||||||
|
dest = "mirrors/tool";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
baoPkcs11 = hive {
|
baoPkcs11 = hive {
|
||||||
deploy.bao.enable = true;
|
deploy.bao.enable = true;
|
||||||
deploy.bao.seal = "pkcs11";
|
deploy.bao.seal = "pkcs11";
|
||||||
|
|
@ -205,6 +222,20 @@ let
|
||||||
in
|
in
|
||||||
wg.ips == [ "10.100.0.1/24" ] && wg.privateKeyFile == "/etc/wireguard/hive.key";
|
wg.ips == [ "10.100.0.1/24" ] && wg.privateKeyFile == "/etc/wireguard/hive.key";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# Same reasoning one namespace over, plus the shape the mesh did not
|
||||||
|
# have: `mirrors` is a single option of a list-of-submodule type, so its
|
||||||
|
# one rename entry has to carry a whole compound value rather than a
|
||||||
|
# scalar. Both arms read a rendered effect — the host firewall and the
|
||||||
|
# env var c0re seeds mirrors from — not the option.
|
||||||
|
name = "a config written against the pre-rename forge paths still opens the firewall and seeds the mirror";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
ports = forgeOldPath.networking.firewall.allowedTCPPorts;
|
||||||
|
seeded = builtins.fromJSON forgeOldPath.systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS;
|
||||||
|
in
|
||||||
|
builtins.elem 3000 ports && builtins.any (m: m.dest == "mirrors/tool") seeded;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# The gateway's per-name issuer choice. If this ever collapses to a
|
# The gateway's per-name issuer choice. If this ever collapses to a
|
||||||
# constant, every swarm-service vhost serves a certificate its CA
|
# constant, every swarm-service vhost serves a certificate its CA
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue