use std::path::Path;
use opentelemetry::KeyValue;
use opentelemetry_sdk::{resource::ResourceDetector, Resource};
use opentelemetry_semantic_conventions::attribute as semco;
use thiserror::Error;
use super::{
imds::{ImdsClient, ImdsError, ImdsProvider},
utils::{debug_on_error, info_on_error, non_empty, opt_kv, warn_on_error},
};
const DETECTOR: &str = "aws_eks";
const K8S_NAMESPACE_FILE_PATH: &str = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
const CGROUP_FILE_PATH: &str = "/proc/self/cgroup";
const MOUNTINFO_FILE_PATH: &str = "/proc/self/mountinfo";
const MIN_CONTAINER_ID_LEN: usize = 32;
const MAX_CONTAINER_ID_LEN: usize = 64;
const EKS_CLUSTER_NAME_TAG_PATH: &str = "tags/instance/aws:eks:cluster-name";
const EKS_CLUSTER_NAME_ENV_VAR: &str = "AWS_CLUSTER_NAME";
pub struct EksResourceDetector;
impl ResourceDetector for EksResourceDetector {
fn detect(&self) -> Resource {
Self::detect_from(
ImdsClient::new(),
Path::new(K8S_NAMESPACE_FILE_PATH),
Path::new(CGROUP_FILE_PATH),
Path::new(MOUNTINFO_FILE_PATH),
)
}
}
impl EksResourceDetector {
fn detect_from<P: ImdsProvider>(
imds: Result<P, ImdsError>,
namespace_path: &Path,
cgroup_path: &Path,
mountinfo_path: &Path,
) -> Resource {
let Some(namespace) = info_on_error(DETECTOR, get_namespace(namespace_path)) else {
return Resource::builder_empty().build();
};
let imds = debug_on_error(DETECTOR, imds);
let document = imds
.as_ref()
.and_then(|imds| warn_on_error(DETECTOR, imds.get_identity_document()));
let on_aws_ec2_node = document.is_some();
let (region, account_id, ec2_document_attributes) = document
.map(|document| {
let host_arch = document
.host_arch()
.map(|arch| KeyValue::new(semco::HOST_ARCH, arch));
(
document.region,
document.account_id,
[
host_arch,
opt_kv(semco::CLOUD_AVAILABILITY_ZONE, document.availability_zone),
opt_kv(semco::HOST_ID, document.instance_id),
opt_kv(semco::HOST_TYPE, document.instance_type),
opt_kv(semco::HOST_IMAGE_ID, document.image_id),
],
)
})
.unwrap_or_default();
let cluster_name = warn_on_error(
DETECTOR,
imds.as_ref()
.and_then(|imds| debug_on_error(DETECTOR, imds.get(EKS_CLUSTER_NAME_TAG_PATH)))
.and_then(non_empty)
.or_else(|| std::env::var(EKS_CLUSTER_NAME_ENV_VAR).ok())
.ok_or(EksError::ClusterNameNotFound),
);
if !on_aws_ec2_node && cluster_name.is_none() {
return Resource::builder_empty().build();
};
let region = region.or_else(|| std::env::var("AWS_REGION").ok());
let account_id = account_id.or_else(|| std::env::var("AWS_ACCOUNT_ID").ok());
let cluster_arn = match (®ion, &account_id, &cluster_name) {
(Some(region), Some(account_id), Some(cluster_name)) => {
let partition = map_region_to_partition(region);
Some(format!(
"arn:{partition}:eks:{region}:{account_id}:cluster/{cluster_name}"
))
}
_ => None,
};
let attribute_options = [
Some(KeyValue::new(semco::CLOUD_PROVIDER, "aws")),
Some(KeyValue::new(semco::CLOUD_PLATFORM, "aws_eks")),
Some(KeyValue::new(semco::K8S_NAMESPACE_NAME, namespace)),
opt_kv(semco::K8S_POD_NAME, std::env::var("HOSTNAME").ok()),
opt_kv(semco::K8S_POD_UID, std::env::var("POD_UID").ok()),
opt_kv(semco::K8S_NODE_NAME, std::env::var("NODE_NAME").ok()),
opt_kv(semco::K8S_CLUSTER_NAME, cluster_name),
opt_kv(semco::AWS_EKS_CLUSTER_ARN, cluster_arn),
opt_kv(
semco::CONTAINER_ID,
warn_on_error(DETECTOR, get_container_id(cgroup_path, mountinfo_path)),
),
opt_kv(
semco::HOST_NAME,
imds.as_ref()
.and_then(|imds| warn_on_error(DETECTOR, imds.get("hostname"))),
),
opt_kv(semco::CLOUD_REGION, region),
opt_kv(semco::CLOUD_ACCOUNT_ID, account_id),
];
Resource::builder_empty()
.with_attributes(ec2_document_attributes.into_iter().flatten())
.with_attributes(attribute_options.into_iter().flatten())
.build()
}
}
#[derive(Debug, Error)]
enum EksError {
#[error("Cannot read file {path}: {error}")]
FsError {
path: String,
#[source]
error: std::io::Error,
},
#[error("Empty file at {K8S_NAMESPACE_FILE_PATH}")]
EmptyNamespace,
#[error("Could not extract the container id from {CGROUP_FILE_PATH} or {MOUNTINFO_FILE_PATH}")]
NoContainerId,
#[error("Could not find the cluster name, neither in the AWS EC2 tags through IMDS nor from the `{EKS_CLUSTER_NAME_ENV_VAR}` environment variable")]
ClusterNameNotFound,
}
fn get_namespace(path: &Path) -> Result<String, EksError> {
let namespace = std::fs::read_to_string(path)
.map_err(|error| EksError::FsError {
path: path.to_string_lossy().into_owned(),
error,
})?
.trim()
.to_owned();
if namespace.is_empty() {
Err(EksError::EmptyNamespace)
} else {
Ok(namespace)
}
}
fn get_container_id(cgroup_path: &Path, mountinfo_path: &Path) -> Result<String, EksError> {
if let Ok(content) = std::fs::read_to_string(cgroup_path) {
if let Some(id) = content.lines().find_map(container_id_from_cgroup_line) {
return Ok(id.to_owned());
}
}
if let Ok(content) = std::fs::read_to_string(mountinfo_path) {
if let Some(id) = content.lines().find_map(container_id_from_mountinfo_line) {
return Ok(id.to_owned());
}
}
Err(EksError::NoContainerId)
}
fn container_id_from_cgroup_line(line: &str) -> Option<&str> {
let last_segment = line[line.rfind('/')? + 1..].trim();
let candidate = match last_segment.rfind([':', '-']) {
Some(index) => &last_segment[index + 1..],
None => last_segment,
};
let candidate = candidate
.split_once('.')
.map_or(candidate, |(before, _)| before);
is_valid_container_id(candidate).then_some(candidate)
}
fn container_id_from_mountinfo_line(line: &str) -> Option<&str> {
let mut fields = line.split_once(" - ")?.0.split_whitespace();
let root = fields.nth(3)?;
let mount_point = fields.next()?;
if mount_point != "/etc/hostname" {
return None;
}
let mut previous = "";
for segment in root.split('/') {
if matches!(previous, "containers" | "overlay-containers") && is_valid_container_id(segment)
{
return Some(segment);
}
previous = segment;
}
None
}
fn is_valid_container_id(candidate: &str) -> bool {
(MIN_CONTAINER_ID_LEN..=MAX_CONTAINER_ID_LEN).contains(&candidate.len())
&& candidate.bytes().all(|b| b.is_ascii_hexdigit())
}
fn map_region_to_partition(region: &str) -> &'static str {
match region {
r if r.starts_with("eusc-") => "aws-eusc",
r if r.starts_with("cn-") => "aws-cn",
r if r.starts_with("us-gov-") => "aws-us-gov",
r if r.starts_with("us-iso-") => "aws-iso",
r if r.starts_with("us-isob-") => "aws-iso-b",
r if r.starts_with("us-isof-") => "aws-iso-f",
r if r.starts_with("eu-isoe-") => "aws-iso-e",
_ => "aws",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::imds::{tests::FakeImdsClient, ImdsError};
use sealed_test::prelude::*;
use std::io::Write;
const ID64: &str = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
const ID32: &str = "aabbccddeeff00112233445566778899";
const FULL_IMDS_DOC: &str = r#"
{
"accountId": "123456789012",
"region": "us-east-1",
"availabilityZone": "us-east-1c",
"instanceId": "i-0node",
"instanceType": "m5.large",
"imageId": "ami-0nodeimage",
"architecture": "x86_64"
}
"#;
#[test]
fn is_valid_container_id_valid() {
assert!(is_valid_container_id(ID32));
assert!(is_valid_container_id(ID64));
let mixed = "aAbBcCdDeEfF001122334455aAbBcCdDeEfF0011223344";
assert!(is_valid_container_id(mixed));
}
#[test]
fn is_valid_container_id_invalid() {
let short31 = "aabbccddeeff0011223344556677889";
assert_eq!(short31.len(), 31);
assert!(!is_valid_container_id(short31));
let long65 = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899a";
assert_eq!(long65.len(), 65);
assert!(!is_valid_container_id(long65));
let with_g = "aabbccddeeff00112233445566778899aabbccddeeff0011223344556677889g";
assert_eq!(with_g.len(), 64);
assert!(!is_valid_container_id(with_g));
let with_dash = "aabbccddeeff00112233445566778899aabbccddeeff001122334455667788-9";
assert_eq!(with_dash.len(), 64);
assert!(!is_valid_container_id(with_dash));
assert!(!is_valid_container_id(""));
}
#[test]
fn cgroup_line_plain_docker_v1() {
let line = format!("12:cpuset:/docker/{ID64}");
assert_eq!(container_id_from_cgroup_line(&line), Some(ID64));
}
#[test]
fn cgroup_line_kubernetes_cgroupfs() {
let line = format!(
"11:memory:/kubepods/besteffort/podabcd1234-ef56-7890-abcd-ef1234567890/{ID64}"
);
assert_eq!(container_id_from_cgroup_line(&line), Some(ID64));
}
#[test]
fn cgroup_line_docker_systemd() {
let line = format!("10:cpu:/system.slice/docker-{ID64}.scope");
assert_eq!(container_id_from_cgroup_line(&line), Some(ID64));
}
#[test]
fn cgroup_line_containerd_systemd() {
let line = format!("9:cpuset:/system.slice/cri-containerd-{ID64}.scope");
assert_eq!(container_id_from_cgroup_line(&line), Some(ID64));
}
#[test]
fn cgroup_line_crio_systemd() {
let line = format!("8:memory:/system.slice/crio-{ID64}.scope");
assert_eq!(container_id_from_cgroup_line(&line), Some(ID64));
}
#[test]
fn cgroup_line_rejected() {
assert_eq!(container_id_from_cgroup_line("0::/"), None);
assert_eq!(
container_id_from_cgroup_line("1:name=systemd:/user.slice/user-1000.slice"),
None
);
let bad_id = "aabbccddeeff00112233445566778899aabbccddeeff001122334455667788zz";
let line = format!("12:cpuset:/docker/{bad_id}");
assert_eq!(container_id_from_cgroup_line(&line), None);
}
fn mountinfo_line(root: &str, mount_point: &str) -> String {
format!("36 35 0:33 {root} {mount_point} rw - ext4 /dev/sda1 rw")
}
#[test]
fn mountinfo_line_containers_valid() {
let root = format!("/docker/containers/{ID64}/hostname");
let line = mountinfo_line(&root, "/etc/hostname");
assert_eq!(container_id_from_mountinfo_line(&line), Some(ID64));
}
#[test]
fn mountinfo_line_overlay_containers_valid() {
let root = format!("/var/lib/overlay-containers/{ID64}/userdata/hostname");
let line = mountinfo_line(&root, "/etc/hostname");
assert_eq!(container_id_from_mountinfo_line(&line), Some(ID64));
}
#[test]
fn mountinfo_line_wrong_mount_point() {
let root = format!("/docker/containers/{ID64}/hostname");
let line = mountinfo_line(&root, "/etc/hosts");
assert_eq!(container_id_from_mountinfo_line(&line), None);
}
#[test]
fn mountinfo_line_no_separator() {
let line = format!("36 35 0:33 /docker/containers/{ID64}/hostname /etc/hostname rw");
assert_eq!(container_id_from_mountinfo_line(&line), None);
}
#[test]
fn mountinfo_line_invalid_id_after_containers() {
let root = "/docker/containers/not-a-valid-hex-id/hostname";
let line = mountinfo_line(root, "/etc/hostname");
assert_eq!(container_id_from_mountinfo_line(&line), None);
}
fn temp_file(content: &str) -> (tempfile::NamedTempFile, std::path::PathBuf) {
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(f, "{content}").unwrap();
let path = f.path().to_path_buf();
(f, path)
}
#[test]
fn partition_default_aws() {
for region in &[
"us-east-1",
"us-west-2",
"eu-west-1",
"eu-central-1",
"ap-southeast-2",
"ap-northeast-1",
"sa-east-1",
"ca-central-1",
"me-south-1",
"af-south-1",
"il-central-1",
"mx-central-1",
] {
assert_eq!(
map_region_to_partition(region),
"aws",
"expected partition 'aws' for region '{region}'"
);
}
}
#[test]
fn partition_aws_cn() {
assert_eq!(map_region_to_partition("cn-north-1"), "aws-cn");
assert_eq!(map_region_to_partition("cn-northwest-1"), "aws-cn");
}
#[test]
fn partition_aws_us_gov() {
assert_eq!(map_region_to_partition("us-gov-east-1"), "aws-us-gov");
assert_eq!(map_region_to_partition("us-gov-west-1"), "aws-us-gov");
}
#[test]
fn partition_aws_eusc() {
assert_eq!(map_region_to_partition("eusc-de-east-1"), "aws-eusc");
}
#[test]
fn partition_aws_iso() {
assert_eq!(map_region_to_partition("us-iso-east-1"), "aws-iso");
assert_eq!(map_region_to_partition("us-iso-west-1"), "aws-iso");
}
#[test]
fn partition_aws_iso_b() {
assert_eq!(map_region_to_partition("us-isob-east-1"), "aws-iso-b");
assert_eq!(map_region_to_partition("us-isob-west-1"), "aws-iso-b");
}
#[test]
fn partition_aws_iso_e() {
assert_eq!(map_region_to_partition("eu-isoe-west-1"), "aws-iso-e");
}
#[test]
fn partition_aws_iso_f() {
assert_eq!(map_region_to_partition("us-isof-east-1"), "aws-iso-f");
assert_eq!(map_region_to_partition("us-isof-south-1"), "aws-iso-f");
}
#[test]
fn partition_fallback_for_unknown_and_empty() {
assert_eq!(map_region_to_partition(""), "aws");
assert_eq!(map_region_to_partition("garbage"), "aws");
assert_eq!(map_region_to_partition("unknown-region-99"), "aws");
}
#[sealed_test]
fn detect_from_no_namespace_file_returns_empty() {
let resource = EksResourceDetector::detect_from(
Ok(FakeImdsClient::new()),
Path::new("/nonexistent/path/to/namespace"),
Path::new("/nonexistent/cgroup"),
Path::new("/nonexistent/mountinfo"),
);
assert_eq!(resource, Resource::builder_empty().build());
}
#[sealed_test]
fn detect_from_no_aws_tie_returns_empty() {
let (_ns_file, ns_path) = temp_file("default");
let (_cgroup_file, cgroup_path) = temp_file("0::/\n");
let (_mi_file, mi_path) = temp_file("");
temp_env::with_vars(
[
("AWS_CLUSTER_NAME", None::<&str>),
("AWS_REGION", None::<&str>),
("AWS_ACCOUNT_ID", None::<&str>),
],
|| {
let resource = EksResourceDetector::detect_from(
Err::<FakeImdsClient, _>(ImdsError::EmptyAuthToken),
&ns_path,
&cgroup_path,
&mi_path,
);
assert_eq!(resource, Resource::builder_empty().build());
},
);
}
#[sealed_test]
fn detect_from_happy_path_with_imds() {
let (_ns_file, ns_path) = temp_file("kube-system");
let (_cgroup_file, cgroup_path) =
temp_file(&format!("11:memory:/kubepods/besteffort/podabc/{ID64}\n"));
let (_mi_file, mi_path) = temp_file("");
let fake = FakeImdsClient::new()
.with_document(FULL_IMDS_DOC)
.with_get(EKS_CLUSTER_NAME_TAG_PATH, "my-eks-cluster")
.with_get("services/partition", "aws")
.with_get("hostname", "ip-10-0-1-5.ec2.internal");
temp_env::with_vars(
[
("HOSTNAME", Some("my-pod-xyz")),
("POD_UID", Some("pod-uid-123")),
("NODE_NAME", Some("ip-10-0-1-5")),
("AWS_CLUSTER_NAME", None::<&str>),
("AWS_REGION", None::<&str>),
("AWS_ACCOUNT_ID", None::<&str>),
],
|| {
let resource =
EksResourceDetector::detect_from(Ok(fake), &ns_path, &cgroup_path, &mi_path);
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_eks"),
KeyValue::new(semco::HOST_ARCH, "amd64"),
KeyValue::new(semco::CLOUD_AVAILABILITY_ZONE, "us-east-1c"),
KeyValue::new(semco::HOST_ID, "i-0node"),
KeyValue::new(semco::HOST_TYPE, "m5.large"),
KeyValue::new(semco::HOST_IMAGE_ID, "ami-0nodeimage"),
KeyValue::new(semco::K8S_NAMESPACE_NAME, "kube-system"),
KeyValue::new(semco::K8S_POD_NAME, "my-pod-xyz"),
KeyValue::new(semco::K8S_POD_UID, "pod-uid-123"),
KeyValue::new(semco::K8S_NODE_NAME, "ip-10-0-1-5"),
KeyValue::new(semco::K8S_CLUSTER_NAME, "my-eks-cluster"),
KeyValue::new(
semco::AWS_EKS_CLUSTER_ARN,
"arn:aws:eks:us-east-1:123456789012:cluster/my-eks-cluster",
),
KeyValue::new(semco::CONTAINER_ID, ID64),
KeyValue::new(semco::HOST_NAME, "ip-10-0-1-5.ec2.internal"),
KeyValue::new(semco::CLOUD_REGION, "us-east-1"),
KeyValue::new(semco::CLOUD_ACCOUNT_ID, "123456789012"),
])
.build();
assert_eq!(resource, expected);
},
);
}
#[sealed_test]
fn detect_from_fargate_fallback_via_env_vars() {
let (_ns_file, ns_path) = temp_file("default");
let (_cgroup_file, cgroup_path) = temp_file("0::/\n");
let (_mi_file, mi_path) = temp_file("");
temp_env::with_vars(
[
("AWS_CLUSTER_NAME", Some("fargate-cluster")),
("AWS_REGION", Some("eu-west-1")),
("AWS_ACCOUNT_ID", Some("999888777666")),
("HOSTNAME", Some("fargate-pod-abc")),
("POD_UID", None::<&str>),
("NODE_NAME", None::<&str>),
],
|| {
let resource = EksResourceDetector::detect_from(
Err::<FakeImdsClient, _>(ImdsError::EmptyAuthToken),
&ns_path,
&cgroup_path,
&mi_path,
);
let expected = Resource::builder_empty()
.with_attributes([
KeyValue::new(semco::CLOUD_PROVIDER, "aws"),
KeyValue::new(semco::CLOUD_PLATFORM, "aws_eks"),
KeyValue::new(semco::K8S_NAMESPACE_NAME, "default"),
KeyValue::new(semco::K8S_POD_NAME, "fargate-pod-abc"),
KeyValue::new(semco::K8S_CLUSTER_NAME, "fargate-cluster"),
KeyValue::new(
semco::AWS_EKS_CLUSTER_ARN,
"arn:aws:eks:eu-west-1:999888777666:cluster/fargate-cluster",
),
KeyValue::new(semco::CLOUD_REGION, "eu-west-1"),
KeyValue::new(semco::CLOUD_ACCOUNT_ID, "999888777666"),
])
.build();
assert_eq!(resource, expected);
},
);
}
#[sealed_test]
fn detect_from_container_id_from_mountinfo_when_cgroup_empty() {
let (_ns_file, ns_path) = temp_file("default");
let (_cgroup_file, cgroup_path) = temp_file("0::/\n");
let root = format!("/docker/containers/{ID64}/hostname");
let mountinfo_line_str = format!("36 35 0:33 {root} /etc/hostname rw - ext4 /dev/sda1 rw");
let (_mi_file, mi_path) = temp_file(&mountinfo_line_str);
let fake = FakeImdsClient::new()
.with_document(FULL_IMDS_DOC)
.with_get(EKS_CLUSTER_NAME_TAG_PATH, "my-cluster")
.with_get("services/partition", "aws");
temp_env::with_vars(
[
("HOSTNAME", None::<&str>),
("POD_UID", None::<&str>),
("NODE_NAME", None::<&str>),
("AWS_CLUSTER_NAME", None::<&str>),
("AWS_REGION", None::<&str>),
("AWS_ACCOUNT_ID", None::<&str>),
],
|| {
let resource =
EksResourceDetector::detect_from(Ok(fake), &ns_path, &cgroup_path, &mi_path);
let attributes: std::collections::HashMap<_, _> = resource
.iter()
.map(|(k, v)| (k.as_str().to_owned(), v.clone()))
.collect();
assert_eq!(
attributes.get(semco::CONTAINER_ID).map(|v| v.as_str()),
Some(ID64.into())
);
},
);
}
#[sealed_test]
fn detect_from_container_id_absent_when_no_cgroup_or_mountinfo() {
let (_ns_file, ns_path) = temp_file("default");
let (_cgroup_file, cgroup_path) = temp_file("0::/\n");
let (_mi_file, mi_path) = temp_file("");
let fake = FakeImdsClient::new()
.with_document(FULL_IMDS_DOC)
.with_get(EKS_CLUSTER_NAME_TAG_PATH, "my-cluster")
.with_get("services/partition", "aws");
temp_env::with_vars(
[
("HOSTNAME", None::<&str>),
("POD_UID", None::<&str>),
("NODE_NAME", None::<&str>),
("AWS_CLUSTER_NAME", None::<&str>),
("AWS_REGION", None::<&str>),
("AWS_ACCOUNT_ID", None::<&str>),
],
|| {
let resource =
EksResourceDetector::detect_from(Ok(fake), &ns_path, &cgroup_path, &mi_path);
let attributes: std::collections::HashMap<_, _> = resource
.iter()
.map(|(k, v)| (k.as_str().to_owned(), v.clone()))
.collect();
assert!(
!attributes.contains_key(semco::CONTAINER_ID),
"container.id should be absent when cgroup and mountinfo carry none"
);
},
);
}
}