1use std::path::Path;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum Environment {
20 Kubernetes,
22 Docker,
24 Container,
26 BareMetal,
28}
29
30impl Environment {
31 #[must_use]
40 pub fn detect() -> Self {
41 if Self::is_kubernetes_by_token() || Self::is_kubernetes_by_env() {
43 return Self::Kubernetes;
44 }
45
46 if Self::is_docker_by_file() {
48 return Self::Docker;
49 }
50
51 if Self::is_container_by_cgroups() {
53 return Self::Container;
54 }
55
56 Self::BareMetal
57 }
58
59 #[must_use]
61 pub const fn is_container(&self) -> bool {
62 matches!(self, Self::Kubernetes | Self::Docker | Self::Container)
63 }
64
65 #[must_use]
67 pub const fn is_kubernetes(&self) -> bool {
68 matches!(self, Self::Kubernetes)
69 }
70
71 #[must_use]
73 pub const fn is_docker(&self) -> bool {
74 matches!(self, Self::Docker)
75 }
76
77 #[must_use]
79 pub const fn is_bare_metal(&self) -> bool {
80 matches!(self, Self::BareMetal)
81 }
82
83 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 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 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#[derive(Debug, Clone)]
150pub struct RuntimeContext {
151 pub environment: Environment,
153 pub pod_name: Option<String>,
155 pub namespace: Option<String>,
157 pub node_name: Option<String>,
159 pub container_id: Option<String>,
161 pub memory_limit_bytes: Option<u64>,
163 pub cpu_quota_cores: Option<f64>,
165}
166
167impl RuntimeContext {
168 #[must_use]
173 pub fn detect() -> Self {
174 let environment = Environment::detect();
175
176 let pod_name = std::env::var("POD_NAME").ok().or_else(|| {
177 if environment.is_container() {
178 std::env::var("HOSTNAME").ok()
179 } else {
180 None
181 }
182 });
183
184 let namespace = std::env::var("POD_NAMESPACE").ok().or_else(|| {
185 std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
187 .ok()
188 .map(|s| s.trim().to_string())
189 });
190
191 let node_name = std::env::var("NODE_NAME").ok();
192
193 let container_id = if environment.is_container() {
194 std::env::var("HOSTNAME").ok()
195 } else {
196 None
197 };
198
199 let memory_limit_bytes = if environment.is_container() {
201 read_cgroup_memory_limit()
202 } else {
203 None
204 };
205
206 let cpu_quota_cores = if environment.is_container() {
207 read_cgroup_cpu_quota()
208 } else {
209 None
210 };
211
212 Self {
213 environment,
214 pod_name,
215 namespace,
216 node_name,
217 container_id,
218 memory_limit_bytes,
219 cpu_quota_cores,
220 }
221 }
222
223 #[must_use]
225 pub fn is_kubernetes(&self) -> bool {
226 self.environment.is_kubernetes()
227 }
228
229 #[must_use]
231 pub fn is_container(&self) -> bool {
232 self.environment.is_container()
233 }
234
235 #[must_use]
237 pub fn is_bare_metal(&self) -> bool {
238 self.environment.is_bare_metal()
239 }
240}
241
242impl Default for RuntimeContext {
243 fn default() -> Self {
244 Self::detect()
245 }
246}
247
248static RUNTIME_CONTEXT: std::sync::OnceLock<RuntimeContext> = std::sync::OnceLock::new();
249
250#[must_use]
255pub fn runtime_context() -> &'static RuntimeContext {
256 RUNTIME_CONTEXT.get_or_init(RuntimeContext::detect)
257}
258
259fn read_cgroup_memory_limit() -> Option<u64> {
265 let content = std::fs::read_to_string("/sys/fs/cgroup/memory.max").ok()?;
266 let trimmed = content.trim();
267 if trimmed == "max" {
268 return None; }
270 trimmed.parse::<u64>().ok()
271}
272
273fn read_cgroup_cpu_quota() -> Option<f64> {
277 let content = std::fs::read_to_string("/sys/fs/cgroup/cpu.max").ok()?;
278 let parts: Vec<&str> = content.split_whitespace().collect();
279 if parts.len() < 2 || parts[0] == "max" {
280 return None; }
282 let quota: f64 = parts[0].parse().ok()?;
283 let period: f64 = parts[1].parse().ok()?;
284 if period > 0.0 {
285 Some(quota / period)
286 } else {
287 None
288 }
289}
290
291#[must_use]
299pub fn is_helm() -> bool {
300 if std::env::var("HELM_RELEASE_NAME").is_ok() {
302 return true;
303 }
304
305 let labels_path = Path::new("/etc/podinfo/labels");
307 if labels_path.exists()
308 && let Ok(content) = std::fs::read_to_string(labels_path)
309 {
310 return content.contains("helm.sh/chart")
311 || content.contains("app.kubernetes.io/managed-by=\"Helm\"");
312 }
313
314 false
315}
316
317#[must_use]
321pub fn get_app_env() -> String {
322 std::env::var("APP_ENV")
323 .or_else(|_| std::env::var("ENVIRONMENT"))
324 .or_else(|_| std::env::var("ENV"))
325 .unwrap_or_else(|_| "development".to_string())
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn test_environment_display() {
334 assert_eq!(Environment::Kubernetes.to_string(), "kubernetes");
335 assert_eq!(Environment::Docker.to_string(), "docker");
336 assert_eq!(Environment::Container.to_string(), "container");
337 assert_eq!(Environment::BareMetal.to_string(), "bare_metal");
338 }
339
340 #[test]
341 fn test_environment_is_container() {
342 assert!(Environment::Kubernetes.is_container());
343 assert!(Environment::Docker.is_container());
344 assert!(Environment::Container.is_container());
345 assert!(!Environment::BareMetal.is_container());
346 }
347
348 #[test]
349 fn test_environment_is_kubernetes() {
350 assert!(Environment::Kubernetes.is_kubernetes());
351 assert!(!Environment::Docker.is_kubernetes());
352 assert!(!Environment::Container.is_kubernetes());
353 assert!(!Environment::BareMetal.is_kubernetes());
354 }
355
356 #[test]
357 fn test_environment_is_bare_metal() {
358 assert!(!Environment::Kubernetes.is_bare_metal());
359 assert!(!Environment::Docker.is_bare_metal());
360 assert!(!Environment::Container.is_bare_metal());
361 assert!(Environment::BareMetal.is_bare_metal());
362 }
363
364 #[test]
365 fn test_get_app_env_default() {
366 temp_env::with_vars(
367 [
368 ("APP_ENV", None::<&str>),
369 ("ENVIRONMENT", None),
370 ("ENV", None),
371 ],
372 || assert_eq!(get_app_env(), "development"),
373 );
374 }
375
376 #[test]
377 fn test_get_app_env_from_app_env() {
378 temp_env::with_var("APP_ENV", Some("production"), || {
379 assert_eq!(get_app_env(), "production");
380 });
381 }
382
383 #[test]
384 fn test_environment_detect_returns_valid() {
385 let env = Environment::detect();
387 assert!(matches!(
388 env,
389 Environment::Kubernetes
390 | Environment::Docker
391 | Environment::Container
392 | Environment::BareMetal
393 ));
394 }
395}