Skip to main content

orca_core/config/
mod.rs

1mod ai;
2mod cluster;
3mod service;
4
5use std::path::Path;
6
7use crate::error::{OrcaError, Result};
8
9// -- Re-exports --
10
11pub 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
20// -- Load methods --
21
22impl 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        toml::from_str(&content)
27            .map_err(|e| OrcaError::Config(format!("failed to parse {}: {e}", path.display())))
28    }
29}
30
31impl ServicesConfig {
32    pub fn load(path: &Path) -> Result<Self> {
33        let content = std::fs::read_to_string(path)
34            .map_err(|e| OrcaError::Config(format!("failed to read {}: {e}", path.display())))?;
35        toml::from_str(&content)
36            .map_err(|e| OrcaError::Config(format!("failed to parse {}: {e}", path.display())))
37    }
38
39    /// Auto-discover services from subdirectories.
40    ///
41    /// Scans `dir/*/service.toml` and merges all service definitions.
42    /// If a `secrets.json` exists in the same subdirectory, secret patterns
43    /// in env vars (`${secrets.KEY}`) are resolved before returning.
44    pub fn load_dir(dir: &Path) -> Result<Self> {
45        let mut all_services = Vec::new();
46        let entries = std::fs::read_dir(dir)
47            .map_err(|e| OrcaError::Config(format!("failed to read {}: {e}", dir.display())))?;
48
49        let mut subdirs: Vec<_> = entries
50            .filter_map(|e| e.ok())
51            .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
52            .collect();
53        subdirs.sort_by_key(|e| e.file_name());
54
55        for entry in subdirs {
56            let svc_file = entry.path().join("service.toml");
57            if svc_file.exists() {
58                let mut config = Self::load(&svc_file)?;
59                let project_name = entry.file_name().to_string_lossy().to_string();
60
61                // Secrets are resolved later in service_config_to_spec()
62                // so that spec_matches() compares unresolved templates,
63                // preventing unnecessary restarts when token values change.
64
65                // Set project name and default network from directory
66                for svc in &mut config.service {
67                    svc.project = Some(project_name.clone());
68                    if svc.network.is_none() {
69                        svc.network = Some(project_name.clone());
70                    }
71                }
72
73                all_services.extend(config.service);
74            }
75        }
76
77        if all_services.is_empty() {
78            return Err(OrcaError::Config(format!(
79                "no service.toml files found in {}",
80                dir.display()
81            )));
82        }
83
84        Ok(ServicesConfig {
85            service: all_services,
86        })
87    }
88}
89
90#[cfg(test)]
91#[path = "tests_parse.rs"]
92mod tests_parse;
93
94#[cfg(test)]
95#[path = "tests_load.rs"]
96mod tests_load;