feat(#2415): c0re ci_runner orchestration + wire into ensure_all
Part B (c0re half). New forge/ci_runner.rs: validate the hive-ci runner's
persisted .runner against the forge (GET /admin/runners/{id}); if absent or
stale, mint a fresh registration token (GET /admin/runners/registration-token,
raw request against the local http forge — forgejo-api 0.11 doesn't wrap it)
and hand it to hive-priv's RegisterCiRunner to write the host env-file +
restart the runner. Gated on HYPERHIVE_FORGE_CI_ENABLED; best-effort (never
aborts the startup sweep). Called from ensure_all after the org/repo seeding.
The nix boot-path change (drop prefetch gating, add runner precond, set the
env var) lands next on this branch.
This commit is contained in:
parent
858475549a
commit
2941a889f0
2 changed files with 119 additions and 0 deletions
115
hive-c0re/src/forge/ci_runner.rs
Normal file
115
hive-c0re/src/forge/ci_runner.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! hive-ci Forgejo Actions runner registration, driven from hive-c0re.
|
||||
//!
|
||||
//! Moves the runner-registration forge round-trip OFF the container's
|
||||
//! boot-critical path (it was a host-side `hive-ci-prefetch` oneshot that
|
||||
//! gated `container@hive-ci` start — see `nix/host-modules/hive-ci.nix`).
|
||||
//! hive-c0re holds the forge admin token; here it validates the runner's
|
||||
//! existing credentials and, when they're absent or stale, mints a fresh
|
||||
//! registration token and hands it to hive-priv, which writes it to the host
|
||||
//! env-file the container bind-mounts read-only and restarts the in-container
|
||||
//! runner. The admin token never enters the container — only the registration
|
||||
//! token does, exactly as the prefetch did.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::forge_http_base;
|
||||
|
||||
/// Host path to the hive-ci runner's persisted credentials. The container is
|
||||
/// non-ephemeral, so `.runner` survives restarts; a present file with a sane
|
||||
/// `id` means the runner is already registered.
|
||||
const RUNNER_FILE: &str = "/var/lib/nixos-containers/hive-ci/var/lib/gitea-runner/hive/.runner";
|
||||
|
||||
/// Whether the operator enabled the CI runner. The nix module sets
|
||||
/// `HYPERHIVE_FORGE_CI_ENABLED=1` on `hive-c0re.service` when
|
||||
/// `services.hyperhive.forge.ci.enable` is on; absent means CI is off and
|
||||
/// there is no hive-ci container to register a runner for.
|
||||
fn ci_enabled() -> bool {
|
||||
std::env::var("HYPERHIVE_FORGE_CI_ENABLED").is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
}
|
||||
|
||||
/// Ensure the hive-ci runner is registered against the forge. Best-effort:
|
||||
/// every failure is logged and swallowed so it never aborts the startup
|
||||
/// sweep ([`super::ensure_all`]). No-op when CI is disabled or the runner
|
||||
/// already holds valid credentials (so a healthy runner is never restarted).
|
||||
pub(super) async fn ensure_ci_runner_registered(core_token: &str) {
|
||||
if !ci_enabled() {
|
||||
return;
|
||||
}
|
||||
// Existing creds still valid on the forge → nothing to do.
|
||||
if let Some(id) = existing_runner_id()
|
||||
&& runner_valid(core_token, id).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Absent or stale → mint a fresh registration token and hand it to
|
||||
// hive-priv (root) to write the env-file + restart the runner.
|
||||
match fetch_registration_token(core_token).await {
|
||||
Ok(token) => {
|
||||
if let Err(e) = crate::priv_client::register_ci_runner(&token).await {
|
||||
tracing::warn!(error = ?e, "ci runner: hive-priv register_ci_runner failed");
|
||||
} else {
|
||||
tracing::info!("ci runner: registered hive-ci runner with a fresh token");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!(error = ?e, "ci runner: fetch registration token failed"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the runner id from the persisted `.runner` JSON, if present and sane
|
||||
/// (a `0` id is the gitea-runner "unregistered" sentinel).
|
||||
fn existing_runner_id() -> Option<u64> {
|
||||
let raw = std::fs::read_to_string(RUNNER_FILE).ok()?;
|
||||
let json: Value = serde_json::from_str(&raw).ok()?;
|
||||
let id = json.get("id")?.as_u64()?;
|
||||
(id != 0).then_some(id)
|
||||
}
|
||||
|
||||
/// `GET /admin/runners/{id}` — `true` iff the runner still exists on the forge
|
||||
/// (HTTP 200). A 404 (deleted from the admin panel) or any other status means
|
||||
/// re-registration is needed. A transport error (forge unreachable) is treated
|
||||
/// as "keep the existing creds" so a network blip never wipes a valid runner.
|
||||
async fn runner_valid(core_token: &str, id: u64) -> bool {
|
||||
let url = format!("{}/api/v1/admin/runners/{id}", forge_http_base());
|
||||
match reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("Authorization", format!("token {core_token}"))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => resp.status().is_success(),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "ci runner: validation request failed; keeping existing creds");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET /admin/runners/registration-token` — mint a fresh registration token.
|
||||
/// forgejo-api (0.11) doesn't wrap this endpoint, so this is a raw request
|
||||
/// against the local HTTP forge ([`forge_http_base`]), mirroring the prefetch's
|
||||
/// former curl; Forgejo accepts `Authorization: token <admin>`.
|
||||
async fn fetch_registration_token(core_token: &str) -> Result<String> {
|
||||
let url = format!(
|
||||
"{}/api/v1/admin/runners/registration-token",
|
||||
forge_http_base()
|
||||
);
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("Authorization", format!("token {core_token}"))
|
||||
.send()
|
||||
.await
|
||||
.context("GET admin/runners/registration-token")?;
|
||||
let status = resp.status();
|
||||
let json: Value = resp
|
||||
.json()
|
||||
.await
|
||||
.context("parse registration-token response")?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("registration-token HTTP {status}: {json}");
|
||||
}
|
||||
json.get("token")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.context("registration-token response missing 'token' field")
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
//! collaborator access to for operator-curated shared content.
|
||||
//! No-op when `hive-forge` isn't running. Full design: `docs/forge.md`.
|
||||
|
||||
mod ci_runner;
|
||||
pub mod config_pr_poll;
|
||||
mod pr_merge;
|
||||
mod repos;
|
||||
|
|
@ -266,6 +267,9 @@ pub async fn ensure_all() {
|
|||
if let Err(e) = ensure_config_org_avatar(token).await {
|
||||
tracing::warn!(error = ?e, "forge: ensure_config_org_avatar failed");
|
||||
}
|
||||
// Register the hive-ci Actions runner (off the container's boot path;
|
||||
// no-op when CI is disabled or the runner already holds valid creds).
|
||||
ci_runner::ensure_ci_runner_registered(token).await;
|
||||
}
|
||||
let Ok(containers) = crate::lifecycle::list().await else {
|
||||
tracing::warn!("forge: nixos-container list failed; skipping user sweep");
|
||||
|
|
|
|||
Loading…
Reference in a new issue