heldar_kernel/services/
fleet_register.rs1use std::sync::Arc;
18use std::time::Duration;
19
20use anyhow::Context;
21use serde_json::{json, Value};
22
23use crate::config::Config;
24
25fn register_url(cp_url: &str) -> String {
27 format!("{}/api/v1/fleet/nodes", cp_url.trim_end_matches('/'))
28}
29
30fn register_body(site_id: &str, base_url: &str, token: &str) -> Value {
33 json!({ "id": site_id, "base_url": base_url, "token": token })
34}
35
36fn build_client(cfg: &Config) -> anyhow::Result<reqwest::Client> {
39 let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(10));
40 if let Some(t) = &cfg.cp_tls {
41 let cert = std::fs::read(&t.client_cert)
42 .with_context(|| format!("reading client cert {}", t.client_cert.display()))?;
43 let key = std::fs::read(&t.client_key)
44 .with_context(|| format!("reading client key {}", t.client_key.display()))?;
45 let ca = std::fs::read(&t.server_ca)
46 .with_context(|| format!("reading control-plane CA {}", t.server_ca.display()))?;
47 let mut identity_pem = key;
49 identity_pem.extend_from_slice(&cert);
50 let identity =
51 reqwest::Identity::from_pem(&identity_pem).context("building client identity")?;
52 let root = reqwest::Certificate::from_pem(&ca).context("parsing control-plane CA")?;
53 builder = builder.identity(identity).add_root_certificate(root);
54 }
55 builder.build().context("building HTTP client")
56}
57
58pub async fn run(cfg: Arc<Config>) {
62 let (Some(cp_url), Some(site_id), Some(base_url)) = (
63 cfg.cp_url.as_deref(),
64 cfg.site_id.as_deref(),
65 cfg.public_base_url.as_deref(),
66 ) else {
67 std::future::pending::<()>().await;
69 return;
70 };
71
72 let client = match build_client(&cfg) {
73 Ok(c) => c,
74 Err(e) => {
75 tracing::error!(error = %e, "fleet self-registration disabled: bad mTLS config");
77 std::future::pending::<()>().await;
78 return;
79 }
80 };
81 let url = register_url(cp_url);
82 let body = register_body(site_id, base_url, &cfg.cp_token);
83 let mut tick = tokio::time::interval(Duration::from_secs(cfg.cp_register_interval_s.max(1)));
85 loop {
86 tick.tick().await;
87 match client.post(&url).json(&body).send().await {
88 Ok(r) if r.status().is_success() => {
89 tracing::debug!(node = %site_id, control_plane = %cp_url, "registered with fleet control plane")
90 }
91 Ok(r) => {
92 tracing::warn!(node = %site_id, status = %r.status(), "fleet registration rejected")
93 }
94 Err(e) => {
95 tracing::warn!(node = %site_id, error = %e, "fleet registration failed; retry next heartbeat")
96 }
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn register_url_appends_path_and_trims_trailing_slash() {
107 assert_eq!(
108 register_url("http://cp.lan:9100"),
109 "http://cp.lan:9100/api/v1/fleet/nodes"
110 );
111 assert_eq!(
112 register_url("http://cp.lan:9100/"),
113 "http://cp.lan:9100/api/v1/fleet/nodes"
114 );
115 }
116
117 #[test]
118 fn register_body_carries_identity_url_and_token() {
119 let b = register_body("site-a", "https://edge-a.lan", "tok");
120 assert_eq!(b["id"], "site-a");
121 assert_eq!(b["base_url"], "https://edge-a.lan");
122 assert_eq!(b["token"], "tok");
123 }
124}