Watch
0
0
Fork
You've already forked hyperhive
0

keep the mirror hint off forge errors, forward the hyperhive mirror from any controller host

This commit is contained in:
atlas 2026-11-09 13:07:41 +01:00
commit 52cdfe84a0
5 changed files with 70 additions and 36 deletions

View file

@ -89,9 +89,9 @@ Run `nixos-rebuild switch --flake .#my-host`. The swarm UI then answers at
The swarm controller pins the hive's flake inputs to a commit of hyperhive on The swarm controller pins the hive's flake inputs to a commit of hyperhive on
the swarm's own forge, so the forge needs a `hyperhive/hyperhive` repo before the swarm's own forge, so the forge needs a `hyperhive/hyperhive` repo before
the first agent builds. On a host that runs the forge and the controller, the first agent builds. The controller creates it as a pull-mirror of
the controller creates it as a pull-mirror of `https://forge.darkest.space/hyperhive/hyperhive`, on the swarm's forge wherever
`https://forge.darkest.space/hyperhive/hyperhive`. You run no extra step. that runs. You run no extra step.
| option | what it does | | option | what it does |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

View file

@ -1,13 +1,15 @@
# Glue: wire the forge and the swarm-controller together when one host runs both. # Glue: wire the swarm-controller's forge seeding to the forge options.
# #
# ONE PAIRING PER FILE — forge ← swarm-controller, and nothing else. Deleting # ONE PAIRING PER FILE — forge ← swarm-controller, and nothing else. Deleting
# this leaves the controller seeding no pull-mirrors, the forge resolving # this leaves the controller seeding no pull-mirrors, the forge resolving
# workflow actions from `data.forgejo.org` and a fresh swarm without the hyperhive # workflow actions from `data.forgejo.org` and a fresh swarm without the hyperhive
# repo its meta flake pins. # repo its meta flake pins.
# #
# ⚠️ Gated on BOTH services being here: the mirrors reach a controller on this # ⚠️ The mirror list is forwarded wherever the controller runs: it creates the
# host only, and the forge's own `config` block hangs off # mirrors through the forge's API, so the forge may be on another host. The
# `deploy.forgejo.enable`. The forge's `mirrors` warning covers the split case. # list is that of the controller's own host; the forge's `mirrors` warning
# covers a list declared on a forge host without one. `actionsFromSelf` changes
# the forge's own settings, so it follows only a forge beside the controller.
{ {
lib, lib,
config, config,
@ -17,14 +19,12 @@ let
deployCfg = config.services.hyperhive.deploy; deployCfg = config.services.hyperhive.deploy;
in in
{ {
config = lib.mkIf (deployCfg.forgejo.enable && deployCfg.swarm-controller.enable) { config = lib.mkMerge [
# The controller seeds the `actions/*` mirrors, so a forge beside one can (lib.mkIf deployCfg.swarm-controller.enable {
# resolve actions from itself. `mkDefault`: an operator's explicit value wins. # The meta flake pins the hyperhive repo on the swarm's forge, and a fresh
services.hyperhive.deploy.forgejo.actionsFromSelf = lib.mkDefault true; # forge has none. The controller creates the mirror only if
# `hyperhive/hyperhive` is absent, so a swarm that hosts the real repo is
# The meta flake pins the hyperhive repo on this forge, and a fresh forge has # not touched. `mkDefault`: an operator's explicit value wins.
# none. The controller creates the mirror only if `hyperhive/hyperhive` is
# absent, so a swarm that hosts the real repo is not touched.
services.hyperhive.deploy.forgejo.hyperhiveUpstream = services.hyperhive.deploy.forgejo.hyperhiveUpstream =
lib.mkDefault "https://forge.darkest.space/hyperhive/hyperhive"; lib.mkDefault "https://forge.darkest.space/hyperhive/hyperhive";
@ -32,5 +32,12 @@ in
# forge-objects pass (`swarm-controller/src/forge/objects.rs`). # forge-objects pass (`swarm-controller/src/forge/objects.rs`).
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS = systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS =
builtins.toJSON deployCfg.forgejo.effectiveMirrors; builtins.toJSON deployCfg.forgejo.effectiveMirrors;
}; })
(lib.mkIf (deployCfg.forgejo.enable && deployCfg.swarm-controller.enable) {
# The controller seeds the `actions/*` mirrors, so a forge beside one can
# resolve actions from itself. `mkDefault`: an operator's explicit value wins.
services.hyperhive.deploy.forgejo.actionsFromSelf = lib.mkDefault true;
})
];
} }

View file

@ -171,6 +171,9 @@ in
imports = [ imports = [
./service.nix ./service.nix
# Set on every host, not only the forge's: the controller's host forwards the
# list whether or not the forge runs beside it.
{ services.hyperhive.deploy.forgejo.effectiveMirrors = effectiveMirrors; }
(lib.mkRemovedOptionModule [ "services" "hyperhive" "deploy" "forgejo" "sso" "clientSecretFile" ] '' (lib.mkRemovedOptionModule [ "services" "hyperhive" "deploy" "forgejo" "sso" "clientSecretFile" ] ''
The forge's OIDC client secret is fetched from the swarm secret store The forge's OIDC client secret is fetched from the swarm secret store
(swarm-bao-forge-oidc) into a fixed runtime path. Remove this (swarm-bao-forge-oidc) into a fixed runtime path. Remove this
@ -311,12 +314,13 @@ in
build fetches it from there, so without it the first agent's build fetches it from there, so without it the first agent's
`update_meta` step fails and no agent container is built. `update_meta` step fails and no agent container is built.
Defaults to `https://forge.darkest.space/hyperhive/hyperhive` when this Defaults to `https://forge.darkest.space/hyperhive/hyperhive` on a host
host also runs the swarm-controller, which seeds the mirror (set by that runs the swarm-controller, which seeds the mirror on the swarm's
`glue-forge-controller.nix`), and to `null` otherwise. The controller forge wherever that runs (set by `glue-forge-controller.nix`), and to
creates the mirror only when `hyperhive/hyperhive` does not exist: a `null` otherwise. The controller creates the mirror only when
swarm that already holds the repo as a normal repo keeps it unchanged. A `hyperhive/hyperhive` entry in `hyperhive/hyperhive` does not exist: a swarm that already holds the
{option}`services.hyperhive.deploy.forgejo.mirrors` takes precedence. repo as a normal repo keeps it unchanged. A `hyperhive/hyperhive` entry
in {option}`services.hyperhive.deploy.forgejo.mirrors` takes precedence.
''; '';
}; };
@ -1400,7 +1404,6 @@ in
# ../glue-forge-controller.nix); a controller elsewhere reads its own # ../glue-forge-controller.nix); a controller elsewhere reads its own
# host's list, so the warning below says so at eval rather than leaving # host's list, so the warning below says so at eval rather than leaving
# it to be noticed as a missing repo. # it to be noticed as a missing repo.
services.hyperhive.deploy.forgejo.effectiveMirrors = effectiveMirrors;
warnings = lib.optional (effectiveMirrors != [ ] && !deployCfg.swarm-controller.enable) '' warnings = lib.optional (effectiveMirrors != [ ] && !deployCfg.swarm-controller.enable) ''
services.hyperhive.deploy.forgejo.mirrors (or the actions/* and hyperhive mirrors services.hyperhive.deploy.forgejo.mirrors (or the actions/* and hyperhive mirrors
deploy.forgejo.actionsFromSelf and hyperhiveUpstream add) is set on a host that does not run the deploy.forgejo.actionsFromSelf and hyperhiveUpstream add) is set on a host that does not run the

View file

@ -1410,19 +1410,20 @@ let
# forge has none, so the controller seeds a mirror of it: by default, # forge has none, so the controller seeds a mirror of it: by default,
# with another upstream when the option says so, and not at all when it # with another upstream when the option says so, and not at all when it
# is null (controllerWithMirrors) or without the controller (forgeOnly). # is null (controllerWithMirrors) or without the controller (forgeOnly).
name = "a forge beside the controller mirrors hyperhive/hyperhive unless told not to"; # controllerAlone has the forge on another host and still gets the default.
name = "the controller mirrors hyperhive/hyperhive on its forge unless told not to";
ok = ok =
let let
mirrorsOf = mirrorsOf =
m: builtins.fromJSON m.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS; m: builtins.fromJSON m.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS;
hyperhiveOf = m: lib.filter (e: e.dest == "hyperhive/hyperhive") (mirrorsOf m); hyperhiveOf = m: lib.filter (e: e.dest == "hyperhive/hyperhive") (mirrorsOf m);
in defaultHyperhive = {
hyperhiveOf forgeWithController == [
{
upstream = "https://forge.darkest.space/hyperhive/hyperhive"; upstream = "https://forge.darkest.space/hyperhive/hyperhive";
dest = "hyperhive/hyperhive"; dest = "hyperhive/hyperhive";
} };
] in
hyperhiveOf forgeWithController == [ defaultHyperhive ]
&& hyperhiveOf controllerAlone == [ defaultHyperhive ]
&& map (e: e.upstream) (hyperhiveOf controllerWithSelfActions) == [ hyperhiveElsewhere ] && map (e: e.upstream) (hyperhiveOf controllerWithSelfActions) == [ hyperhiveElsewhere ]
&& hyperhiveOf controllerWithMirrors == [ ] && hyperhiveOf controllerWithMirrors == [ ]
&& forgeOnly.services.hyperhive.deploy.forgejo.hyperhiveUpstream == null; && forgeOnly.services.hyperhive.deploy.forgejo.hyperhiveUpstream == null;

View file

@ -374,8 +374,7 @@ async fn hyperhive_pin(
let Some(flake) = committed_flake else { let Some(flake) = committed_flake else {
let head = forge let head = forge
.head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, None, &[]) .head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, None, &[])
.await .await?
.with_context(missing_hyperhive)?
.with_context(missing_hyperhive)?; .with_context(missing_hyperhive)?;
return Ok(HyperhivePin { return Ok(HyperhivePin {
rev: head.commit, rev: head.commit,
@ -575,6 +574,8 @@ mod tests {
compares: Mutex<Vec<(String, String)>>, compares: Mutex<Vec<(String, String)>>,
/// Every compare fails, as a forge that is down does. /// Every compare fails, as a forge that is down does.
compare_fails: bool, compare_fails: bool,
/// Reads of this `owner/repo` fail, as on a forge that refuses or is down.
head_fails: Option<String>,
/// Every `head_files` read, as `("owner/repo", branch)`. /// Every `head_files` read, as `("owner/repo", branch)`.
head_reads: Mutex<Vec<(String, Option<String>)>>, head_reads: Mutex<Vec<(String, Option<String>)>>,
} }
@ -603,6 +604,9 @@ mod tests {
branch: Option<&str>, branch: Option<&str>,
paths: &[&str], paths: &[&str],
) -> Result<Option<DefaultBranchFiles>> { ) -> Result<Option<DefaultBranchFiles>> {
if self.head_fails.as_deref() == Some(&format!("{owner}/{repo}")) {
anyhow::bail!("forge answered 503");
}
self.head_reads self.head_reads
.lock() .lock()
.unwrap() .unwrap()
@ -874,6 +878,25 @@ mod tests {
assert!(forge.commits.lock().unwrap().is_empty()); assert!(forge.commits.lock().unwrap().is_empty());
} }
#[tokio::test]
async fn a_forge_error_reading_hyperhive_does_not_carry_the_mirror_hint() {
let mut forge = forge_with(&[]);
forge.head_fails = Some("hyperhive/hyperhive".to_owned());
let err = update(
&forge,
&FakeLocker::locked(),
&settings(),
None,
&argus_and_reviewer(),
&[],
)
.await
.unwrap_err();
let err = format!("{err:#}");
assert!(err.contains("forge answered 503"), "{err}");
assert!(!err.contains("hyperhiveUpstream"), "{err}");
}
#[tokio::test] #[tokio::test]
async fn a_refused_lock_commits_nothing() { async fn a_refused_lock_commits_nothing() {
let forge = forge_with(&[]); let forge = forge_with(&[]);