use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Environment {
Kubernetes,
Docker,
Container,
BareMetal,
}
impl Environment {
#[must_use]
pub fn detect() -> Self {
if Self::is_kubernetes_by_token() || Self::is_kubernetes_by_env() {
return Self::Kubernetes;
}
if Self::is_docker_by_file() {
return Self::Docker;
}
if Self::is_container_by_cgroups() {
return Self::Container;
}
Self::BareMetal
}
#[must_use]
pub const fn is_container(&self) -> bool {
matches!(self, Self::Kubernetes | Self::Docker | Self::Container)
}
#[must_use]
pub const fn is_kubernetes(&self) -> bool {
matches!(self, Self::Kubernetes)
}
#[must_use]
pub const fn is_docker(&self) -> bool {
matches!(self, Self::Docker)
}
#[must_use]
pub const fn is_bare_metal(&self) -> bool {
matches!(self, Self::BareMetal)
}
fn is_kubernetes_by_token() -> bool {
Path::new("/var/run/secrets/kubernetes.io/serviceaccount/token").exists()
}
fn is_kubernetes_by_env() -> bool {
std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
}
fn is_docker_by_file() -> bool {
Path::new("/.dockerenv").exists()
}
fn is_container_by_cgroups() -> bool {
if let Ok(content) = std::fs::read_to_string("/proc/1/cgroup")
&& (content.contains("/docker/")
|| content.contains("/kubepods/")
|| content.contains("/lxc/")
|| content.contains("/containerd/"))
{
return true;
}
if let Ok(content) = std::fs::read_to_string("/proc/1/mountinfo")
&& (content.contains("/docker/")
|| content.contains("/kubepods/")
|| content.contains("/containerd/"))
{
return true;
}
false
}
}
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Kubernetes => write!(f, "kubernetes"),
Self::Docker => write!(f, "docker"),
Self::Container => write!(f, "container"),
Self::BareMetal => write!(f, "bare_metal"),
}
}
}
impl Default for Environment {
fn default() -> Self {
Self::detect()
}
}
#[derive(Debug, Clone)]
pub struct RuntimeContext {
pub environment: Environment,
pub pod_name: Option<String>,
pub namespace: Option<String>,
pub node_name: Option<String>,
pub container_id: Option<String>,
pub memory_limit_bytes: Option<u64>,
pub cpu_quota_cores: Option<f64>,
}
impl RuntimeContext {
#[must_use]
pub fn detect() -> Self {
let environment = Environment::detect();
let pod_name = std::env::var("POD_NAME").ok().or_else(|| {
if environment.is_container() {
std::env::var("HOSTNAME").ok()
} else {
None
}
});
let namespace = std::env::var("POD_NAMESPACE").ok().or_else(|| {
std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
.ok()
.map(|s| s.trim().to_string())
});
let node_name = std::env::var("NODE_NAME").ok();
let container_id = if environment.is_container() {
std::env::var("HOSTNAME").ok()
} else {
None
};
let memory_limit_bytes = if environment.is_container() {
read_cgroup_memory_limit()
} else {
None
};
let cpu_quota_cores = if environment.is_container() {
read_cgroup_cpu_quota()
} else {
None
};
Self {
environment,
pod_name,
namespace,
node_name,
container_id,
memory_limit_bytes,
cpu_quota_cores,
}
}
#[must_use]
pub fn is_kubernetes(&self) -> bool {
self.environment.is_kubernetes()
}
#[must_use]
pub fn is_container(&self) -> bool {
self.environment.is_container()
}
#[must_use]
pub fn is_bare_metal(&self) -> bool {
self.environment.is_bare_metal()
}
}
impl Default for RuntimeContext {
fn default() -> Self {
Self::detect()
}
}
static RUNTIME_CONTEXT: std::sync::OnceLock<RuntimeContext> = std::sync::OnceLock::new();
#[must_use]
pub fn runtime_context() -> &'static RuntimeContext {
RUNTIME_CONTEXT.get_or_init(RuntimeContext::detect)
}
fn read_cgroup_memory_limit() -> Option<u64> {
let content = std::fs::read_to_string("/sys/fs/cgroup/memory.max").ok()?;
let trimmed = content.trim();
if trimmed == "max" {
return None; }
trimmed.parse::<u64>().ok()
}
fn read_cgroup_cpu_quota() -> Option<f64> {
let content = std::fs::read_to_string("/sys/fs/cgroup/cpu.max").ok()?;
let parts: Vec<&str> = content.split_whitespace().collect();
if parts.len() < 2 || parts[0] == "max" {
return None; }
let quota: f64 = parts[0].parse().ok()?;
let period: f64 = parts[1].parse().ok()?;
if period > 0.0 {
Some(quota / period)
} else {
None
}
}
#[must_use]
pub fn is_helm() -> bool {
if std::env::var("HELM_RELEASE_NAME").is_ok() {
return true;
}
let labels_path = Path::new("/etc/podinfo/labels");
if labels_path.exists()
&& let Ok(content) = std::fs::read_to_string(labels_path)
{
return content.contains("helm.sh/chart")
|| content.contains("app.kubernetes.io/managed-by=\"Helm\"");
}
false
}
#[must_use]
pub fn get_app_env() -> String {
std::env::var("APP_ENV")
.or_else(|_| std::env::var("ENVIRONMENT"))
.or_else(|_| std::env::var("ENV"))
.unwrap_or_else(|_| "development".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment_display() {
assert_eq!(Environment::Kubernetes.to_string(), "kubernetes");
assert_eq!(Environment::Docker.to_string(), "docker");
assert_eq!(Environment::Container.to_string(), "container");
assert_eq!(Environment::BareMetal.to_string(), "bare_metal");
}
#[test]
fn test_environment_is_container() {
assert!(Environment::Kubernetes.is_container());
assert!(Environment::Docker.is_container());
assert!(Environment::Container.is_container());
assert!(!Environment::BareMetal.is_container());
}
#[test]
fn test_environment_is_kubernetes() {
assert!(Environment::Kubernetes.is_kubernetes());
assert!(!Environment::Docker.is_kubernetes());
assert!(!Environment::Container.is_kubernetes());
assert!(!Environment::BareMetal.is_kubernetes());
}
#[test]
fn test_environment_is_bare_metal() {
assert!(!Environment::Kubernetes.is_bare_metal());
assert!(!Environment::Docker.is_bare_metal());
assert!(!Environment::Container.is_bare_metal());
assert!(Environment::BareMetal.is_bare_metal());
}
#[test]
fn test_get_app_env_default() {
temp_env::with_vars(
[
("APP_ENV", None::<&str>),
("ENVIRONMENT", None),
("ENV", None),
],
|| assert_eq!(get_app_env(), "development"),
);
}
#[test]
fn test_get_app_env_from_app_env() {
temp_env::with_var("APP_ENV", Some("production"), || {
assert_eq!(get_app_env(), "production");
});
}
#[test]
fn test_environment_detect_returns_valid() {
let env = Environment::detect();
assert!(matches!(
env,
Environment::Kubernetes
| Environment::Docker
| Environment::Container
| Environment::BareMetal
));
}
#[test]
fn test_runtime_context_detect_does_not_panic() {
let ctx = RuntimeContext::detect();
assert!(matches!(
ctx.environment,
Environment::Kubernetes
| Environment::Docker
| Environment::Container
| Environment::BareMetal
));
}
#[test]
fn test_runtime_context_bare_metal_has_no_k8s_fields() {
let ctx = RuntimeContext::detect();
if ctx.environment.is_bare_metal() {
assert!(
ctx.node_name.is_none(),
"node_name should be None on bare metal"
);
}
}
#[test]
fn test_runtime_context_reads_pod_name_env() {
temp_env::with_vars(
[
("POD_NAME", Some("test-pod-123")),
("KUBERNETES_SERVICE_HOST", Some("10.0.0.1")),
],
|| {
let ctx = RuntimeContext::detect();
assert_eq!(ctx.pod_name.as_deref(), Some("test-pod-123"));
},
);
}
#[test]
fn test_runtime_context_reads_namespace_env() {
temp_env::with_var("POD_NAMESPACE", Some("production"), || {
let ctx = RuntimeContext::detect();
assert_eq!(ctx.namespace.as_deref(), Some("production"));
});
}
#[test]
fn test_runtime_context_reads_node_name_env() {
temp_env::with_var("NODE_NAME", Some("node-1"), || {
let ctx = RuntimeContext::detect();
assert_eq!(ctx.node_name.as_deref(), Some("node-1"));
});
}
#[test]
fn test_runtime_context_global_singleton() {
let ctx1 = runtime_context();
let ctx2 = runtime_context();
assert_eq!(ctx1.environment, ctx2.environment);
assert_eq!(ctx1.pod_name, ctx2.pod_name);
}
#[test]
fn test_runtime_context_is_kubernetes_convenience() {
let mut ctx = RuntimeContext::detect();
ctx.environment = Environment::Kubernetes;
assert!(ctx.is_kubernetes());
assert!(ctx.is_container());
assert!(!ctx.is_bare_metal());
}
#[test]
fn test_runtime_context_is_bare_metal_convenience() {
let mut ctx = RuntimeContext::detect();
ctx.environment = Environment::BareMetal;
assert!(!ctx.is_kubernetes());
assert!(!ctx.is_container());
assert!(ctx.is_bare_metal());
}
#[test]
fn test_read_cgroup_memory_limit_returns_option() {
let limit = read_cgroup_memory_limit();
let _ = limit;
}
#[test]
fn test_read_cgroup_cpu_quota_returns_option() {
let quota = read_cgroup_cpu_quota();
let _ = quota;
}
#[test]
fn test_prestop_delay_default_bare_metal() {
temp_env::with_var("PRESTOP_DELAY_SECS", None::<&str>, || {
let ctx = RuntimeContext::detect();
if ctx.environment.is_bare_metal() {
assert!(!ctx.is_kubernetes());
}
});
}
}