use std::sync::Arc;
use std::time::Duration;
use anyhow::Context;
use serde_json::{json, Value};
use crate::config::Config;
fn register_url(cp_url: &str) -> String {
format!("{}/api/v1/fleet/nodes", cp_url.trim_end_matches('/'))
}
fn register_body(site_id: &str, base_url: &str, token: &str) -> Value {
json!({ "id": site_id, "base_url": base_url, "token": token })
}
fn build_client(cfg: &Config) -> anyhow::Result<reqwest::Client> {
let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(10));
if let Some(t) = &cfg.cp_tls {
let cert = std::fs::read(&t.client_cert)
.with_context(|| format!("reading client cert {}", t.client_cert.display()))?;
let key = std::fs::read(&t.client_key)
.with_context(|| format!("reading client key {}", t.client_key.display()))?;
let ca = std::fs::read(&t.server_ca)
.with_context(|| format!("reading control-plane CA {}", t.server_ca.display()))?;
let mut identity_pem = key;
identity_pem.extend_from_slice(&cert);
let identity =
reqwest::Identity::from_pem(&identity_pem).context("building client identity")?;
let root = reqwest::Certificate::from_pem(&ca).context("parsing control-plane CA")?;
builder = builder.identity(identity).add_root_certificate(root);
}
builder.build().context("building HTTP client")
}
pub async fn run(cfg: Arc<Config>) {
let (Some(cp_url), Some(site_id), Some(base_url)) = (
cfg.cp_url.as_deref(),
cfg.site_id.as_deref(),
cfg.public_base_url.as_deref(),
) else {
std::future::pending::<()>().await;
return;
};
let client = match build_client(&cfg) {
Ok(c) => c,
Err(e) => {
tracing::error!(error = %e, "fleet self-registration disabled: bad mTLS config");
std::future::pending::<()>().await;
return;
}
};
let url = register_url(cp_url);
let body = register_body(site_id, base_url, &cfg.cp_token);
let mut tick = tokio::time::interval(Duration::from_secs(cfg.cp_register_interval_s.max(1)));
loop {
tick.tick().await;
match client.post(&url).json(&body).send().await {
Ok(r) if r.status().is_success() => {
tracing::debug!(node = %site_id, control_plane = %cp_url, "registered with fleet control plane")
}
Ok(r) => {
tracing::warn!(node = %site_id, status = %r.status(), "fleet registration rejected")
}
Err(e) => {
tracing::warn!(node = %site_id, error = %e, "fleet registration failed; retry next heartbeat")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_url_appends_path_and_trims_trailing_slash() {
assert_eq!(
register_url("http://cp.lan:9100"),
"http://cp.lan:9100/api/v1/fleet/nodes"
);
assert_eq!(
register_url("http://cp.lan:9100/"),
"http://cp.lan:9100/api/v1/fleet/nodes"
);
}
#[test]
fn register_body_carries_identity_url_and_token() {
let b = register_body("site-a", "https://edge-a.lan", "tok");
assert_eq!(b["id"], "site-a");
assert_eq!(b["base_url"], "https://edge-a.lan");
assert_eq!(b["token"], "tok");
}
}