Skip to main content

hyperi_rustlib/
runtime.rs

1// Project:   hyperi-rustlib
2// File:      src/runtime.rs
3// Purpose:   Container-aware runtime path management
4// Language:  Rust
5//
6// License:   FSL-1.1-ALv2
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Runtime path management.
10//!
11//! Provides container-aware path resolution that works identically in
12//! Kubernetes, Docker, and local development environments.
13
14use std::path::PathBuf;
15
16use crate::env::Environment;
17
18/// Default container base path.
19const CONTAINER_BASE_PATH: &str = "/app";
20
21/// Standard application paths based on runtime environment.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RuntimePaths {
24    /// Read-only configuration directory (ConfigMap in K8s, ~/.config locally)
25    pub config_dir: PathBuf,
26    /// Read-only secrets directory (Secret in K8s, ~/.{app}/secrets locally)
27    pub secrets_dir: PathBuf,
28    /// Persistent data directory (PVC in K8s, ~/.local/share locally)
29    pub data_dir: PathBuf,
30    /// Ephemeral temporary directory (EmptyDir in K8s, /tmp locally)
31    pub temp_dir: PathBuf,
32    /// Application logs directory
33    pub logs_dir: PathBuf,
34    /// Cache directory
35    pub cache_dir: PathBuf,
36    /// Runtime directory (PID files, sockets)
37    pub run_dir: PathBuf,
38}
39
40impl RuntimePaths {
41    /// Discover paths based on auto-detected environment.
42    #[must_use]
43    pub fn discover() -> Self {
44        Self::discover_for(Environment::detect())
45    }
46
47    /// Discover paths for a specific environment.
48    #[must_use]
49    pub fn discover_for(env: Environment) -> Self {
50        match env {
51            Environment::Kubernetes | Environment::Docker | Environment::Container => {
52                Self::container_paths()
53            }
54            Environment::BareMetal => Self::local_paths(),
55        }
56    }
57
58    /// Get paths for container environments.
59    fn container_paths() -> Self {
60        let base = std::env::var("CONTAINER_BASE_PATH")
61            .unwrap_or_else(|_| CONTAINER_BASE_PATH.to_string());
62        let base_path = PathBuf::from(&base);
63
64        Self {
65            config_dir: base_path.join("config"),
66            secrets_dir: base_path.join("secrets"),
67            data_dir: base_path.join("data"),
68            temp_dir: base_path.join("tmp"),
69            logs_dir: base_path.join("logs"),
70            cache_dir: base_path.join("cache"),
71            run_dir: base_path.join("run"),
72        }
73    }
74
75    /// Get paths for local development (XDG-compliant).
76    fn local_paths() -> Self {
77        let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "hs-app".to_string());
78
79        // Use dirs crate for XDG-compliant paths
80        let config_dir = dirs::config_dir()
81            .unwrap_or_else(|| PathBuf::from("~/.config"))
82            .join(&app_name);
83
84        let data_dir = dirs::data_dir()
85            .unwrap_or_else(|| PathBuf::from("~/.local/share"))
86            .join(&app_name);
87
88        let cache_dir = dirs::cache_dir()
89            .unwrap_or_else(|| PathBuf::from("~/.cache"))
90            .join(&app_name);
91
92        let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~"));
93
94        Self {
95            config_dir,
96            secrets_dir: home_dir.join(format!(".{app_name}")).join("secrets"),
97            data_dir: data_dir.clone(),
98            temp_dir: std::env::temp_dir().join(&app_name),
99            logs_dir: data_dir.join("logs"),
100            cache_dir,
101            run_dir: dirs::runtime_dir()
102                .unwrap_or_else(|| PathBuf::from("/tmp"))
103                .join(&app_name),
104        }
105    }
106
107    /// Create all directories if they don't exist.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if directory creation fails.
112    pub fn ensure_dirs(&self) -> std::io::Result<()> {
113        std::fs::create_dir_all(&self.config_dir)?;
114        std::fs::create_dir_all(&self.secrets_dir)?;
115        std::fs::create_dir_all(&self.data_dir)?;
116        std::fs::create_dir_all(&self.temp_dir)?;
117        std::fs::create_dir_all(&self.logs_dir)?;
118        std::fs::create_dir_all(&self.cache_dir)?;
119        std::fs::create_dir_all(&self.run_dir)?;
120        Ok(())
121    }
122
123    /// Check if all required directories exist.
124    #[must_use]
125    pub fn all_exist(&self) -> bool {
126        self.config_dir.exists()
127            && self.secrets_dir.exists()
128            && self.data_dir.exists()
129            && self.temp_dir.exists()
130            && self.logs_dir.exists()
131            && self.cache_dir.exists()
132            && self.run_dir.exists()
133    }
134}
135
136impl Default for RuntimePaths {
137    fn default() -> Self {
138        Self::discover()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_container_paths() {
148        let paths = RuntimePaths::discover_for(Environment::Kubernetes);
149
150        assert_eq!(paths.config_dir, PathBuf::from("/app/config"));
151        assert_eq!(paths.secrets_dir, PathBuf::from("/app/secrets"));
152        assert_eq!(paths.data_dir, PathBuf::from("/app/data"));
153        assert_eq!(paths.temp_dir, PathBuf::from("/app/tmp"));
154        assert_eq!(paths.logs_dir, PathBuf::from("/app/logs"));
155        assert_eq!(paths.cache_dir, PathBuf::from("/app/cache"));
156        assert_eq!(paths.run_dir, PathBuf::from("/app/run"));
157    }
158
159    #[test]
160    fn test_docker_uses_container_paths() {
161        let docker_paths = RuntimePaths::discover_for(Environment::Docker);
162        let k8s_paths = RuntimePaths::discover_for(Environment::Kubernetes);
163
164        // Docker and K8s should use same container paths
165        assert_eq!(docker_paths, k8s_paths);
166    }
167
168    #[test]
169    fn test_local_paths_use_xdg() {
170        let paths = RuntimePaths::discover_for(Environment::BareMetal);
171
172        // Local paths should be in home directory, not /app
173        assert!(!paths.config_dir.starts_with("/app"));
174        assert!(!paths.data_dir.starts_with("/app"));
175    }
176
177    #[test]
178    fn test_custom_container_base_path() {
179        temp_env::with_var("CONTAINER_BASE_PATH", Some("/custom"), || {
180            let paths = RuntimePaths::discover_for(Environment::Docker);
181            assert_eq!(paths.config_dir, PathBuf::from("/custom/config"));
182        });
183    }
184}