seed a hyperhive/hyperhive mirror so update_meta finds it on a fresh swarm
This commit is contained in:
parent
dbdbed4dcc
commit
a6b5bafa83
10 changed files with 173 additions and 15 deletions
|
|
@ -85,6 +85,21 @@ services.hyperhive = {
|
|||
Run `nixos-rebuild switch --flake .#my-host`. The swarm UI then answers at
|
||||
`https://<swarm.domain>/`.
|
||||
|
||||
### The hyperhive mirror
|
||||
|
||||
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 first agent builds. On a host that runs the forge and the controller,
|
||||
the controller creates it as a pull-mirror of
|
||||
`https://forge.darkest.space/hyperhive/hyperhive`. You run no extra step.
|
||||
|
||||
| option | what it does |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `deploy.forgejo.hyperhiveUpstream` | The clone URL the mirror pulls from. Set another URL to mirror a fork, or `null` to seed nothing and provide the repo yourself. The controller creates the mirror only if `hyperhive/hyperhive` doesn't exist, so a repo already there stays as it is. |
|
||||
|
||||
Without the repo, the first agent's `update_meta` job step fails with
|
||||
`hyperhive/hyperhive is empty or absent on the swarm forge`.
|
||||
|
||||
## Everything else is optional
|
||||
|
||||
Leave the other options at their defaults until you need them:
|
||||
|
|
|
|||
|
|
@ -149,7 +149,10 @@ swarmctl agent create iris --hive firsthive
|
|||
|
||||
The controller provisions iris's identity, forge user and config repo
|
||||
(`agent-configs/iris`, from the default template), then has the hive build
|
||||
and start the container. It returns once the scheduler queues the job — watch the
|
||||
and start the container. The hive builds from a copy of hyperhive on the swarm's own forge: the
|
||||
controller mirrors it there as `hyperhive/hyperhive` on a fresh swarm, so the
|
||||
first agent needs no extra step (see
|
||||
[`nixos.md`](nixos.md#the-hyperhive-mirror)). It returns once the scheduler queues the job — watch the
|
||||
swarm UI's job view for progress.
|
||||
|
||||
Later config changes are PRs on `agent-configs/iris`; you merge them on the
|
||||
|
|
|
|||
|
|
@ -412,7 +412,8 @@ one per hive. It ensures them at start and every five minutes after
|
|||
repo with no `main` rule alone;
|
||||
- the pull-mirrors from `deploy.forgejo.mirrors` on the controller's
|
||||
host (with the `actions/checkout` and `actions/upload-artifact` ones
|
||||
`deploy.forgejo.actionsFromSelf` adds);
|
||||
`deploy.forgejo.actionsFromSelf` adds, and `hyperhive/hyperhive` from
|
||||
`deploy.forgejo.hyperhiveUpstream`);
|
||||
- `internal/docs` (private) and `internal/knowledge` (public, with a
|
||||
seed `README.md` while empty);
|
||||
- the `agent-configs` org avatar
|
||||
|
|
|
|||
|
|
@ -251,6 +251,14 @@ environment (JSON-encoded `[{upstream, dest}]` list). It reads the list of
|
|||
the host the controller runs on: mirrors declared on any other host aren't
|
||||
seeded, and evaluation warns about them.
|
||||
|
||||
**The hyperhive mirror**: the same list carries `hyperhive/hyperhive`, from
|
||||
`services.hyperhive.deploy.forgejo.hyperhiveUpstream` (default
|
||||
`https://forge.darkest.space/hyperhive/hyperhive` on a host that runs the
|
||||
controller, unset elsewhere). The controller creates the mirror only when
|
||||
`hyperhive/hyperhive` doesn't exist, and it leaves a repo that exists but is
|
||||
not a mirror unchanged. `deploy.forgejo.mirrors` takes precedence for the same
|
||||
`dest`.
|
||||
|
||||
**General-purpose mirrors**: you can pre-seed any external repo as a
|
||||
pull-mirror via `services.hyperhive.deploy.forgejo.mirrors`:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# Glue: wire the forge and the swarm-controller together when one host runs both.
|
||||
#
|
||||
# ONE PAIRING PER FILE — forge ← swarm-controller, and nothing else. Deleting
|
||||
# this leaves the controller seeding no pull-mirrors and the forge resolving
|
||||
# workflow actions from `data.forgejo.org`.
|
||||
# this leaves the controller seeding no pull-mirrors, the forge resolving
|
||||
# workflow actions from `data.forgejo.org` and a fresh swarm without the hyperhive
|
||||
# repo its meta flake pins.
|
||||
#
|
||||
# ⚠️ Gated on BOTH services being here: the mirrors reach a controller on this
|
||||
# host only, and the forge's own `config` block hangs off
|
||||
|
|
@ -21,6 +22,12 @@ in
|
|||
# resolve actions from itself. `mkDefault`: an operator's explicit value wins.
|
||||
services.hyperhive.deploy.forgejo.actionsFromSelf = lib.mkDefault true;
|
||||
|
||||
# The meta flake pins the hyperhive repo on this forge, and a fresh forge has
|
||||
# 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 =
|
||||
lib.mkDefault "https://forge.darkest.space/hyperhive/hyperhive";
|
||||
|
||||
# JSON-encoded list of { upstream, dest }, seeded by the controller's
|
||||
# forge-objects pass (`swarm-controller/src/forge/objects.rs`).
|
||||
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS =
|
||||
|
|
|
|||
|
|
@ -143,11 +143,17 @@ let
|
|||
"checkout"
|
||||
"upload-artifact"
|
||||
];
|
||||
# The swarm-controller reads the hyperhive repo from this forge (the pin of
|
||||
# the meta flake), so a swarm that has no copy mirrors it.
|
||||
hyperhiveMirrors = lib.optional (deployCfg.forgejo.hyperhiveUpstream != null) {
|
||||
upstream = deployCfg.forgejo.hyperhiveUpstream;
|
||||
dest = "hyperhive/hyperhive";
|
||||
};
|
||||
# Minus any dest the operator already declared, which would duplicate it.
|
||||
effectiveMirrors =
|
||||
deployCfg.forgejo.mirrors
|
||||
++ lib.optionals actionsFromSelf (
|
||||
lib.filter (a: !(lib.any (m: m.dest == a.dest) deployCfg.forgejo.mirrors)) actionMirrors
|
||||
++ lib.filter (a: !(lib.any (m: m.dest == a.dest) deployCfg.forgejo.mirrors)) (
|
||||
lib.optionals actionsFromSelf actionMirrors ++ hyperhiveMirrors
|
||||
);
|
||||
|
||||
# Share host netns — forgejo's HTTP / SSH listeners then look
|
||||
|
|
@ -266,7 +272,8 @@ in
|
|||
With {option}`services.hyperhive.deploy.forgejo.actionsFromSelf`
|
||||
enabled (the default on a host that runs the swarm-controller),
|
||||
`actions/checkout` and `actions/upload-artifact` mirrors are appended
|
||||
to this list.
|
||||
to this list, and so is `hyperhive/hyperhive` with
|
||||
{option}`services.hyperhive.deploy.forgejo.hyperhiveUpstream`.
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -293,13 +300,33 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
hyperhiveUpstream = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "https://forge.darkest.space/hyperhive/hyperhive";
|
||||
description = ''
|
||||
Clone URL of the hyperhive repo to pull-mirror to `hyperhive/hyperhive`
|
||||
on this forge, or `null` for no mirror. The swarm-controller pins the
|
||||
meta flake's `hyperhive` input to a commit of that repo and every
|
||||
build fetches it from there, so without it the first agent's
|
||||
`update_meta` step fails and no agent container is built.
|
||||
|
||||
Defaults to `https://forge.darkest.space/hyperhive/hyperhive` when this
|
||||
host also runs the swarm-controller, which seeds the mirror (set by
|
||||
`glue-forge-controller.nix`), and to `null` otherwise. The controller
|
||||
creates the mirror only when `hyperhive/hyperhive` does not exist: a
|
||||
swarm that already holds the repo as a normal repo keeps it unchanged. A `hyperhive/hyperhive` entry in
|
||||
{option}`services.hyperhive.deploy.forgejo.mirrors` takes precedence.
|
||||
'';
|
||||
};
|
||||
|
||||
effectiveMirrors = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.attrs;
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
{option}`mirrors` plus the `actions/*` mirrors {option}`actionsFromSelf`
|
||||
adds; what `glue-forge-controller.nix` forwards to the controller.
|
||||
adds and the `hyperhive/hyperhive` mirror {option}`hyperhiveUpstream` adds; what `glue-forge-controller.nix` forwards to the controller.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
@ -1375,8 +1402,8 @@ in
|
|||
# it to be noticed as a missing repo.
|
||||
services.hyperhive.deploy.forgejo.effectiveMirrors = effectiveMirrors;
|
||||
warnings = lib.optional (effectiveMirrors != [ ] && !deployCfg.swarm-controller.enable) ''
|
||||
services.hyperhive.deploy.forgejo.mirrors (or the actions/* mirrors
|
||||
deploy.forgejo.actionsFromSelf adds) is set on a host that does not run the
|
||||
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
|
||||
swarm-controller. The controller seeds mirrors from its own host's list,
|
||||
so these are not seeded: declare them on the swarm-controller's host.
|
||||
'';
|
||||
|
|
|
|||
|
|
@ -118,11 +118,13 @@ let
|
|||
upstream = "https://example.invalid/tool";
|
||||
dest = "mirrors/tool";
|
||||
};
|
||||
hyperhiveElsewhere = "https://example.invalid/hyperhive";
|
||||
controllerWithMirrors = hiveWithController {
|
||||
deploy.swarm-controller.enable = true;
|
||||
deploy.forgejo.enable = true;
|
||||
deploy.forgejo.mirrors = [ aMirror ];
|
||||
deploy.forgejo.actionsFromSelf = false;
|
||||
deploy.forgejo.hyperhiveUpstream = null;
|
||||
deploy.swarm-controller.configOrgAvatarPng = "/etc/marker-avatar.png";
|
||||
};
|
||||
mirrorsNoController = hive {
|
||||
|
|
@ -139,6 +141,7 @@ let
|
|||
deploy.forgejo.enable = true;
|
||||
deploy.forgejo.mirrors = [ aMirror ];
|
||||
deploy.forgejo.actionsFromSelf = true;
|
||||
deploy.forgejo.hyperhiveUpstream = hyperhiveElsewhere;
|
||||
};
|
||||
|
||||
# The controller with every shared service on another host, and the same
|
||||
|
|
@ -1371,6 +1374,7 @@ let
|
|||
"mirrors/tool"
|
||||
"actions/checkout"
|
||||
"actions/upload-artifact"
|
||||
"hyperhive/hyperhive"
|
||||
]
|
||||
&& actionsUrlOf controllerWithSelfActions == "self"
|
||||
&& mirrorsOf controllerWithMirrors == [ aMirror ]
|
||||
|
|
@ -1401,6 +1405,28 @@ let
|
|||
&& actionsUrlOf forgeWithController == "self"
|
||||
&& lib.elem "actions/checkout" (map (m: m.dest) (mirrorsOf forgeWithController));
|
||||
}
|
||||
{
|
||||
# The meta flake pins the hyperhive repo on the swarm forge, and a fresh
|
||||
# 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
|
||||
# is null (controllerWithMirrors) or without the controller (forgeOnly).
|
||||
name = "a forge beside the controller mirrors hyperhive/hyperhive unless told not to";
|
||||
ok =
|
||||
let
|
||||
mirrorsOf =
|
||||
m: builtins.fromJSON m.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS;
|
||||
hyperhiveOf = m: lib.filter (e: e.dest == "hyperhive/hyperhive") (mirrorsOf m);
|
||||
in
|
||||
hyperhiveOf forgeWithController == [
|
||||
{
|
||||
upstream = "https://forge.darkest.space/hyperhive/hyperhive";
|
||||
dest = "hyperhive/hyperhive";
|
||||
}
|
||||
]
|
||||
&& map (e: e.upstream) (hyperhiveOf controllerWithSelfActions) == [ hyperhiveElsewhere ]
|
||||
&& hyperhiveOf controllerWithMirrors == [ ]
|
||||
&& forgeOnly.services.hyperhive.deploy.forgejo.hyperhiveUpstream == null;
|
||||
}
|
||||
{
|
||||
name = "the org avatar lands on the controller's unit, not hive-c0re's";
|
||||
ok =
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ pkgs.testers.runNixOSTest (
|
|||
deploy.hive-controller.enable = true;
|
||||
deploy.hive-controller.hiveName = hive;
|
||||
deploy.bao.seal = "shamir";
|
||||
# The test pushes its own hyperhive/hyperhive; a mirror of the
|
||||
# upstream could not be created offline and would race the push.
|
||||
deploy.forgejo.hyperhiveUpstream = null;
|
||||
swarm.domain = swarmDomain;
|
||||
swarm.hives.${hive} = { };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -311,6 +311,8 @@ struct TeamState {
|
|||
struct RepoState {
|
||||
private: bool,
|
||||
empty: bool,
|
||||
/// A pull-mirror. A plain repo at a mirror's `dest` is left as it is.
|
||||
mirror: bool,
|
||||
mirror_interval: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -485,7 +487,9 @@ fn plan(desired: &Desired, observed: &Observed) -> Vec<Action> {
|
|||
repo: m.repo.clone(),
|
||||
upstream: m.upstream.clone(),
|
||||
}),
|
||||
Seen::Present(s) if s.mirror_interval.as_deref() != Some(MIRROR_INTERVAL) => {
|
||||
Seen::Present(s)
|
||||
if s.mirror && s.mirror_interval.as_deref() != Some(MIRROR_INTERVAL) =>
|
||||
{
|
||||
actions.push(Action::SetMirrorInterval {
|
||||
owner: m.owner.clone(),
|
||||
repo: m.repo.clone(),
|
||||
|
|
@ -697,6 +701,7 @@ impl Client {
|
|||
|r| RepoState {
|
||||
private: r.private.unwrap_or(false),
|
||||
empty: r.empty.unwrap_or(false),
|
||||
mirror: r.mirror.unwrap_or(false),
|
||||
mirror_interval: r.mirror_interval,
|
||||
},
|
||||
);
|
||||
|
|
@ -1191,10 +1196,18 @@ mod tests {
|
|||
RepoState {
|
||||
private,
|
||||
empty,
|
||||
mirror: false,
|
||||
mirror_interval: interval.map(str::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
fn mirror_state(interval: Option<&str>) -> RepoState {
|
||||
RepoState {
|
||||
mirror: true,
|
||||
..repo_state(false, false, interval)
|
||||
}
|
||||
}
|
||||
|
||||
/// Every object of [`desired`] present with its desired settings.
|
||||
fn converged() -> Observed {
|
||||
let mut o = Observed::default();
|
||||
|
|
@ -1218,7 +1231,7 @@ mod tests {
|
|||
);
|
||||
o.repos.insert(
|
||||
key("actions", "checkout"),
|
||||
Seen::Present(repo_state(false, false, Some(MIRROR_INTERVAL))),
|
||||
Seen::Present(mirror_state(Some(MIRROR_INTERVAL))),
|
||||
);
|
||||
o.avatar_set = true;
|
||||
o
|
||||
|
|
@ -1299,7 +1312,7 @@ mod tests {
|
|||
);
|
||||
o.repos.insert(
|
||||
(s("actions"), s("checkout")),
|
||||
Seen::Present(repo_state(false, false, None)),
|
||||
Seen::Present(mirror_state(None)),
|
||||
);
|
||||
o.avatar_set = false;
|
||||
assert_eq!(
|
||||
|
|
@ -1329,6 +1342,28 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plain_repo_at_a_mirror_dest_is_left_alone() {
|
||||
let mut o = converged();
|
||||
o.repos.insert(
|
||||
(s("actions"), s("checkout")),
|
||||
Seen::Present(repo_state(false, false, None)),
|
||||
);
|
||||
assert_eq!(plan(&desired(), &o), Vec::new());
|
||||
// Control: the same repo as a mirror with no interval is patched.
|
||||
o.repos.insert(
|
||||
(s("actions"), s("checkout")),
|
||||
Seen::Present(mirror_state(None)),
|
||||
);
|
||||
assert_eq!(
|
||||
plan(&desired(), &o),
|
||||
vec![Action::SetMirrorInterval {
|
||||
owner: s("actions"),
|
||||
repo: s("checkout"),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreadable_object_gets_no_write() {
|
||||
let mut o = nothing();
|
||||
|
|
|
|||
|
|
@ -40,6 +40,15 @@ const REV_FILE: &str = "/etc/hyperhive/swarm-controller.json";
|
|||
const HYPERHIVE_OWNER: &str = "hyperhive";
|
||||
const HYPERHIVE_REPO: &str = "hyperhive";
|
||||
|
||||
/// The error for a swarm forge with no readable hyperhive repo, naming the nix
|
||||
/// option that mirrors one there.
|
||||
fn missing_hyperhive() -> String {
|
||||
format!(
|
||||
"{HYPERHIVE_OWNER}/{HYPERHIVE_REPO} is empty or absent on the swarm forge; \
|
||||
services.hyperhive.deploy.forgejo.hyperhiveUpstream mirrors it there"
|
||||
)
|
||||
}
|
||||
|
||||
/// What every agent of the swarm gets, the same on every hive.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -365,8 +374,9 @@ async fn hyperhive_pin(
|
|||
let Some(flake) = committed_flake else {
|
||||
let head = forge
|
||||
.head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, None, &[])
|
||||
.await?
|
||||
.with_context(|| format!("{HYPERHIVE_OWNER}/{HYPERHIVE_REPO} is empty or absent"))?;
|
||||
.await
|
||||
.with_context(missing_hyperhive)?
|
||||
.with_context(missing_hyperhive)?;
|
||||
return Ok(HyperhivePin {
|
||||
rev: head.commit,
|
||||
moved: false,
|
||||
|
|
@ -841,6 +851,29 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_forge_without_hyperhive_names_the_option_that_mirrors_it() {
|
||||
let mut forge = forge_with(&[]);
|
||||
forge.repos.remove("hyperhive/hyperhive");
|
||||
let err = update(
|
||||
&forge,
|
||||
&FakeLocker::locked(),
|
||||
&settings(),
|
||||
None,
|
||||
&argus_and_reviewer(),
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let err = format!("{err:#}");
|
||||
assert!(
|
||||
err.contains("hyperhive/hyperhive is empty or absent")
|
||||
&& err.contains("services.hyperhive.deploy.forgejo.hyperhiveUpstream"),
|
||||
"{err}"
|
||||
);
|
||||
assert!(forge.commits.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refused_lock_commits_nothing() {
|
||||
let forge = forge_with(&[]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue