use anyhow::{anyhow, Context, Result};
use log::{debug, warn};
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
const QOS_CLASSES: [&str; 3] = ["", "burstable-", "besteffort-"];
const CGROUP_ROOT: &str = "/sys/fs/cgroup";
pub struct CgroupResolver {
cgroup_root: PathBuf,
}
impl CgroupResolver {
pub fn new() -> Self {
Self {
cgroup_root: PathBuf::from(CGROUP_ROOT),
}
}
#[allow(dead_code)]
pub fn with_root(cgroup_root: PathBuf) -> Self {
Self { cgroup_root }
}
pub fn resolve(&self, pod_uid: &str, container_id: &str) -> Result<u64> {
let normalized_uid = pod_uid.replace('-', "_");
let clean_container_id = container_id.split("://").last().unwrap_or(container_id);
for qos in QOS_CLASSES {
if let Some(inode) = self.try_containerd_path(&normalized_uid, clean_container_id, qos)
{
return Ok(inode);
}
}
Err(anyhow!(
"Could not resolve cgroup for pod {} container {}",
pod_uid,
container_id
))
}
fn try_containerd_path(&self, pod_uid: &str, container_id: &str, qos: &str) -> Option<u64> {
let pod_slice = if qos.is_empty() {
format!("kubepods-pod{}.slice", pod_uid)
} else {
format!(
"kubepods-{}.slice/kubepods-{}pod{}.slice",
qos.trim_end_matches('-'),
qos,
pod_uid
)
};
let container_scope = format!("cri-containerd-{}.scope", container_id);
let path = self
.cgroup_root
.join("kubepods.slice")
.join(&pod_slice)
.join(&container_scope);
debug!("Trying cgroup path: {}", path.display());
self.get_inode(&path)
}
fn get_inode(&self, path: &Path) -> Option<u64> {
match fs::metadata(path) {
Ok(metadata) => {
let inode = metadata.ino();
debug!("Found cgroup at {} with inode {}", path.display(), inode);
Some(inode)
}
Err(e) => {
debug!("Path {} not found: {}", path.display(), e);
None
}
}
}
pub fn scan_all(&self) -> Result<Vec<(u64, String, String)>> {
let mut results = Vec::new();
let kubepods_path = self.cgroup_root.join("kubepods.slice");
if !kubepods_path.exists() {
warn!("kubepods.slice not found at {}", kubepods_path.display());
return Ok(results);
}
self.scan_directory(&kubepods_path, &mut results)?;
Ok(results)
}
fn scan_directory(&self, dir: &Path, results: &mut Vec<(u64, String, String)>) -> Result<()> {
let entries = fs::read_dir(dir).context(format!("Failed to read directory: {:?}", dir))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
self.scan_directory(&path, results)?;
} else {
continue;
}
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name.starts_with("cri-containerd-") && name.ends_with(".scope") {
if let Some(inode) = self.get_inode(&path) {
let container_id = name
.strip_prefix("cri-containerd-")
.and_then(|s| s.strip_suffix(".scope"))
.unwrap_or("")
.to_string();
if let Some(pod_uid) = extract_pod_uid_from_path(&path) {
results.push((inode, pod_uid, container_id));
}
}
}
}
Ok(())
}
}
impl Default for CgroupResolver {
fn default() -> Self {
Self::new()
}
}
fn extract_pod_uid_from_path(path: &Path) -> Option<String> {
for ancestor in path.ancestors() {
if let Some(name) = ancestor.file_name().and_then(|n| n.to_str()) {
if name.contains("-pod") && name.ends_with(".slice") {
if let Some(start) = name.find("-pod") {
let uid_start = start + 4; if let Some(uid_part) = name.get(uid_start..) {
if let Some(uid) = uid_part.strip_suffix(".slice") {
return Some(uid.replace('_', "-"));
}
}
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_pod_uid_simple() {
let path =
PathBuf::from("/sys/fs/cgroup/kubepods.slice/kubepods-pod12345.slice/container.scope");
let uid = extract_pod_uid_from_path(&path);
assert_eq!(uid, Some("12345".to_string()));
}
#[test]
fn test_extract_pod_uid_with_qos() {
let path = PathBuf::from("/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod12345_6789.slice/container.scope");
let uid = extract_pod_uid_from_path(&path);
assert_eq!(uid, Some("12345-6789".to_string()));
}
}