mirror of
https://codeberg.org/Toasterson/solstice-ci.git
synced 2026-04-11 05:40:41 +00:00
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.
117 lines
3.5 KiB
Rust
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(())
|
|
}
|