tatara_engine/kindling_bridge/
identity.rs1use 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#[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
86pub 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
112pub 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
120pub 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
169pub 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}