Skip to main content

mecha10_core/context/
config.rs

1//! Infrastructure configuration loading (Redis URLs, control plane, mecha10.json).
2
3use super::{Context, InfrastructureConfig};
4use crate::error::{Mecha10Error, Result};
5use anyhow::Context as _;
6
7impl Context {
8    /// Load infrastructure configuration based on environment
9    ///
10    /// Priority order:
11    /// 1. mecha10.json (project configuration) - redis.url field
12    /// 2. Environment variables (MECHA10_REDIS_URL or REDIS_URL)
13    /// 3. config/infrastructure.<environment>.yaml
14    ///
15    /// Set via MECHA10_ENVIRONMENT environment variable.
16    pub(super) fn load_infrastructure_config() -> Result<InfrastructureConfig> {
17        use crate::config::load_config;
18        use std::env;
19        use std::path::PathBuf;
20
21        // 1. Try loading from mecha10.json FIRST (project configuration)
22        if let Ok(redis_url) = Self::try_load_redis_from_mecha10_json() {
23            return Ok(InfrastructureConfig {
24                redis: crate::context::infra_config::RedisConfig {
25                    url: redis_url,
26                    max_connections: 10,
27                    connection_timeout_ms: 5000,
28                },
29                control_plane_redis: None,
30                bridge: None,
31                postgres: None,
32                mlflow: None,
33                minio: None,
34            });
35        }
36
37        // 2. Check for environment variable overrides
38        // If REDIS_URL or MECHA10_REDIS_URL is set, use that and create minimal config
39        if let Ok(redis_url) = env::var("MECHA10_REDIS_URL").or_else(|_| env::var("REDIS_URL")) {
40            return Ok(InfrastructureConfig {
41                redis: crate::context::infra_config::RedisConfig {
42                    url: redis_url,
43                    max_connections: 10,
44                    connection_timeout_ms: 5000,
45                },
46                control_plane_redis: None,
47                bridge: None,
48                postgres: None,
49                mlflow: None,
50                minio: None,
51            });
52        }
53
54        // 3. Load from infrastructure.yaml
55        let environment = env::var("MECHA10_ENVIRONMENT").unwrap_or_else(|_| "development".to_string());
56
57        let config_filename = format!("infrastructure.{}.yaml", environment);
58
59        // Try multiple paths in order:
60        // 1. Current directory (for running from workspace root)
61        // 2. Workspace root (for running tests from target/debug/deps)
62        // 3. Two levels up (for running tests from packages/core/target/debug/deps)
63        let paths_to_try = vec![
64            PathBuf::from(format!("config/{}", config_filename)),
65            PathBuf::from(format!("../../config/{}", config_filename)),
66            PathBuf::from(format!("../../../../config/{}", config_filename)),
67        ];
68
69        for path in &paths_to_try {
70            if path.exists() {
71                return load_config(path)
72                    .with_context(|| {
73                        format!(
74                            "Failed to load infrastructure config for environment '{}' from '{}'",
75                            environment,
76                            path.display()
77                        )
78                    })
79                    .map_err(|e| Mecha10Error::Configuration(format!("{:#}", e)));
80            }
81        }
82
83        // If no config file found, return error with helpful message
84        Err(Mecha10Error::Configuration(format!(
85            "Failed to load infrastructure config for environment '{}'. \
86             Tried paths: {}. \
87             Make sure the file exists, set MECHA10_REDIS_URL environment variable, \
88             or configure redis.url in mecha10.json.",
89            environment,
90            paths_to_try
91                .iter()
92                .map(|p| p.display().to_string())
93                .collect::<Vec<_>>()
94                .join(", ")
95        )))
96    }
97
98    /// Try to load Redis URL from mecha10.json
99    ///
100    /// Walks up from the current directory (unbounded - see
101    /// `crate::fs_utils::find_project_root`) looking for a mecha10.json and reads its
102    /// redis.url field.
103    fn try_load_redis_from_mecha10_json() -> Result<String> {
104        use std::env;
105
106        let current_dir = env::current_dir()
107            .map_err(|e| Mecha10Error::Configuration(format!("Failed to get current directory: {}", e)))?;
108
109        let project_root = crate::fs_utils::find_project_root(&current_dir).ok_or_else(|| {
110            Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
111        })?;
112
113        let path = project_root.join(crate::fs_utils::PROJECT_CONFIG_FILE);
114
115        // Read and parse mecha10.json
116        let content = std::fs::read_to_string(&path)
117            .map_err(|e| Mecha10Error::Configuration(format!("Failed to read {}: {}", path.display(), e)))?;
118
119        let json: serde_json::Value = serde_json::from_str(&content)
120            .map_err(|e| Mecha10Error::Configuration(format!("Failed to parse {}: {}", path.display(), e)))?;
121
122        // Extract redis.url
123        json.get("redis")
124            .and_then(|r| r.get("url"))
125            .and_then(|u| u.as_str())
126            .map(|s| s.to_string())
127            .ok_or_else(|| {
128                Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
129            })
130    }
131
132    /// Get Redis URL from infrastructure configuration (helper for internal use)
133    pub(super) fn get_redis_url_from_config(config: &InfrastructureConfig) -> Result<String> {
134        // First try environment variables (for backward compatibility and testing)
135        if let Ok(url) = std::env::var("MECHA10_REDIS_URL") {
136            return Ok(url);
137        }
138        if let Ok(url) = std::env::var("REDIS_URL") {
139            return Ok(url);
140        }
141
142        // Use config
143        Ok(config.redis.url.clone())
144    }
145
146    /// Get Redis URL from infrastructure configuration
147    pub fn get_redis_url() -> Result<String> {
148        let config = Self::load_infrastructure_config()?;
149        Self::get_redis_url_from_config(&config)
150    }
151
152    /// Get control plane Redis URL from infrastructure configuration
153    pub fn get_control_plane_redis_url() -> Result<Option<String>> {
154        let config = Self::load_infrastructure_config()?;
155        Ok(config.control_plane_redis.map(|c| c.url))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use serial_test::serial;
163    use std::fs;
164
165    // These tests mutate the process-wide current directory, so they must run serially
166    // (`#[serial]`) to avoid racing with other tests in this binary.
167
168    #[test]
169    #[serial]
170    fn try_load_redis_from_mecha10_json_finds_config_several_levels_up() {
171        let original_dir = std::env::current_dir().unwrap();
172
173        let project = tempfile::tempdir().unwrap();
174        fs::write(
175            project.path().join("mecha10.json"),
176            r#"{"redis": {"url": "redis://nested-project:6379"}}"#,
177        )
178        .unwrap();
179
180        // Previously this call site probed only 2 hops up ("../", "../../"); this nests 3
181        // levels deep, past the old cap, to prove the walk-up is now unbounded.
182        let nested = project.path().join("a").join("b").join("c");
183        fs::create_dir_all(&nested).unwrap();
184        std::env::set_current_dir(&nested).unwrap();
185
186        let result = Context::try_load_redis_from_mecha10_json();
187
188        std::env::set_current_dir(original_dir).unwrap();
189
190        assert_eq!(result.unwrap(), "redis://nested-project:6379");
191    }
192
193    #[test]
194    #[serial]
195    fn try_load_redis_from_mecha10_json_errs_when_not_found() {
196        let original_dir = std::env::current_dir().unwrap();
197
198        // A standalone tempdir with no mecha10.json anywhere in its ancestry (other than
199        // whatever happens to be above the OS temp dir, which never contains one in CI/dev).
200        let dir = tempfile::tempdir().unwrap();
201        std::env::set_current_dir(dir.path()).unwrap();
202
203        let result = Context::try_load_redis_from_mecha10_json();
204
205        std::env::set_current_dir(original_dir).unwrap();
206
207        assert!(result.is_err());
208    }
209}