Skip to main content

hyperi_rustlib/
env.rs

1// Project:   hyperi-rustlib
2// File:      src/env.rs
3// Purpose:   Runtime environment detection (K8s, Docker, container, bare metal)
4// Language:  Rust
5//
6// License:   FSL-1.1-ALv2
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Runtime environment detection.
10//!
11//! Detects whether the application is running in Kubernetes, Docker,
12//! a generic container, or on bare metal. This information is used
13//! to configure paths, logging format, and other runtime behaviour.
14
15use std::path::Path;
16
17/// Runtime environment types.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Environment {
20    /// Running in Kubernetes
21    Kubernetes,
22    /// Running in Docker (but not K8s)
23    Docker,
24    /// Running in a generic container (detected via cgroups)
25    Container,
26    /// Running on bare metal / local development
27    BareMetal,
28}
29
30impl Environment {
31    /// Detect the current runtime environment.
32    ///
33    /// Detection priority (highest confidence first):
34    /// 1. Kubernetes service account token exists
35    /// 2. Kubernetes environment variables present
36    /// 3. Docker: `/.dockerenv` file exists
37    /// 4. Container: cgroups contain container markers
38    /// 5. Default: `BareMetal`
39    #[must_use]
40    pub fn detect() -> Self {
41        // Check for Kubernetes first (highest priority)
42        if Self::is_kubernetes_by_token() || Self::is_kubernetes_by_env() {
43            return Self::Kubernetes;
44        }
45
46        // Check for Docker
47        if Self::is_docker_by_file() {
48            return Self::Docker;
49        }
50
51        // Check for generic container via cgroups
52        if Self::is_container_by_cgroups() {
53            return Self::Container;
54        }
55
56        Self::BareMetal
57    }
58
59    /// Check if running in any container environment.
60    #[must_use]
61    pub const fn is_container(&self) -> bool {
62        matches!(self, Self::Kubernetes | Self::Docker | Self::Container)
63    }
64
65    /// Check if running in Kubernetes.
66    #[must_use]
67    pub const fn is_kubernetes(&self) -> bool {
68        matches!(self, Self::Kubernetes)
69    }
70
71    /// Check if running in Docker (but not K8s).
72    #[must_use]
73    pub const fn is_docker(&self) -> bool {
74        matches!(self, Self::Docker)
75    }
76
77    /// Check if running on bare metal.
78    #[must_use]
79    pub const fn is_bare_metal(&self) -> bool {
80        matches!(self, Self::BareMetal)
81    }
82
83    // Detection helpers
84
85    fn is_kubernetes_by_token() -> bool {
86        Path::new("/var/run/secrets/kubernetes.io/serviceaccount/token").exists()
87    }
88
89    fn is_kubernetes_by_env() -> bool {
90        std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
91    }
92
93    fn is_docker_by_file() -> bool {
94        Path::new("/.dockerenv").exists()
95    }
96
97    fn is_container_by_cgroups() -> bool {
98        // Check cgroup v1
99        if let Ok(content) = std::fs::read_to_string("/proc/1/cgroup")
100            && (content.contains("/docker/")
101                || content.contains("/kubepods/")
102                || content.contains("/lxc/")
103                || content.contains("/containerd/"))
104        {
105            return true;
106        }
107
108        // Check cgroup v2 (unified hierarchy)
109        if let Ok(content) = std::fs::read_to_string("/proc/1/mountinfo")
110            && (content.contains("/docker/")
111                || content.contains("/kubepods/")
112                || content.contains("/containerd/"))
113        {
114            return true;
115        }
116
117        false
118    }
119}
120
121impl std::fmt::Display for Environment {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            Self::Kubernetes => write!(f, "kubernetes"),
125            Self::Docker => write!(f, "docker"),
126            Self::Container => write!(f, "container"),
127            Self::BareMetal => write!(f, "bare_metal"),
128        }
129    }
130}
131
132impl Default for Environment {
133    fn default() -> Self {
134        Self::detect()
135    }
136}
137
138/// Check if the application was deployed via Helm.
139///
140/// Looks for Helm-specific labels in Kubernetes downward API.
141#[must_use]
142pub fn is_helm() -> bool {
143    // Check for Helm release name env var (commonly set)
144    if std::env::var("HELM_RELEASE_NAME").is_ok() {
145        return true;
146    }
147
148    // Check for Helm labels via downward API
149    let labels_path = Path::new("/etc/podinfo/labels");
150    if labels_path.exists()
151        && let Ok(content) = std::fs::read_to_string(labels_path)
152    {
153        return content.contains("helm.sh/chart")
154            || content.contains("app.kubernetes.io/managed-by=\"Helm\"");
155    }
156
157    false
158}
159
160/// Get the current application environment name (dev, staging, prod).
161///
162/// Checks in order: `APP_ENV`, `ENVIRONMENT`, `ENV`, defaults to "development".
163#[must_use]
164pub fn get_app_env() -> String {
165    std::env::var("APP_ENV")
166        .or_else(|_| std::env::var("ENVIRONMENT"))
167        .or_else(|_| std::env::var("ENV"))
168        .unwrap_or_else(|_| "development".to_string())
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_environment_display() {
177        assert_eq!(Environment::Kubernetes.to_string(), "kubernetes");
178        assert_eq!(Environment::Docker.to_string(), "docker");
179        assert_eq!(Environment::Container.to_string(), "container");
180        assert_eq!(Environment::BareMetal.to_string(), "bare_metal");
181    }
182
183    #[test]
184    fn test_environment_is_container() {
185        assert!(Environment::Kubernetes.is_container());
186        assert!(Environment::Docker.is_container());
187        assert!(Environment::Container.is_container());
188        assert!(!Environment::BareMetal.is_container());
189    }
190
191    #[test]
192    fn test_environment_is_kubernetes() {
193        assert!(Environment::Kubernetes.is_kubernetes());
194        assert!(!Environment::Docker.is_kubernetes());
195        assert!(!Environment::Container.is_kubernetes());
196        assert!(!Environment::BareMetal.is_kubernetes());
197    }
198
199    #[test]
200    fn test_environment_is_bare_metal() {
201        assert!(!Environment::Kubernetes.is_bare_metal());
202        assert!(!Environment::Docker.is_bare_metal());
203        assert!(!Environment::Container.is_bare_metal());
204        assert!(Environment::BareMetal.is_bare_metal());
205    }
206
207    #[test]
208    fn test_get_app_env_default() {
209        temp_env::with_vars(
210            [
211                ("APP_ENV", None::<&str>),
212                ("ENVIRONMENT", None),
213                ("ENV", None),
214            ],
215            || assert_eq!(get_app_env(), "development"),
216        );
217    }
218
219    #[test]
220    fn test_get_app_env_from_app_env() {
221        temp_env::with_var("APP_ENV", Some("production"), || {
222            assert_eq!(get_app_env(), "production");
223        });
224    }
225
226    #[test]
227    fn test_environment_detect_returns_valid() {
228        // Just ensure detect() doesn't panic and returns a valid variant
229        let env = Environment::detect();
230        assert!(matches!(
231            env,
232            Environment::Kubernetes
233                | Environment::Docker
234                | Environment::Container
235                | Environment::BareMetal
236        ));
237    }
238}