1mod ai;
2mod cluster;
3mod service;
4
5use std::path::Path;
6
7use crate::error::{OrcaError, Result};
8
9pub use crate::backup::{BackupConfig, BackupTarget};
12pub use ai::{AiAlertConfig, AiConfig, AlertDeliveryChannels, AutoRemediateConfig};
13pub use cluster::NetworkConfig;
14pub use cluster::{
15 AlertChannelConfig, ApiToken, ClusterConfig, ClusterMeta, FallbackConfig, NodeConfig,
16 NodeGpuConfig, ObservabilityConfig, Role,
17};
18pub use service::{BuildConfig, ProbeConfig, ServiceConfig, ServicesConfig};
19
20impl ClusterConfig {
23 pub fn load(path: &Path) -> Result<Self> {
24 let content = std::fs::read_to_string(path)
25 .map_err(|e| OrcaError::Config(format!("failed to read {}: {e}", path.display())))?;
26 let mut config: Self = toml::from_str(&content)
27 .map_err(|e| OrcaError::Config(format!("failed to parse {}: {e}", path.display())))?;
28 config.resolve_secrets();
29 Ok(config)
30 }
31
32 fn resolve_secrets(&mut self) {
34 let store = match crate::secrets::SecretStore::open(crate::secrets::default_path()) {
35 Ok(s) => s,
36 Err(_) => return,
37 };
38 let resolve = |s: &mut Option<String>| {
39 if let Some(val) = s
40 && val.contains("${secrets.")
41 {
42 let as_map = std::collections::HashMap::from([("_".to_string(), val.clone())]);
43 let resolved = store.resolve_env(&as_map);
44 *val = resolved.get("_").cloned().unwrap_or_default();
45 }
46 };
47 if let Some(ai) = &mut self.ai {
49 resolve(&mut ai.api_key);
50 resolve(&mut ai.endpoint);
51 }
52 if let Some(net) = &mut self.network {
54 resolve(&mut net.setup_key);
55 }
56 }
57}
58
59impl ServicesConfig {
60 pub fn load(path: &Path) -> Result<Self> {
61 let content = std::fs::read_to_string(path)
62 .map_err(|e| OrcaError::Config(format!("failed to read {}: {e}", path.display())))?;
63 toml::from_str(&content)
64 .map_err(|e| OrcaError::Config(format!("failed to parse {}: {e}", path.display())))
65 }
66
67 pub fn load_dir(dir: &Path) -> Result<Self> {
73 let mut all_services = Vec::new();
74 let entries = std::fs::read_dir(dir)
75 .map_err(|e| OrcaError::Config(format!("failed to read {}: {e}", dir.display())))?;
76
77 let mut subdirs: Vec<_> = entries
78 .filter_map(|e| e.ok())
79 .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
80 .collect();
81 subdirs.sort_by_key(|e| e.file_name());
82
83 for entry in subdirs {
84 let svc_file = entry.path().join("service.toml");
85 if svc_file.exists() {
86 let mut config = Self::load(&svc_file)?;
87 let project_name = entry.file_name().to_string_lossy().to_string();
88
89 for svc in &mut config.service {
95 svc.project = Some(project_name.clone());
96 if svc.network.is_none() {
97 svc.network = Some(project_name.clone());
98 }
99 }
100
101 all_services.extend(config.service);
102 }
103 }
104
105 if all_services.is_empty() {
106 return Err(OrcaError::Config(format!(
107 "no service.toml files found in {}",
108 dir.display()
109 )));
110 }
111
112 Ok(ServicesConfig {
113 service: all_services,
114 })
115 }
116}
117
118#[cfg(test)]
119#[path = "tests_parse.rs"]
120mod tests_parse;
121
122#[cfg(test)]
123#[path = "tests_load.rs"]
124mod tests_load;
125
126#[cfg(test)]
127#[path = "tests_secrets.rs"]
128mod tests_secrets;