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}
162
163impl RuntimeContext {
164 #[must_use]
169 pub fn detect() -> Self {
170 let environment = Environment::detect();
171
172 let pod_name = std::env::var("POD_NAME").ok().or_else(|| {
173 if environment.is_container() {
174 std::env::var("HOSTNAME").ok()
175 } else {
176 None
177 }
178 });
179
180 let namespace = std::env::var("POD_NAMESPACE").ok().or_else(|| {
181 std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
183 .ok()
184 .map(|s| s.trim().to_string())
185 });
186
187 let node_name = std::env::var("NODE_NAME").ok();
188
189 let container_id = if environment.is_container() {
190 std::env::var("HOSTNAME").ok()
191 } else {
192 None
193 };
194
195 Self {
196 environment,
197 pod_name,
198 namespace,
199 node_name,
200 container_id,
201 }
202 }
203
204 #[must_use]
206 pub fn is_kubernetes(&self) -> bool {
207 self.environment.is_kubernetes()
208 }
209
210 #[must_use]
212 pub fn is_container(&self) -> bool {
213 self.environment.is_container()
214 }
215
216 #[must_use]
218 pub fn is_bare_metal(&self) -> bool {
219 self.environment.is_bare_metal()
220 }
221}
222
223impl Default for RuntimeContext {
224 fn default() -> Self {
225 Self::detect()
226 }
227}
228
229static RUNTIME_CONTEXT: std::sync::OnceLock<RuntimeContext> = std::sync::OnceLock::new();
230
231#[must_use]
236pub fn runtime_context() -> &'static RuntimeContext {
237 RUNTIME_CONTEXT.get_or_init(RuntimeContext::detect)
238}
239
240#[must_use]
248pub fn is_helm() -> bool {
249 if std::env::var("HELM_RELEASE_NAME").is_ok() {
251 return true;
252 }
253
254 let labels_path = Path::new("/etc/podinfo/labels");
256 if labels_path.exists()
257 && let Ok(content) = std::fs::read_to_string(labels_path)
258 {
259 return content.contains("helm.sh/chart")
260 || content.contains("app.kubernetes.io/managed-by=\"Helm\"");
261 }
262
263 false
264}
265
266#[must_use]
270pub fn get_app_env() -> String {
271 std::env::var("APP_ENV")
272 .or_else(|_| std::env::var("ENVIRONMENT"))
273 .or_else(|_| std::env::var("ENV"))
274 .unwrap_or_else(|_| "development".to_string())
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_environment_display() {
283 assert_eq!(Environment::Kubernetes.to_string(), "kubernetes");
284 assert_eq!(Environment::Docker.to_string(), "docker");
285 assert_eq!(Environment::Container.to_string(), "container");
286 assert_eq!(Environment::BareMetal.to_string(), "bare_metal");
287 }
288
289 #[test]
290 fn test_environment_is_container() {
291 assert!(Environment::Kubernetes.is_container());
292 assert!(Environment::Docker.is_container());
293 assert!(Environment::Container.is_container());
294 assert!(!Environment::BareMetal.is_container());
295 }
296
297 #[test]
298 fn test_environment_is_kubernetes() {
299 assert!(Environment::Kubernetes.is_kubernetes());
300 assert!(!Environment::Docker.is_kubernetes());
301 assert!(!Environment::Container.is_kubernetes());
302 assert!(!Environment::BareMetal.is_kubernetes());
303 }
304
305 #[test]
306 fn test_environment_is_bare_metal() {
307 assert!(!Environment::Kubernetes.is_bare_metal());
308 assert!(!Environment::Docker.is_bare_metal());
309 assert!(!Environment::Container.is_bare_metal());
310 assert!(Environment::BareMetal.is_bare_metal());
311 }
312
313 #[test]
314 fn test_get_app_env_default() {
315 temp_env::with_vars(
316 [
317 ("APP_ENV", None::<&str>),
318 ("ENVIRONMENT", None),
319 ("ENV", None),
320 ],
321 || assert_eq!(get_app_env(), "development"),
322 );
323 }
324
325 #[test]
326 fn test_get_app_env_from_app_env() {
327 temp_env::with_var("APP_ENV", Some("production"), || {
328 assert_eq!(get_app_env(), "production");
329 });
330 }
331
332 #[test]
333 fn test_environment_detect_returns_valid() {
334 let env = Environment::detect();
336 assert!(matches!(
337 env,
338 Environment::Kubernetes
339 | Environment::Docker
340 | Environment::Container
341 | Environment::BareMetal
342 ));
343 }
344}