rework(#2072): seed mirrors in c0re startup, not a host-side oneshot
Per mara: the mirror seeding belongs in hive-c0re's forge provisioning
sweep, where the core admin token + org-ensure already live — not a parallel
host-side nix oneshot.
- forge.rs: ensure_mirrors() reads HYPERHIVE_FORGE_MIRRORS (JSON list of
{upstream,dest}), ensures each dest org (reuse ensure_org) + creates the
pull-mirror via the migrate API (reuse forge_http, serde_json::json! body,
409/existing = success). Called in ensure_all() right after the SEEDED_ORGS
loop (token in scope, warn-and-continue like the other ensure_* steps).
- hive-forge.nix: forward effectiveMirrors to c0re via
systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS; drop the
forgejo-seed-mirrors.service + its script + the host-side core-token read.
Keep the forge.mirrors option, DEFAULT_ACTIONS_URL=self (CI-gated), and the
dest-shape / no-c0re-namespace-collision assertions.
Verified locally: nix parse + treefmt (incl rustfmt) clean; serde/serde_json
patterns mirror dashboard.rs. cargo build runs in CI (no cc in my container).
This commit is contained in:
parent
6f5dade9c9
commit
53df2c9598
2 changed files with 100 additions and 101 deletions
|
|
@ -913,6 +913,94 @@ async fn ensure_org(name: &str, admin_token: &str) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One operator-declared pull-mirror, forwarded from the nix
|
||||||
|
/// `services.hyperhive.forge.mirrors` option as JSON in
|
||||||
|
/// `HYPERHIVE_FORGE_MIRRORS`.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct Mirror {
|
||||||
|
/// Upstream clone URL to mirror from (e.g. `https://github.com/actions/checkout`).
|
||||||
|
upstream: String,
|
||||||
|
/// Local `<owner>/<repo>` the mirror is created at.
|
||||||
|
dest: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure each `HYPERHIVE_FORGE_MIRRORS` entry exists as a real Forgejo
|
||||||
|
/// pull-mirror. The env carries the JSON-encoded nix `forge.mirrors` list
|
||||||
|
/// (plus the CI-auto `actions/checkout` entry). Absent/empty env = no-op.
|
||||||
|
/// Per-mirror failures warn and continue — never abort the startup sweep.
|
||||||
|
async fn ensure_mirrors(admin_token: &str) {
|
||||||
|
let raw = match std::env::var("HYPERHIVE_FORGE_MIRRORS") {
|
||||||
|
Ok(s) if !s.trim().is_empty() => s,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let mirrors: Vec<Mirror> = match serde_json::from_str(&raw) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "forge: HYPERHIVE_FORGE_MIRRORS is not valid JSON; skipping mirror seed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for m in mirrors {
|
||||||
|
let Some((owner, repo)) = m.dest.split_once('/') else {
|
||||||
|
tracing::warn!(dest = %m.dest, "forge: mirror dest is not <owner>/<repo>; skipping");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Create the dest org first (idempotent); the mirror can't land
|
||||||
|
// without its owner existing.
|
||||||
|
if let Err(e) = ensure_org(owner, admin_token).await {
|
||||||
|
tracing::warn!(%owner, error = ?e, "forge: ensure_org for mirror failed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Err(e) = ensure_mirror_repo(&m.upstream, owner, repo, admin_token).await {
|
||||||
|
tracing::warn!(dest = %m.dest, error = ?e, "forge: ensure_mirror_repo failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `owner/repo` as a pull-mirror of `upstream` via the migrate API.
|
||||||
|
/// Idempotent: a cheap existence check skips an already-seeded mirror (it
|
||||||
|
/// persists in the non-ephemeral forge state across reboots), and a
|
||||||
|
/// 409/422 from migrate is also treated as success.
|
||||||
|
async fn ensure_mirror_repo(
|
||||||
|
upstream: &str,
|
||||||
|
owner: &str,
|
||||||
|
repo: &str,
|
||||||
|
admin_token: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let get_url = format!("{FORGE_HTTP}/api/v1/repos/{owner}/{repo}");
|
||||||
|
let (status, _) = forge_http(reqwest::Method::GET, &get_url, admin_token, "").await?;
|
||||||
|
if status.is_success() {
|
||||||
|
tracing::debug!(%owner, %repo, "forge: pull-mirror already present");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// serde_json::json! → the upstream URL is escaped safely (no string
|
||||||
|
// interpolation into the JSON body).
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"clone_addr": upstream,
|
||||||
|
"repo_owner": owner,
|
||||||
|
"repo_name": repo,
|
||||||
|
"mirror": true,
|
||||||
|
"service": "git",
|
||||||
|
"private": false,
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let url = format!("{FORGE_HTTP}/api/v1/repos/migrate");
|
||||||
|
let (status, text) = forge_http(reqwest::Method::POST, &url, admin_token, &body).await?;
|
||||||
|
match status.as_u16() {
|
||||||
|
201 => {
|
||||||
|
tracing::info!(%owner, %repo, %upstream, "forge: created pull-mirror");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
409 | 422 => {
|
||||||
|
tracing::debug!(%owner, %repo, "forge: pull-mirror already exists");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
anyhow::bail!("POST /api/v1/repos/migrate {owner}/{repo} returned HTTP {other}: {text}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated
|
/// Whether `ns` is a hive-managed Forgejo namespace that agent-initiated
|
||||||
/// repo creation must never target — `internal` (operator-curated
|
/// repo creation must never target — `internal` (operator-curated
|
||||||
/// shared content) + `agent-configs` / `core` (hive-c0re-internal). The
|
/// shared content) + `agent-configs` / `core` (hive-c0re-internal). The
|
||||||
|
|
@ -1141,6 +1229,11 @@ pub async fn ensure_all() {
|
||||||
tracing::warn!(%org, error = ?e, "forge: ensure_org failed");
|
tracing::warn!(%org, error = ?e, "forge: ensure_org failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Seed the operator-declared pull-mirrors (nix `forge.mirrors` +
|
||||||
|
// the CI-auto `actions/checkout`, forwarded via the
|
||||||
|
// `HYPERHIVE_FORGE_MIRRORS` env). Each ensures its own dest org, so
|
||||||
|
// this is independent of the SEEDED_ORGS loop above.
|
||||||
|
ensure_mirrors(token).await;
|
||||||
// Provision the operator merge-gate team (empty) inside the agents
|
// Provision the operator merge-gate team (empty) inside the agents
|
||||||
// org so branch protection can reference it before anyone joins
|
// org so branch protection can reference it before anyone joins
|
||||||
//. The operator adds herself as a member out-of-band.
|
//. The operator adds herself as a member out-of-band.
|
||||||
|
|
|
||||||
|
|
@ -53,86 +53,6 @@ let
|
||||||
++ lib.optional (
|
++ lib.optional (
|
||||||
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
ciEnabled && !(lib.any (m: m.dest == actionCheckoutMirror.dest) cfg.mirrors)
|
||||||
) actionCheckoutMirror;
|
) actionCheckoutMirror;
|
||||||
|
|
||||||
# Host-side core admin token hive-c0re mints after provisioning the forge
|
|
||||||
# admin (same file hive-ci-prefetch reads). Root-only; never enters a
|
|
||||||
# container — so the mirror seed runs host-side, exactly like
|
|
||||||
# hive-ci-prefetch, rather than minting a second token in-container.
|
|
||||||
coreTokenPath = "/var/lib/hyperhive/forge-core-token";
|
|
||||||
|
|
||||||
# Idempotently create each `effectiveMirrors` entry as a real Forgejo
|
|
||||||
# pull-mirror via the migrate API. Host-side: only talks to the forge on
|
|
||||||
# loopback (forgejo itself does the upstream clone, so the upstream-DNS
|
|
||||||
# dependency lives in the container + is off the CI critical path).
|
|
||||||
# Modelled on hive-ci-prefetch's wait-for-core-token loop.
|
|
||||||
seedMirrorsScript = pkgs.writeShellScript "forgejo-seed-mirrors" ''
|
|
||||||
set -uo pipefail
|
|
||||||
FORGE_URL="http://127.0.0.1:${toString cfg.httpPort}"
|
|
||||||
|
|
||||||
CORE_TOKEN=""
|
|
||||||
for i in $(seq 1 60); do
|
|
||||||
if [ -f "${coreTokenPath}" ]; then CORE_TOKEN=$(cat "${coreTokenPath}"); break; fi
|
|
||||||
echo "forgejo-seed-mirrors: waiting for core token ($i/60)..." >&2
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
if [ -z "''${CORE_TOKEN:-}" ]; then
|
|
||||||
echo "forgejo-seed-mirrors: core token absent after 60s — cannot seed mirrors" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
AUTH="Authorization: token $CORE_TOKEN"
|
|
||||||
rc=0
|
|
||||||
|
|
||||||
seed_one() {
|
|
||||||
upstream="$1"; owner="$2"; repo="$3"
|
|
||||||
# Ensure the dest org (idempotent: 201 created / 422 already exists).
|
|
||||||
org_body=$(${pkgs.jq}/bin/jq -nc --arg u "$owner" '{ username: $u }')
|
|
||||||
ohttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
||||||
-H "$AUTH" -H 'Content-Type: application/json' \
|
|
||||||
"$FORGE_URL/api/v1/orgs" -d "$org_body" || echo 000)
|
|
||||||
case "$ohttp" in
|
|
||||||
201 | 422) ;;
|
|
||||||
*) echo "forgejo-seed-mirrors: ensure org '$owner' returned HTTP $ohttp" >&2 ;;
|
|
||||||
esac
|
|
||||||
# Skip if the repo already exists (the mirror persists across reboots
|
|
||||||
# in the non-ephemeral forge state, so this no-ops on every reboot
|
|
||||||
# after the first).
|
|
||||||
rhttp=$(${pkgs.curl}/bin/curl -s -o /dev/null -w '%{http_code}' \
|
|
||||||
-H "$AUTH" "$FORGE_URL/api/v1/repos/$owner/$repo" || echo 000)
|
|
||||||
if [ "$rhttp" = 200 ]; then
|
|
||||||
echo "forgejo-seed-mirrors: $owner/$repo already present — skipping" >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
# Create the pull-mirror. service=git → generic git clone of
|
|
||||||
# clone_addr (no upstream API token needed); mirror=true → forgejo
|
|
||||||
# keeps it re-syncing on its mirror interval.
|
|
||||||
mig_body=$(${pkgs.jq}/bin/jq -nc \
|
|
||||||
--arg c "$upstream" --arg o "$owner" --arg r "$repo" \
|
|
||||||
'{ clone_addr: $c, repo_owner: $o, repo_name: $r, mirror: true, service: "git", private: false }')
|
|
||||||
resp=$(${pkgs.curl}/bin/curl -s -w $'\n%{http_code}' -X POST \
|
|
||||||
-H "$AUTH" -H 'Content-Type: application/json' \
|
|
||||||
"$FORGE_URL/api/v1/repos/migrate" \
|
|
||||||
-d "$mig_body" \
|
|
||||||
|| printf '\n000')
|
|
||||||
mhttp=$(printf '%s' "$resp" | tail -n1)
|
|
||||||
case "$mhttp" in
|
|
||||||
2*) echo "forgejo-seed-mirrors: created pull-mirror $owner/$repo from $upstream" >&2 ;;
|
|
||||||
*)
|
|
||||||
echo "forgejo-seed-mirrors: migrate $owner/$repo failed HTTP $mhttp: $(printf '%s' "$resp" | sed '$d')" >&2
|
|
||||||
rc=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
${lib.concatMapStringsSep "\n" (
|
|
||||||
m:
|
|
||||||
let
|
|
||||||
parts = lib.splitString "/" m.dest;
|
|
||||||
in
|
|
||||||
"seed_one ${lib.escapeShellArg m.upstream} ${lib.escapeShellArg (builtins.elemAt parts 0)} ${lib.escapeShellArg (builtins.elemAt parts 1)}"
|
|
||||||
) effectiveMirrors}
|
|
||||||
|
|
||||||
exit $rc
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
||||||
|
|
@ -591,26 +511,12 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Seed the configured pull-mirrors once the forge + core token are up.
|
# Forward the declared pull-mirrors to hive-c0re, which seeds them in
|
||||||
# Host-side (the core admin token never enters a container) and modelled
|
# its forge provisioning sweep (`forge.rs::ensure_mirrors`, alongside
|
||||||
# on hive-ci-prefetch. partOf the forge container so it re-runs (and
|
# the SEEDED_ORGS ensure). c0re already holds the core admin token and
|
||||||
# re-ensures, idempotently) on every forge (re)start. Only present when
|
# ensures the orgs there, so the seeding lives in one place rather than
|
||||||
# there's something to seed.
|
# a parallel host-side unit. JSON-encoded list of { upstream, dest };
|
||||||
systemd.services.forgejo-seed-mirrors = lib.mkIf (effectiveMirrors != [ ]) {
|
# `[]` when nothing to seed (c0re no-ops).
|
||||||
description = "Seed Forgejo pull-mirrors (host-side)";
|
systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS = builtins.toJSON effectiveMirrors;
|
||||||
after = [
|
|
||||||
"hive-c0re.service"
|
|
||||||
"container@hive-forge.service"
|
|
||||||
];
|
|
||||||
wants = [ "container@hive-forge.service" ];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
partOf = [ "container@hive-forge.service" ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
ExecStart = seedMirrorsScript;
|
|
||||||
SyslogIdentifier = "forgejo-seed-mirrors";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue