Skip to main content

tatara_engine/kindling_bridge/
identity.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use tracing::{debug, info};
6
7use tatara_core::cluster::types::{NodeId, NodeMeta, NodeRoles};
8use tatara_core::domain::job::{DriverType, Resources};
9
10/// Minimal subset of kindling's NodeIdentity we need.
11/// We read kindling's node.yaml directly — no dependency on kindling as a library.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct KindlingIdentity {
14    #[serde(default)]
15    pub version: String,
16    #[serde(default)]
17    pub profile: String,
18    #[serde(default)]
19    pub hostname: String,
20    #[serde(default)]
21    pub hardware: HardwareConfig,
22    #[serde(default)]
23    pub fleet: FleetConfig,
24    #[serde(default)]
25    pub network: NetworkConfig,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct HardwareConfig {
30    #[serde(default)]
31    pub platform: String,
32    #[serde(default)]
33    pub cpu: CpuConfig,
34    #[serde(default)]
35    pub memory: Option<MemoryConfig>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39pub struct CpuConfig {
40    #[serde(default)]
41    pub cores: Option<u32>,
42    #[serde(default)]
43    pub threads: Option<u32>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct MemoryConfig {
48    pub size_gb: f64,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52pub struct FleetConfig {
53    pub controller: Option<String>,
54    pub environment: Option<String>,
55    pub owner: Option<String>,
56    pub team: Option<String>,
57    #[serde(default)]
58    pub tags: Vec<String>,
59    #[serde(default)]
60    pub peers: Vec<FleetPeer>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct FleetPeer {
65    pub name: String,
66    pub hostname: String,
67    #[serde(default = "default_ssh_user")]
68    pub ssh_user: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, Default)]
72pub struct NetworkConfig {
73    #[serde(default)]
74    pub interfaces: HashMap<String, NetworkInterface>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, Default)]
78pub struct NetworkInterface {
79    pub address: Option<String>,
80}
81
82fn default_ssh_user() -> String {
83    "root".to_string()
84}
85
86/// Load kindling identity from disk.
87pub fn load_identity(path: Option<&Path>) -> Result<Option<KindlingIdentity>> {
88    let identity_path = path
89        .map(PathBuf::from)
90        .unwrap_or_else(|| default_identity_path());
91
92    if !identity_path.exists() {
93        debug!(path = %identity_path.display(), "Kindling identity file not found");
94        return Ok(None);
95    }
96
97    let content = std::fs::read_to_string(&identity_path)
98        .with_context(|| format!("Failed to read {}", identity_path.display()))?;
99
100    let identity: KindlingIdentity = serde_yaml::from_str(&content)
101        .with_context(|| format!("Failed to parse {}", identity_path.display()))?;
102
103    info!(
104        hostname = %identity.hostname,
105        profile = %identity.profile,
106        "Loaded kindling identity"
107    );
108
109    Ok(Some(identity))
110}
111
112/// Derive a deterministic NodeId from the hostname.
113/// Uses a hash to produce a u64 that's stable across restarts.
114pub fn derive_node_id(hostname: &str) -> NodeId {
115    use sha2::{Digest, Sha256};
116    let hash = Sha256::digest(hostname.as_bytes());
117    u64::from_le_bytes(hash[..8].try_into().unwrap())
118}
119
120/// Build tatara NodeMeta from kindling identity + runtime detection.
121pub fn build_node_meta(
122    identity: &KindlingIdentity,
123    roles: NodeRoles,
124    http_addr: &str,
125    gossip_addr: &str,
126    raft_addr: &str,
127    drivers: Vec<DriverType>,
128) -> NodeMeta {
129    let cpu_mhz = identity
130        .hardware
131        .cpu
132        .threads
133        .or(identity.hardware.cpu.cores)
134        .unwrap_or_else(|| {
135            std::thread::available_parallelism()
136                .map(|p| p.get() as u32)
137                .unwrap_or(1)
138        }) as u64
139        * 1000;
140
141    let memory_mb = identity
142        .hardware
143        .memory
144        .as_ref()
145        .map(|m| (m.size_gb * 1024.0) as u64)
146        .unwrap_or(0);
147
148    NodeMeta {
149        node_id: derive_node_id(&identity.hostname),
150        hostname: identity.hostname.clone(),
151        http_addr: http_addr.to_string(),
152        gossip_addr: gossip_addr.to_string(),
153        raft_addr: raft_addr.to_string(),
154        os: std::env::consts::OS.to_string(),
155        arch: std::env::consts::ARCH.to_string(),
156        roles,
157        drivers,
158        total_resources: Resources { cpu_mhz, memory_mb },
159        available_resources: Resources { cpu_mhz, memory_mb },
160        allocations_running: 0,
161        joined_at: chrono::Utc::now(),
162        version: env!("CARGO_PKG_VERSION").to_string(),
163        eligible: true,
164        wireguard_pubkey: None,
165        tunnel_address: None,
166    }
167}
168
169/// Extract seed peers from kindling's fleet config.
170/// Maps fleet peers to gossip addresses (hostname:gossip_port).
171pub fn fleet_seed_peers(fleet: &FleetConfig, gossip_port: u16) -> Vec<String> {
172    fleet
173        .peers
174        .iter()
175        .map(|p| format!("{}:{}", p.hostname, gossip_port))
176        .collect()
177}
178
179fn default_identity_path() -> PathBuf {
180    dirs::config_dir()
181        .unwrap_or_else(|| PathBuf::from("~/.config"))
182        .join("kindling")
183        .join("node.yaml")
184}