solstice-ci/crates/runner-integration/src/registration.rs
Till Wegmueller 5dfd9c367b Fix Forgejo runner auth: use x-runner-token/x-runner-uuid headers
Forgejo's connect-rpc API uses custom headers for authentication, not
Authorization: Bearer. Registration uses x-runner-token only, while
post-registration calls require both x-runner-token and x-runner-uuid.
2026-04-06 23:43:07 +02:00

117 lines
3.5 KiB
Rust

use std::path::Path;
use miette::{IntoDiagnostic, Result, miette};
use tracing::{info, warn};
use crate::connect::ConnectClient;
use crate::proto::runner::v1::{DeclareRequest, RegisterRequest};
use crate::state::RunnerIdentity;
const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Load existing runner credentials from disk, or register a new runner.
pub async fn ensure_registered(
client: &ConnectClient,
state_path: &str,
registration_token: Option<&str>,
runner_name: &str,
labels: &[String],
) -> Result<RunnerIdentity> {
// Try loading existing state
if let Some(identity) = load_state(state_path) {
info!(
uuid = %identity.uuid,
name = %identity.name,
"loaded existing runner registration"
);
// Re-declare labels on every startup so Forgejo stays in sync
declare(client, &identity.uuid, &identity.token, labels).await?;
return Ok(identity);
}
// No saved state — must register
let token = registration_token.ok_or_else(|| {
miette!(
"no saved runner state at {state_path} and RUNNER_REGISTRATION_TOKEN is not set; \
cannot register with Forgejo"
)
})?;
info!(name = runner_name, "registering new runner with Forgejo");
let req = RegisterRequest {
name: runner_name.to_string(),
token: token.to_string(),
version: VERSION.to_string(),
labels: labels.to_vec(),
ephemeral: false,
..Default::default()
};
let resp = client.register(&req, token).await?;
let runner = resp
.runner
.ok_or_else(|| miette!("Forgejo returned empty runner in RegisterResponse"))?;
let identity = RunnerIdentity {
id: runner.id,
uuid: runner.uuid,
token: runner.token,
name: runner.name,
registered_at: time::OffsetDateTime::now_utc().to_string(),
};
save_state(state_path, &identity)?;
info!(uuid = %identity.uuid, id = identity.id, "runner registered successfully");
// Declare labels after fresh registration
declare(client, &identity.uuid, &identity.token, labels).await?;
Ok(identity)
}
async fn declare(
client: &ConnectClient,
uuid: &str,
runner_token: &str,
labels: &[String],
) -> Result<()> {
let req = DeclareRequest {
version: VERSION.to_string(),
labels: labels.to_vec(),
};
client.declare(&req, uuid, runner_token).await?;
info!(labels = ?labels, "declared runner labels");
Ok(())
}
fn load_state(path: &str) -> Option<RunnerIdentity> {
let p = Path::new(path);
if !p.exists() {
return None;
}
match std::fs::read_to_string(p) {
Ok(data) => match serde_json::from_str::<RunnerIdentity>(&data) {
Ok(id) => Some(id),
Err(e) => {
warn!(error = %e, path = %path, "failed to parse runner state; will re-register");
None
}
},
Err(e) => {
warn!(error = %e, path = %path, "failed to read runner state; will re-register");
None
}
}
}
fn save_state(path: &str, identity: &RunnerIdentity) -> Result<()> {
// Ensure parent directory exists
if let Some(parent) = Path::new(path).parent() {
std::fs::create_dir_all(parent).into_diagnostic()?;
}
let json = serde_json::to_string_pretty(identity).into_diagnostic()?;
std::fs::write(path, json).into_diagnostic()?;
info!(path = %path, "saved runner state");
Ok(())
}