1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4use crate::cluster::roles::RoleConfig;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ServerConfig {
8 #[serde(default = "default_http_addr")]
9 pub http_addr: String,
10 #[serde(default = "default_grpc_addr")]
11 pub grpc_addr: String,
12 #[serde(default = "default_log_level")]
13 pub log_level: String,
14 #[serde(default)]
15 pub state: StateConfig,
16 #[serde(default)]
17 pub scheduler: SchedulerConfig,
18 #[serde(default)]
19 pub cluster: ClusterConfig,
20 #[serde(default)]
21 pub p2p: P2pConfig,
22 #[serde(default)]
23 pub kindling: KindlingConfig,
24 #[serde(default)]
25 pub reconciler: ReconcilerConfig,
26 #[serde(default)]
27 pub nats: NatsConfig,
28 #[serde(default)]
29 pub sui: SuiConfig,
30 #[serde(default)]
31 pub ports: PortConfig,
32 #[serde(default)]
33 pub volumes: VolumeConfig,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct StateConfig {
38 #[serde(default = "default_server_state_dir")]
39 pub dir: PathBuf,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct SchedulerConfig {
44 #[serde(default = "default_eval_interval")]
45 pub eval_interval_secs: u64,
46 #[serde(default = "default_heartbeat_grace")]
47 pub heartbeat_grace_secs: u64,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ReconcilerConfig {
53 #[serde(default = "default_reconcile_interval")]
55 pub reconcile_interval_secs: u64,
56
57 #[serde(default = "default_reeval_frequency")]
59 pub reeval_every_n_ticks: u64,
60
61 #[serde(default = "default_max_concurrent_evals")]
63 pub max_concurrent_evals: usize,
64
65 #[serde(default = "default_true")]
67 pub drift_detection: bool,
68
69 #[serde(default = "default_true")]
71 pub source_reconciliation: bool,
72
73 #[serde(default = "default_source_reeval_frequency")]
75 pub source_reeval_every_n_ticks: u64,
76
77 #[serde(default = "default_flake_metadata_timeout")]
79 pub flake_metadata_timeout_secs: u64,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ClusterConfig {
85 #[serde(default = "default_cluster_id")]
87 pub cluster_id: String,
88
89 #[serde(default = "default_gossip_addr")]
91 pub gossip_addr: String,
92
93 #[serde(default = "default_raft_addr")]
95 pub raft_addr: String,
96
97 #[serde(default)]
100 pub seed_peers: Vec<String>,
101
102 #[serde(default = "default_true")]
104 pub mdns_discovery: bool,
105
106 #[serde(default = "default_true")]
108 pub kindling_fleet_seeds: bool,
109
110 #[serde(default)]
112 pub roles: RoleConfig,
113
114 #[serde(default = "default_true")]
116 pub auto_bootstrap: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct P2pConfig {
122 #[serde(default = "default_p2p_cache_dir")]
124 pub cache_dir: PathBuf,
125
126 #[serde(default)]
128 pub max_cache_mb: u64,
129
130 #[serde(default = "default_true")]
132 pub eager_replication: bool,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct KindlingConfig {
138 #[serde(default)]
140 pub identity_path: Option<String>,
141
142 #[serde(default)]
144 pub report_path: Option<String>,
145
146 #[serde(default = "default_kindling_addr")]
149 pub daemon_addr: String,
150
151 #[serde(default = "default_true")]
153 pub publish_reports: bool,
154
155 #[serde(default)]
157 pub report_refresh_secs: u64,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ClientConfig {
162 #[serde(default = "default_client_server_addr")]
163 pub server_addr: String,
164 #[serde(default = "default_log_level")]
165 pub log_level: String,
166 #[serde(default = "default_alloc_dir")]
167 pub alloc_dir: PathBuf,
168 #[serde(default)]
169 pub resources: ResourceConfig,
170 #[serde(default)]
171 pub drivers: DriverConfig,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ResourceConfig {
176 #[serde(default)]
177 pub cpu_mhz: u64,
178 #[serde(default)]
179 pub memory_mb: u64,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct DriverConfig {
184 #[serde(default = "default_true")]
185 pub exec: bool,
186 #[serde(default = "default_true")]
187 pub oci: bool,
188 #[serde(default = "default_true")]
189 pub nix: bool,
190}
191
192impl ServerConfig {
193 pub fn load(path: Option<&str>) -> anyhow::Result<Self> {
194 let config_path = match path {
195 Some(p) => PathBuf::from(p),
196 None => default_config_dir().join("server.toml"),
197 };
198
199 if config_path.exists() {
200 let contents = std::fs::read_to_string(&config_path)?;
201 Ok(toml::from_str(&contents)?)
202 } else {
203 Ok(Self::default())
204 }
205 }
206}
207
208impl ClientConfig {
209 pub fn load(path: Option<&str>) -> anyhow::Result<Self> {
210 let config_path = match path {
211 Some(p) => PathBuf::from(p),
212 None => default_config_dir().join("client.toml"),
213 };
214
215 if config_path.exists() {
216 let contents = std::fs::read_to_string(&config_path)?;
217 Ok(toml::from_str(&contents)?)
218 } else {
219 Ok(Self::default())
220 }
221 }
222}
223
224impl Default for ServerConfig {
225 fn default() -> Self {
226 Self {
227 http_addr: default_http_addr(),
228 grpc_addr: default_grpc_addr(),
229 log_level: default_log_level(),
230 state: StateConfig::default(),
231 scheduler: SchedulerConfig::default(),
232 cluster: ClusterConfig::default(),
233 p2p: P2pConfig::default(),
234 kindling: KindlingConfig::default(),
235 reconciler: ReconcilerConfig::default(),
236 nats: NatsConfig::default(),
237 sui: SuiConfig::default(),
238 ports: PortConfig::default(),
239 volumes: VolumeConfig::default(),
240 }
241 }
242}
243
244impl Default for ReconcilerConfig {
245 fn default() -> Self {
246 Self {
247 reconcile_interval_secs: default_reconcile_interval(),
248 reeval_every_n_ticks: default_reeval_frequency(),
249 max_concurrent_evals: default_max_concurrent_evals(),
250 drift_detection: true,
251 source_reconciliation: true,
252 source_reeval_every_n_ticks: default_source_reeval_frequency(),
253 flake_metadata_timeout_secs: default_flake_metadata_timeout(),
254 }
255 }
256}
257
258impl Default for ClientConfig {
259 fn default() -> Self {
260 Self {
261 server_addr: default_client_server_addr(),
262 log_level: default_log_level(),
263 alloc_dir: default_alloc_dir(),
264 resources: ResourceConfig::default(),
265 drivers: DriverConfig::default(),
266 }
267 }
268}
269
270impl Default for StateConfig {
271 fn default() -> Self {
272 Self {
273 dir: default_server_state_dir(),
274 }
275 }
276}
277
278impl Default for SchedulerConfig {
279 fn default() -> Self {
280 Self {
281 eval_interval_secs: default_eval_interval(),
282 heartbeat_grace_secs: default_heartbeat_grace(),
283 }
284 }
285}
286
287impl Default for ClusterConfig {
288 fn default() -> Self {
289 Self {
290 cluster_id: default_cluster_id(),
291 gossip_addr: default_gossip_addr(),
292 raft_addr: default_raft_addr(),
293 seed_peers: Vec::new(),
294 mdns_discovery: true,
295 kindling_fleet_seeds: true,
296 roles: RoleConfig::default(),
297 auto_bootstrap: true,
298 }
299 }
300}
301
302impl Default for P2pConfig {
303 fn default() -> Self {
304 Self {
305 cache_dir: default_p2p_cache_dir(),
306 max_cache_mb: 0,
307 eager_replication: true,
308 }
309 }
310}
311
312impl Default for KindlingConfig {
313 fn default() -> Self {
314 Self {
315 identity_path: None,
316 report_path: None,
317 daemon_addr: default_kindling_addr(),
318 publish_reports: true,
319 report_refresh_secs: 0,
320 }
321 }
322}
323
324impl Default for ResourceConfig {
325 fn default() -> Self {
326 Self {
327 cpu_mhz: 0,
328 memory_mb: 0,
329 }
330 }
331}
332
333impl Default for DriverConfig {
334 fn default() -> Self {
335 Self {
336 exec: true,
337 oci: true,
338 nix: true,
339 }
340 }
341}
342
343fn default_config_dir() -> PathBuf {
344 dirs::config_dir()
345 .unwrap_or_else(|| PathBuf::from("~/.config"))
346 .join("tatara")
347}
348
349fn default_data_dir() -> PathBuf {
350 dirs::data_local_dir()
351 .unwrap_or_else(|| PathBuf::from("~/.local/share"))
352 .join("tatara")
353}
354
355fn default_http_addr() -> String {
356 "0.0.0.0:4646".to_string()
357}
358
359fn default_grpc_addr() -> String {
360 "0.0.0.0:4647".to_string()
361}
362
363fn default_gossip_addr() -> String {
364 "0.0.0.0:4648".to_string()
365}
366
367fn default_raft_addr() -> String {
368 "0.0.0.0:4649".to_string()
369}
370
371fn default_log_level() -> String {
372 "info".to_string()
373}
374
375fn default_cluster_id() -> String {
376 "tatara-default".to_string()
377}
378
379fn default_server_state_dir() -> PathBuf {
380 default_data_dir().join("server")
381}
382
383fn default_alloc_dir() -> PathBuf {
384 default_data_dir().join("alloc")
385}
386
387fn default_p2p_cache_dir() -> PathBuf {
388 default_data_dir().join("p2p")
389}
390
391fn default_client_server_addr() -> String {
392 "127.0.0.1:4647".to_string()
393}
394
395fn default_eval_interval() -> u64 {
396 1
397}
398
399fn default_reconcile_interval() -> u64 {
400 10
401}
402
403fn default_reeval_frequency() -> u64 {
404 6
405}
406
407fn default_max_concurrent_evals() -> usize {
408 2
409}
410
411fn default_heartbeat_grace() -> u64 {
412 30
413}
414
415fn default_source_reeval_frequency() -> u64 {
416 3
417}
418
419fn default_flake_metadata_timeout() -> u64 {
420 30
421}
422
423fn default_kindling_addr() -> String {
424 "http://127.0.0.1:3000".to_string()
425}
426
427fn default_true() -> bool {
428 true
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct NatsConfig {
435 #[serde(default)]
436 pub enabled: bool,
437 #[serde(default = "default_nats_url")]
438 pub url: String,
439}
440
441impl Default for NatsConfig {
442 fn default() -> Self {
443 Self {
444 enabled: false,
445 url: default_nats_url(),
446 }
447 }
448}
449
450fn default_nats_url() -> String {
451 "nats://127.0.0.1:4222".to_string()
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct SuiConfig {
456 #[serde(default)]
457 pub daemon_addr: Option<String>,
458}
459
460impl Default for SuiConfig {
461 fn default() -> Self {
462 Self { daemon_addr: None }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct PortConfig {
468 #[serde(default = "default_port_start")]
469 pub range_start: u16,
470 #[serde(default = "default_port_end")]
471 pub range_end: u16,
472}
473
474impl Default for PortConfig {
475 fn default() -> Self {
476 Self {
477 range_start: default_port_start(),
478 range_end: default_port_end(),
479 }
480 }
481}
482
483fn default_port_start() -> u16 {
484 20000
485}
486fn default_port_end() -> u16 {
487 32000
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct VolumeConfig {
492 #[serde(default = "default_volume_dir")]
493 pub dir: PathBuf,
494}
495
496impl Default for VolumeConfig {
497 fn default() -> Self {
498 Self {
499 dir: default_volume_dir(),
500 }
501 }
502}
503
504fn default_volume_dir() -> PathBuf {
505 dirs::data_dir()
506 .unwrap_or_else(|| PathBuf::from("/tmp"))
507 .join("tatara")
508 .join("volumes")
509}
510
511impl shikumi::TieredConfig for ServerConfig {
524 fn bare() -> Self {
525 Self {
526 http_addr: String::new(),
527 grpc_addr: String::new(),
528 log_level: String::new(),
529 state: <StateConfig as shikumi::TieredConfig>::bare(),
530 scheduler: <SchedulerConfig as shikumi::TieredConfig>::bare(),
531 cluster: <ClusterConfig as shikumi::TieredConfig>::bare(),
532 p2p: <P2pConfig as shikumi::TieredConfig>::bare(),
533 kindling: <KindlingConfig as shikumi::TieredConfig>::bare(),
534 reconciler: <ReconcilerConfig as shikumi::TieredConfig>::bare(),
535 nats: <NatsConfig as shikumi::TieredConfig>::bare(),
536 sui: <SuiConfig as shikumi::TieredConfig>::bare(),
537 ports: <PortConfig as shikumi::TieredConfig>::bare(),
538 volumes: <VolumeConfig as shikumi::TieredConfig>::bare(),
539 }
540 }
541 fn prescribed_default() -> Self { Self::default() }
542}
543
544impl shikumi::TieredConfig for ClientConfig {
545 fn bare() -> Self {
546 Self {
547 server_addr: String::new(),
548 log_level: String::new(),
549 alloc_dir: PathBuf::new(),
550 resources: ResourceConfig::default(),
551 drivers: <DriverConfig as shikumi::TieredConfig>::bare(),
552 }
553 }
554 fn prescribed_default() -> Self { Self::default() }
555}
556
557impl shikumi::TieredConfig for StateConfig {
558 fn bare() -> Self { Self { dir: PathBuf::new() } }
559 fn prescribed_default() -> Self { Self::default() }
560}
561
562impl shikumi::TieredConfig for SchedulerConfig {
563 fn bare() -> Self { Self { eval_interval_secs: 0, heartbeat_grace_secs: 0 } }
564 fn prescribed_default() -> Self { Self::default() }
565}
566
567impl shikumi::TieredConfig for ReconcilerConfig {
568 fn bare() -> Self {
569 Self {
570 reconcile_interval_secs: 0,
571 reeval_every_n_ticks: 0,
572 max_concurrent_evals: 0,
573 drift_detection: false,
574 source_reconciliation: false,
575 source_reeval_every_n_ticks: 0,
576 flake_metadata_timeout_secs: 0,
577 }
578 }
579 fn prescribed_default() -> Self { Self::default() }
580}
581
582impl shikumi::TieredConfig for ClusterConfig {
583 fn bare() -> Self {
584 Self {
585 cluster_id: String::new(),
586 gossip_addr: String::new(),
587 raft_addr: String::new(),
588 seed_peers: Vec::new(),
589 mdns_discovery: false,
590 kindling_fleet_seeds: false,
591 roles: RoleConfig::default(),
592 auto_bootstrap: false,
593 }
594 }
595 fn prescribed_default() -> Self { Self::default() }
596}
597
598impl shikumi::TieredConfig for P2pConfig {
599 fn bare() -> Self {
600 Self {
601 cache_dir: PathBuf::new(),
602 max_cache_mb: 0,
603 eager_replication: false,
604 }
605 }
606 fn prescribed_default() -> Self { Self::default() }
607}
608
609impl shikumi::TieredConfig for KindlingConfig {
610 fn bare() -> Self { <Self as shikumi::TieredConfig>::prescribed_default() }
611 fn prescribed_default() -> Self { Self::default() }
612}
613
614impl shikumi::TieredConfig for DriverConfig {
615 fn bare() -> Self { <Self as shikumi::TieredConfig>::prescribed_default() }
616 fn prescribed_default() -> Self { Self::default() }
617}
618
619impl shikumi::TieredConfig for NatsConfig {
620 fn bare() -> Self { <Self as shikumi::TieredConfig>::prescribed_default() }
621 fn prescribed_default() -> Self { Self::default() }
622}
623
624impl shikumi::TieredConfig for SuiConfig {
625 fn bare() -> Self { <Self as shikumi::TieredConfig>::prescribed_default() }
626 fn prescribed_default() -> Self { Self::default() }
627}
628
629impl shikumi::TieredConfig for PortConfig {
630 fn bare() -> Self { <Self as shikumi::TieredConfig>::prescribed_default() }
631 fn prescribed_default() -> Self { Self::default() }
632}
633
634impl shikumi::TieredConfig for VolumeConfig {
635 fn bare() -> Self { Self { dir: PathBuf::new() } }
636 fn prescribed_default() -> Self { Self::default() }
637}
638
639#[cfg(test)]
640mod tiered_tests {
641 use super::*;
642 use shikumi::{ConfigTier, TieredConfig};
643
644 #[test]
645 fn server_config_bare_is_zero_opinion() {
646 let b = <ServerConfig as TieredConfig>::bare();
647 assert_eq!(b.http_addr, "");
648 assert_eq!(b.grpc_addr, "");
649 assert_eq!(b.log_level, "");
650 assert!(b.state.dir.as_os_str().is_empty());
651 assert_eq!(b.scheduler.eval_interval_secs, 0);
652 assert!(!b.cluster.mdns_discovery);
653 assert!(!b.reconciler.drift_detection);
654 }
655
656 #[test]
657 fn server_config_prescribed_matches_default() {
658 let p = <ServerConfig as TieredConfig>::prescribed_default();
659 let d = ServerConfig::default();
660 assert_eq!(p.http_addr, d.http_addr);
661 assert_eq!(p.log_level, d.log_level);
662 }
663
664 #[test]
665 fn server_config_diff_bare_vs_default_is_non_empty() {
666 let b = <ServerConfig as TieredConfig>::bare();
667 let d = <ServerConfig as TieredConfig>::prescribed_default();
668 let diff = d.diff_against(&b);
669 assert!(!diff.is_empty_diff(), "bare and prescribed_default must differ");
670 }
671
672 #[test]
673 fn server_config_resolve_tier_dispatches_correctly() {
674 assert_eq!(
675 <ServerConfig as TieredConfig>::resolve_tier(ConfigTier::Bare).http_addr,
676 ""
677 );
678 assert!(!<ServerConfig as TieredConfig>::resolve_tier(ConfigTier::Default)
679 .http_addr
680 .is_empty());
681 }
682
683 #[test]
684 fn client_config_bare_is_zero_opinion() {
685 let b = <ClientConfig as TieredConfig>::bare();
686 assert_eq!(b.server_addr, "");
687 assert_eq!(b.log_level, "");
688 assert!(b.alloc_dir.as_os_str().is_empty());
689 }
690}