1#![allow(dead_code)]
2
3use std::{
4 collections::HashMap,
5 fs::{self, OpenOptions},
6 io::{self, BufRead as _, BufReader},
7 os::unix::fs::MetadataExt as _,
8 path::{Path, PathBuf},
9 sync::LazyLock,
10};
11
12use regex::Regex;
13use saluki_error::{generic_error, ErrorContext as _, GenericError};
14use stringtheory::{
15 interning::{GenericMapInterner, Interner as _},
16 MetaString,
17};
18use tracing::{debug, error, trace, warn};
19
20use crate::features::{Feature, FeatureDetector};
21
22const DEFAULT_PROCFS_ROOT: &str = "/proc";
23const DEFAULT_CGROUPFS_ROOT: &str = "/sys/fs/cgroup";
24const DEFAULT_LEGACY_CGROUPFS_ROOT: &str = "/cgroup";
25const DEFAULT_HOST_MAPPED_PROCFS_ROOT: &str = "/host/proc";
26const DEFAULT_HOST_MAPPED_CGROUPFS_ROOT: &str = "/host/sys/fs/cgroup";
27const CGROUPS_V1_BASE_CONTROLLER_NAME: &str = "memory";
28const CGROUPS_V2_CONTROLLERS_FILE: &str = "cgroup.controllers";
29const SELF_CGROUP_PATH: &str = "/proc/self/cgroup";
30
31const MAX_RESERVED_INODE: u64 = 2;
35
36pub struct CgroupsConfiguration {
41 procfs_root: PathBuf,
42 cgroupfs_root: PathBuf,
43}
44
45impl CgroupsConfiguration {
46 pub fn new(
52 procfs_root: Option<PathBuf>, cgroupfs_root: Option<PathBuf>, feature_detector: &FeatureDetector,
53 ) -> Self {
54 let procfs_root = procfs_root.unwrap_or_else(|| {
55 if feature_detector.is_feature_available(Feature::HostMappedProcfs) {
56 PathBuf::from(DEFAULT_HOST_MAPPED_PROCFS_ROOT)
57 } else {
58 PathBuf::from(DEFAULT_PROCFS_ROOT)
59 }
60 });
61
62 let cgroupfs_root = cgroupfs_root.unwrap_or_else(|| {
63 if feature_detector.is_feature_available(Feature::HostMappedCgroupfs) {
67 PathBuf::from(DEFAULT_HOST_MAPPED_CGROUPFS_ROOT)
68 } else if feature_detector.is_feature_available(Feature::LegacyCgroupfsRoot) {
69 PathBuf::from(DEFAULT_LEGACY_CGROUPFS_ROOT)
73 } else {
74 PathBuf::from(DEFAULT_CGROUPFS_ROOT)
75 }
76 });
77
78 Self {
79 procfs_root,
80 cgroupfs_root,
81 }
82 }
83
84 pub fn procfs_path(&self) -> &Path {
86 self.procfs_root.as_path()
87 }
88
89 pub fn cgroupfs_path(&self) -> &Path {
91 self.cgroupfs_root.as_path()
92 }
93}
94
95#[derive(Clone)]
101pub struct CgroupsReader {
102 procfs_path: PathBuf,
103 hierarchy_reader: HierarchyReader,
104 interner: GenericMapInterner,
105}
106
107impl CgroupsReader {
108 pub fn try_from_config(
119 config: &CgroupsConfiguration, interner: GenericMapInterner,
120 ) -> Result<Option<Self>, GenericError> {
121 let hierarchy_reader = HierarchyReader::try_from_config(config)?;
122 Ok(hierarchy_reader.map(|hierarchy_reader| Self {
123 procfs_path: config.procfs_path().to_path_buf(),
124 hierarchy_reader,
125 interner,
126 }))
127 }
128
129 fn try_cgroup_from_path(&self, cgroup_path: &Path) -> Option<Cgroup> {
130 let container_id = extract_container_id_from_path(cgroup_path, &self.interner)?;
131
132 let metadata = match cgroup_path.metadata() {
133 Ok(metadata) => metadata,
134 Err(e) => {
135 trace!(error = %e, cgroup_controller_path = %cgroup_path.display(), "Failed to get metadata for possible cgroup controller path.");
136 return None;
137 }
138 };
139
140 let controller_inode = metadata.ino();
144 if !is_usable_controller_inode(controller_inode) {
145 debug!(
146 controller_inode,
147 %container_id,
148 cgroup_controller_path = %cgroup_path.display(),
149 "Ignoring cgroup controller with reserved inode.",
150 );
151 return None;
152 }
153
154 trace!(
155 controller_inode,
156 %container_id,
157 cgroup_controller_path = %cgroup_path.display(),
158 "Found valid cgroups controller for container.",
159 );
160
161 Some(Cgroup {
162 ino: Some(controller_inode),
163 container_id,
164 })
165 }
166
167 pub fn get_cgroup_by_pid(&self, pid: u32) -> Option<Cgroup> {
172 let proc_pid_cgroup_path = self.procfs_path.join(pid.to_string()).join("cgroup");
174 let lines = match read_lines(&proc_pid_cgroup_path) {
175 Ok(lines) => lines,
176 Err(e) => match e.kind() {
177 io::ErrorKind::NotFound => {
178 debug!(pid, cgroup_lookup_path = %proc_pid_cgroup_path.display(), "Process does not exist or is not attached to a cgroup.");
179 return None;
180 }
181 _ => {
182 debug!(error = %e, pid, cgroup_lookup_path = %proc_pid_cgroup_path.display(), "Failed to read cgroup file for process.");
183 return None;
184 }
185 },
186 };
187
188 let base_controller_name = self.hierarchy_reader.base_controller();
189
190 for entry in lines.iter().filter_map(|s| CgroupControllerEntry::try_from_str(s)) {
193 if entry.name == base_controller_name {
194 if let Some(container_id) = extract_container_id_from_path(entry.path, &self.interner) {
203 return Some(Cgroup {
204 ino: None,
205 container_id,
206 });
207 }
208 } else {
209 debug!(pid, cgroup_lookup_path = %proc_pid_cgroup_path.display(), base_controller_name, "Found cgroup controller for process, but it doesn't match the base controller.");
210 }
211 }
212
213 debug!(pid, cgroup_lookup_path = %proc_pid_cgroup_path.display(), base_controller_name, "Could not find matching base cgroup controller for process.");
214
215 None
216 }
217
218 pub fn get_child_cgroups(&self) -> TraversalResult {
225 let root_path = self.hierarchy_reader.root_path();
227
228 match visit_subdirectories(root_path, |path| self.try_cgroup_from_path(path)) {
229 Ok(traversal) => traversal,
230 Err(e) => {
231 warn!(error = %e, cgroups_root = %root_path.display(), "Failed to visit cgroups hierarchy.");
234
235 TraversalResult::unreadable()
236 }
237 }
238 }
239}
240
241#[derive(Default)]
246pub struct TraversalResult {
247 cgroups: Vec<Cgroup>,
248 skipped: usize,
249 obscured: usize,
250}
251
252impl TraversalResult {
253 fn unreadable() -> Self {
258 Self {
259 cgroups: Vec::new(),
260 skipped: 0,
261 obscured: 1,
262 }
263 }
264
265 fn record_cgroup(&mut self, cgroup: Cgroup) {
267 self.cgroups.push(cgroup);
268 }
269
270 fn record_skip(&mut self, e: &io::Error, path: &Path) {
272 self.skipped += 1;
273
274 match e.kind() {
275 io::ErrorKind::NotFound => {
281 trace!(error = %e, path = %path.display(), "Path disappeared during traversal. Skipping.");
282 }
283
284 io::ErrorKind::PermissionDenied => {
292 debug!(error = %e, path = %path.display(), "Path is not readable. Skipping.");
293 }
294
295 _ => {
298 self.obscured += 1;
299 debug!(error = %e, path = %path.display(), "Failed to traverse path. Skipping.");
300 }
301 }
302 }
303
304 pub fn is_complete(&self) -> bool {
314 self.obscured == 0
315 }
316
317 pub fn skipped(&self) -> usize {
322 self.skipped
323 }
324
325 pub fn into_cgroups(self) -> Vec<Cgroup> {
327 self.cgroups
328 }
329}
330
331#[derive(Clone)]
332enum HierarchyReader {
333 V1 {
334 base_controller_path: PathBuf,
335 controllers: HashMap<String, PathBuf>,
336 },
337
338 V2 {
339 root: PathBuf,
340 controllers: Vec<String>,
341 },
342}
343
344impl HierarchyReader {
345 fn try_from_config(config: &CgroupsConfiguration) -> Result<Option<Self>, GenericError> {
346 let mounts_path = config.procfs_path().join("mounts");
348 let mount_entries = read_lines(&mounts_path)
349 .with_error_context(|| format!("Failed to read mount entries from procfs ({})", mounts_path.display()))?;
350
351 let mut controllers = HashMap::new();
352 let mut maybe_cgroups_v2 = None;
353
354 for mount_entry in mount_entries {
356 let mut fields = mount_entry.split_whitespace();
359 let maybe_cgroup_path = fields.nth(1);
360 let maybe_fs_type = fields.nth(0);
361
362 if let (Some(raw_cgroup_path), Some(fs_type)) = (maybe_cgroup_path, maybe_fs_type) {
363 let cgroup_path = Path::new(raw_cgroup_path);
364
365 if !cgroup_path.starts_with(config.cgroupfs_path()) {
372 continue;
373 }
374
375 match fs_type {
376 "cgroup" => process_cgroupv1_mount_entry(cgroup_path, &mut controllers),
378 "cgroup2" => maybe_cgroups_v2 = process_cgroupv2_mount_entry(cgroup_path)?,
380 _ => {}
381 }
382 }
383 }
384
385 if controllers.is_empty() {
388 if maybe_cgroups_v2.is_some() {
389 debug!("Using cgroups v2 hierarchy.");
390 }
391
392 return Ok(maybe_cgroups_v2);
393 }
394
395 let base_controller_path = controllers
398 .get(CGROUPS_V1_BASE_CONTROLLER_NAME)
399 .cloned()
400 .ok_or_else(|| {
401 generic_error!(
402 "Failed to find base controller ({}) in cgroups v1 hierarchy.",
403 CGROUPS_V1_BASE_CONTROLLER_NAME
404 )
405 })?;
406
407 debug!(root = %base_controller_path.display(), controllers_len = controllers.len(), "Using cgroups v1 hierarchy.");
408
409 Ok(Some(HierarchyReader::V1 {
410 base_controller_path,
411 controllers,
412 }))
413 }
414
415 fn base_controller(&self) -> Option<&'static str> {
416 match self {
417 Self::V1 { .. } => Some(CGROUPS_V1_BASE_CONTROLLER_NAME),
418
419 Self::V2 { .. } => None,
421 }
422 }
423
424 fn root_path(&self) -> &Path {
425 match self {
426 Self::V1 {
427 base_controller_path, ..
428 } => base_controller_path.as_path(),
429 Self::V2 { root, .. } => root.as_path(),
430 }
431 }
432}
433
434pub struct Cgroup {
436 ino: Option<u64>,
437 container_id: MetaString,
438}
439
440impl Cgroup {
441 pub fn inode(&self) -> Option<u64> {
443 self.ino
444 }
445
446 pub fn into_container_id(self) -> MetaString {
448 self.container_id
449 }
450}
451
452struct CgroupControllerEntry<'a> {
453 id: usize,
454 name: Option<&'a str>,
455 path: &'a Path,
456}
457
458impl<'a> CgroupControllerEntry<'a> {
459 fn try_from_str(line: &'a str) -> Option<Self> {
460 let mut fields = line.splitn(3, ':');
461
462 let id = fields.next()?.parse::<usize>().ok()?;
463 let name = fields.next().map(|s| if s.is_empty() { None } else { Some(s) })?;
464 let path = fields.next()?;
465
466 if path.is_empty() {
467 return None;
468 }
469
470 Some(Self {
471 id,
472 name,
473 path: Path::new(path),
474 })
475 }
476}
477
478fn process_cgroupv1_mount_entry(cgroup_path: &Path, controllers: &mut HashMap<String, PathBuf>) {
479 let path_controllers = cgroup_path
481 .file_name()
482 .and_then(|s| s.to_str().map(|s| s.split(',')))
483 .into_iter()
484 .flatten();
485 for path_controller in path_controllers {
486 if let Some(existing_path) = controllers.get(path_controller) {
489 if existing_path.as_os_str().len() < cgroup_path.as_os_str().len() {
490 continue;
491 }
492 }
493
494 controllers.insert(path_controller.to_string(), PathBuf::from(cgroup_path));
495 }
496}
497
498fn process_cgroupv2_mount_entry(cgroup_path: &Path) -> Result<Option<HierarchyReader>, GenericError> {
499 let controllers_path = cgroup_path.join(CGROUPS_V2_CONTROLLERS_FILE);
501 let controllers = read_lines(&controllers_path)
502 .with_error_context(|| {
503 format!(
504 "Failed to read controllers from cgroups v2 hierarchy ({}).",
505 controllers_path.display()
506 )
507 })?
508 .into_iter()
509 .flat_map(|s| s.split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>())
510 .collect::<Vec<_>>();
511
512 Ok(Some(HierarchyReader::V2 {
513 root: cgroup_path.to_path_buf(),
514 controllers,
515 }))
516}
517
518fn read_lines(path: &Path) -> io::Result<Vec<String>> {
519 let file = OpenOptions::new().read(true).open(path)?;
520
521 let reader = BufReader::new(file).lines();
522
523 let mut lines = Vec::new();
524 for line in reader {
525 lines.push(line?);
526 }
527
528 Ok(lines)
529}
530
531fn visit_subdirectories<P, F>(path: P, mut visit: F) -> Result<TraversalResult, GenericError>
542where
543 P: AsRef<Path>,
544 F: FnMut(&Path) -> Option<Cgroup>,
545{
546 let root = path.as_ref();
547
548 let metadata = fs::metadata(root)
550 .with_error_context(|| format!("Failed to query metadata for traversal root ({}).", root.display()))?;
551 if !metadata.is_dir() {
552 return Ok(TraversalResult::default());
553 }
554
555 let mut traversal = TraversalResult::default();
556
557 let mut stack = vec![root.to_path_buf()];
560 while let Some(path) = stack.pop() {
561 let dir_reader = match fs::read_dir(&path) {
564 Ok(dir_reader) => dir_reader,
565 Err(e) => {
566 if path.as_path() == root {
573 return Err(e)
574 .with_error_context(|| format!("Failed to read traversal root ({}).", root.display()));
575 }
576
577 traversal.record_skip(&e, &path);
578 continue;
579 }
580 };
581
582 for entry in dir_reader {
583 let entry = match entry {
584 Ok(entry) => entry,
585 Err(e) => {
586 traversal.record_skip(&e, &path);
587 continue;
588 }
589 };
590
591 let entry_path = entry.path();
592 let file_type = match entry.file_type() {
593 Ok(file_type) => file_type,
594 Err(e) => {
595 traversal.record_skip(&e, &entry_path);
596 continue;
597 }
598 };
599
600 if file_type.is_dir() {
601 if let Some(cgroup) = visit(&entry_path) {
602 traversal.record_cgroup(cgroup);
603 }
604
605 stack.push(entry_path);
606 }
607 }
608 }
609
610 Ok(traversal)
611}
612
613pub(crate) fn get_self_container_id(interner: &GenericMapInterner) -> Option<MetaString> {
618 let lines = read_lines(Path::new(SELF_CGROUP_PATH)).ok()?;
619 get_container_id_from_cgroup_lines(&lines, interner)
620}
621
622fn get_container_id_from_cgroup_lines(lines: &[String], interner: &GenericMapInterner) -> Option<MetaString> {
623 lines
624 .iter()
625 .filter_map(|line| CgroupControllerEntry::try_from_str(line))
626 .filter_map(|entry| entry.path.file_name().and_then(|name| name.to_str()))
627 .find_map(|cgroup_name| extract_container_id(cgroup_name, interner))
628}
629
630fn is_usable_controller_inode(inode: u64) -> bool {
635 inode > MAX_RESERVED_INODE
636}
637
638static CONTAINER_REGEX: LazyLock<Regex> =
646 LazyLock::new(|| Regex::new("([0-9a-f]{64})|([0-9a-f]{32}-\\d+)|([0-9a-f]{8}(-[0-9a-f]{4}){4}$)").unwrap());
647
648fn extract_container_id(cgroup_name: &str, interner: &GenericMapInterner) -> Option<MetaString> {
649 match match_container_id(cgroup_name, interner) {
650 ContainerIdMatch::Container(container_id) => Some(container_id),
651 ContainerIdMatch::Excluded | ContainerIdMatch::Uninternable | ContainerIdMatch::NoMatch => None,
652 }
653}
654
655enum ContainerIdMatch {
657 Container(MetaString),
659
660 Excluded,
662
663 Uninternable,
668
669 NoMatch,
671}
672
673fn match_container_id(cgroup_name: &str, interner: &GenericMapInterner) -> ContainerIdMatch {
679 let container_id = match CONTAINER_REGEX.find(cgroup_name) {
680 Some(container_id) => container_id,
681 None => return ContainerIdMatch::NoMatch,
682 };
683
684 if is_container_named_but_not_a_container(cgroup_name) {
687 return ContainerIdMatch::Excluded;
688 }
689
690 match interner.try_intern(container_id.as_str()) {
691 Some(interned) => ContainerIdMatch::Container(MetaString::from(interned)),
692 None => {
693 error!(container_id = %container_id.as_str(), "Failed to intern container ID.");
694 ContainerIdMatch::Uninternable
695 }
696 }
697}
698
699fn extract_container_id_from_path(cgroup_path: &Path, interner: &GenericMapInterner) -> Option<MetaString> {
712 let leaf_name = cgroup_path.file_name().and_then(|s| s.to_str())?;
713
714 match match_container_id(leaf_name, interner) {
715 ContainerIdMatch::Container(container_id) => return Some(container_id),
716 ContainerIdMatch::Excluded | ContainerIdMatch::Uninternable => return None,
717 ContainerIdMatch::NoMatch => {}
718 }
719
720 for ancestor in cgroup_path.ancestors().skip(1) {
722 let ancestor_name = match ancestor.file_name().and_then(|s| s.to_str()) {
723 Some(ancestor_name) => ancestor_name,
724 None => continue,
725 };
726
727 match match_container_id(ancestor_name, interner) {
728 ContainerIdMatch::Container(container_id) => return Some(container_id),
729
730 ContainerIdMatch::Excluded | ContainerIdMatch::NoMatch => {}
733
734 ContainerIdMatch::Uninternable => return None,
736 }
737 }
738
739 None
740}
741
742fn is_container_named_but_not_a_container(cgroup_name: &str) -> bool {
744 cgroup_name.ends_with(".mount")
750 || cgroup_name.starts_with("crio-conmon-")
751 || cgroup_name.starts_with("libpod-conmon-")
752}
753
754#[cfg(test)]
755mod tests {
756 use std::{
757 collections::HashSet,
758 fs, io,
759 num::NonZeroUsize,
760 os::unix::fs::PermissionsExt as _,
761 path::{Path, PathBuf},
762 };
763
764 use stringtheory::{
765 interning::{GenericMapInterner, InternedString, Interner as _},
766 MetaString,
767 };
768 use tempfile::tempdir;
769
770 use super::{
771 extract_container_id, extract_container_id_from_path, get_container_id_from_cgroup_lines,
772 is_usable_controller_inode, visit_subdirectories, CgroupControllerEntry, CgroupsConfiguration, CgroupsReader,
773 Feature, FeatureDetector, HierarchyReader, TraversalResult, DEFAULT_CGROUPFS_ROOT,
774 DEFAULT_HOST_MAPPED_CGROUPFS_ROOT, DEFAULT_HOST_MAPPED_PROCFS_ROOT, DEFAULT_LEGACY_CGROUPFS_ROOT,
775 DEFAULT_PROCFS_ROOT,
776 };
777
778 #[test]
779 fn parse_controller_entry_cgroups_v1() {
780 let controller_id = 12;
781 let controller_name = "memory";
782 let controller_path_raw = "/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod095a9475_4c4f_4726_912c_65743701ef3f.slice/cri-containerd-06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe.scope";
783 let controller_path = Path::new(controller_path_raw);
784 let raw = format!("{}:{}:{}", controller_id, controller_name, controller_path_raw);
785
786 let entry = CgroupControllerEntry::try_from_str(&raw).unwrap();
787 assert_eq!(entry.id, controller_id);
788 assert_eq!(entry.name, Some(controller_name));
789 assert_eq!(entry.path, controller_path);
790 }
791
792 #[test]
793 fn parse_controller_entry_cgroups_v2() {
794 let controller_id = 0;
795 let controller_path_raw =
796 "/system.slice/docker-0b96e72f48e169638a735c0a05adcfc9d6aba2bf6697b627f1635b4f00ea011d.scope";
797 let controller_path = Path::new(controller_path_raw);
798 let raw = format!("{}::{}", controller_id, controller_path_raw);
799
800 let entry = CgroupControllerEntry::try_from_str(&raw).unwrap();
801 assert_eq!(entry.id, controller_id);
802 assert_eq!(entry.name, None);
803 assert_eq!(entry.path, controller_path);
804 }
805
806 fn extract(raw: &str) -> Option<MetaString> {
807 let interner = GenericMapInterner::new(NonZeroUsize::new(1024).unwrap());
808 extract_container_id(raw, &interner)
809 }
810
811 fn extract_from_path(raw: &str) -> Option<MetaString> {
812 let interner = GenericMapInterner::new(NonZeroUsize::new(1024).unwrap());
813 extract_container_id_from_path(Path::new(raw), &interner)
814 }
815
816 #[test]
817 fn resolves_container_id_from_current_process_cgroup_format() {
818 let container_id = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
819 let cgroup_lines = vec![format!("0::/system.slice/cri-containerd-{container_id}.scope")];
820 let interner = GenericMapInterner::new(NonZeroUsize::new(1024).unwrap());
821
822 assert_eq!(
823 get_container_id_from_cgroup_lines(&cgroup_lines, &interner),
824 Some(MetaString::from(container_id))
825 );
826 }
827
828 #[test]
829 fn does_not_resolve_self_container_from_non_container_cgroup_fixture() {
830 let cgroup_lines = include_str!("testdata/non-container-proc-self-cgroup")
831 .lines()
832 .map(str::to_owned)
833 .collect::<Vec<_>>();
834 let interner = GenericMapInterner::new(NonZeroUsize::new(1024).unwrap());
835
836 assert_eq!(get_container_id_from_cgroup_lines(&cgroup_lines, &interner), None);
837 }
838
839 #[test]
840 fn extract_container_id_cri_containerd() {
841 let expected_container_id =
842 MetaString::from("06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe");
843 let raw = format!("cri-containerd-{}.scope", expected_container_id);
844
845 assert_eq!(extract(&raw), Some(expected_container_id));
846 }
847
848 #[test]
852 fn extract_container_id_excludes_dot_mount_cgroups() {
853 let container_id = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
854 let raw = format!("{}.mount", container_id);
855
856 assert_eq!(extract(&raw), None);
857 }
858
859 #[test]
860 fn extract_container_id_excludes_crio_conmon_cgroups() {
861 let container_id = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
862 let raw = format!("crio-conmon-{}.scope", container_id);
863
864 assert_eq!(extract(&raw), None);
865 }
866
867 #[test]
868 fn extract_container_id_excludes_libpod_conmon_cgroups() {
869 let container_id = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
870 let raw = format!("libpod-conmon-{}.scope", container_id);
871
872 assert_eq!(extract(&raw), None);
873 }
874
875 #[test]
876 fn extract_container_id_includes_libpod_container_cgroups() {
877 let container_id = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
880 let raw = format!("libpod-{}.scope", container_id);
881
882 assert_eq!(extract(&raw), Some(MetaString::from(container_id)));
883 }
884
885 #[test]
886 fn reserved_inodes_are_not_usable_controller_inodes() {
887 assert!(!is_usable_controller_inode(0));
889 assert!(!is_usable_controller_inode(1));
890 assert!(!is_usable_controller_inode(2));
891 }
892
893 #[test]
894 fn ordinary_inodes_are_usable_controller_inodes() {
895 assert!(is_usable_controller_inode(3));
896 assert!(is_usable_controller_inode(4_026_531_835));
897 assert!(is_usable_controller_inode(u64::MAX));
898 }
899
900 const CONTAINER_ID_A: &str = "06d914d2013e51a777feead523895935e33d8ad725b3251ac74c491b3d55d8fe";
901 const CONTAINER_ID_B: &str = "1a2b3c4d5e6f70819293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9";
902
903 #[test]
904 fn extract_from_path_prefers_the_leaf() {
905 let path = format!(
906 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/cri-containerd-{}.scope",
907 CONTAINER_ID_A, CONTAINER_ID_B
908 );
909
910 assert_eq!(extract_from_path(&path), Some(MetaString::from(CONTAINER_ID_B)));
912 }
913
914 #[test]
915 fn extract_from_path_falls_back_to_ancestors() {
916 let path = format!(
918 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/init",
919 CONTAINER_ID_A
920 );
921
922 assert_eq!(extract_from_path(&path), Some(MetaString::from(CONTAINER_ID_A)));
923 }
924
925 #[test]
926 fn extract_from_path_uses_the_deepest_matching_ancestor() {
927 let path = format!(
928 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/cri-containerd-{}.scope/init",
929 CONTAINER_ID_A, CONTAINER_ID_B
930 );
931
932 assert_eq!(extract_from_path(&path), Some(MetaString::from(CONTAINER_ID_B)));
933 }
934
935 #[test]
936 fn extract_from_path_returns_none_without_any_container_segment() {
937 let path = "/sys/fs/cgroup/system.slice/systemd-journald.service";
938
939 assert_eq!(extract_from_path(path), None);
940 }
941
942 #[test]
943 fn extract_from_path_does_not_rescue_excluded_leaves_from_ancestors() {
944 let mount_path = format!(
947 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/{}.mount",
948 CONTAINER_ID_A, CONTAINER_ID_B
949 );
950 let conmon_path = format!(
951 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/crio-conmon-{}.scope",
952 CONTAINER_ID_A, CONTAINER_ID_B
953 );
954
955 assert_eq!(extract_from_path(&mount_path), None);
956 assert_eq!(extract_from_path(&conmon_path), None);
957 }
958
959 #[test]
960 fn extract_from_path_skips_excluded_ancestors() {
961 let path = format!(
963 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/crio-conmon-{}.scope/init",
964 CONTAINER_ID_A, CONTAINER_ID_B
965 );
966
967 assert_eq!(extract_from_path(&path), Some(MetaString::from(CONTAINER_ID_A)));
968 }
969
970 fn interner_holding_but_blocking(held_id: &str, blocked_id: &str) -> (GenericMapInterner, Vec<InternedString>) {
975 let interner = GenericMapInterner::new(NonZeroUsize::new(1024).unwrap());
976 let mut held = vec![interner.try_intern(held_id).expect("interner starts out empty")];
977
978 for filler in 0.. {
982 if interner.try_intern(blocked_id).is_none() {
983 break;
984 }
985
986 assert!(filler < 100_000, "interner never filled up");
987
988 if let Some(interned) = interner.try_intern(&format!("filler-{:060}", filler)) {
989 held.push(interned);
990 }
991 }
992
993 assert!(interner.try_intern(held_id).is_some());
996
997 (interner, held)
998 }
999
1000 #[test]
1001 fn extract_from_path_does_not_fall_back_when_the_leaf_id_cannot_be_interned() {
1002 let (interner, _held) = interner_holding_but_blocking(CONTAINER_ID_A, CONTAINER_ID_B);
1003
1004 let path = format!(
1007 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/cri-containerd-{}.scope",
1008 CONTAINER_ID_A, CONTAINER_ID_B
1009 );
1010
1011 assert_eq!(extract_container_id_from_path(Path::new(&path), &interner), None);
1012 }
1013
1014 #[test]
1015 fn extract_from_path_stops_at_an_ancestor_whose_id_cannot_be_interned() {
1016 let (interner, _held) = interner_holding_but_blocking(CONTAINER_ID_A, CONTAINER_ID_B);
1017
1018 let path = format!(
1021 "/sys/fs/cgroup/system.slice/cri-containerd-{}.scope/cri-containerd-{}.scope/init",
1022 CONTAINER_ID_A, CONTAINER_ID_B
1023 );
1024
1025 assert_eq!(extract_container_id_from_path(Path::new(&path), &interner), None);
1026 }
1027
1028 #[test]
1029 fn extract_from_path_handles_relative_paths() {
1030 let path = format!("kubepods.slice/cri-containerd-{}.scope/init", CONTAINER_ID_A);
1033
1034 assert_eq!(extract_from_path(&path), Some(MetaString::from(CONTAINER_ID_A)));
1035 }
1036
1037 fn visited_names(root: &Path, visited: &[PathBuf]) -> HashSet<String> {
1039 visited
1040 .iter()
1041 .map(|path| path.strip_prefix(root).unwrap().to_string_lossy().into_owned())
1042 .collect()
1043 }
1044
1045 fn names(names: &[&str]) -> HashSet<String> {
1046 names.iter().map(|name| (*name).to_owned()).collect()
1047 }
1048
1049 fn make_unreadable(path: &Path) -> bool {
1053 fs::set_permissions(path, fs::Permissions::from_mode(0o000)).unwrap();
1054
1055 if fs::read_dir(path).is_ok() {
1056 make_readable(path);
1057 return false;
1058 }
1059
1060 true
1061 }
1062
1063 fn make_readable(path: &Path) {
1065 fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
1066 }
1067
1068 fn reader_rooted_at(root: &Path) -> CgroupsReader {
1069 CgroupsReader {
1070 procfs_path: PathBuf::from(DEFAULT_PROCFS_ROOT),
1071 hierarchy_reader: HierarchyReader::V2 {
1072 root: root.to_path_buf(),
1073 controllers: Vec::new(),
1074 },
1075 interner: GenericMapInterner::new(NonZeroUsize::new(1024).unwrap()),
1076 }
1077 }
1078
1079 #[test]
1080 fn visit_subdirectories_visits_every_subdirectory() {
1081 let root = tempdir().unwrap();
1082 fs::create_dir_all(root.path().join("a/aa")).unwrap();
1083 fs::create_dir(root.path().join("b")).unwrap();
1084 fs::write(root.path().join("b/file"), "not a directory").unwrap();
1085
1086 let mut visited = Vec::new();
1087 let traversal = visit_subdirectories(root.path(), |path| {
1088 visited.push(path.to_path_buf());
1089 None
1090 })
1091 .unwrap();
1092
1093 assert_eq!(traversal.skipped, 0);
1094 assert_eq!(traversal.obscured, 0);
1095 assert_eq!(visited_names(root.path(), &visited), names(&["a", "a/aa", "b"]));
1096 }
1097
1098 #[test]
1099 fn record_skip_classifies_by_error_kind() {
1100 let mut traversal = TraversalResult::default();
1101
1102 traversal.record_skip(&io::Error::from(io::ErrorKind::NotFound), Path::new("/gone"));
1105 traversal.record_skip(&io::Error::from(io::ErrorKind::PermissionDenied), Path::new("/denied"));
1106
1107 assert_eq!(traversal.skipped, 2);
1108 assert_eq!(traversal.obscured, 0);
1109 assert!(traversal.is_complete());
1110
1111 traversal.record_skip(&io::Error::from(io::ErrorKind::Other), Path::new("/unreadable"));
1113
1114 assert_eq!(traversal.skipped, 3);
1115 assert_eq!(traversal.obscured, 1);
1116 assert!(!traversal.is_complete());
1117 }
1118
1119 #[test]
1120 fn visit_subdirectories_errors_when_root_is_missing() {
1121 let root = tempdir().unwrap();
1122
1123 assert!(visit_subdirectories(root.path().join("missing"), |_| None).is_err());
1124 }
1125
1126 #[test]
1127 fn visit_subdirectories_errors_when_root_is_unreadable() {
1128 let parent = tempdir().unwrap();
1130 let root = parent.path().join("root");
1131 fs::create_dir_all(root.join("child")).unwrap();
1132
1133 if !make_unreadable(&root) {
1134 return;
1135 }
1136
1137 assert!(fs::metadata(&root).is_ok());
1140
1141 let result = visit_subdirectories(&root, |_| None);
1142
1143 make_readable(&root);
1144
1145 assert!(result.is_err());
1148 }
1149
1150 #[test]
1151 fn visit_subdirectories_ignores_non_directory_root() {
1152 let root = tempdir().unwrap();
1153 let file_path = root.path().join("file");
1154 fs::write(&file_path, "not a directory").unwrap();
1155
1156 let mut visited = Vec::new();
1157 let traversal = visit_subdirectories(&file_path, |path| {
1158 visited.push(path.to_path_buf());
1159 None
1160 })
1161 .unwrap();
1162
1163 assert_eq!(traversal.skipped, 0);
1164 assert!(visited.is_empty());
1165 }
1166
1167 #[test]
1168 fn visit_subdirectories_skips_directories_removed_mid_traversal() {
1169 let root = tempdir().unwrap();
1170 for name in ["a", "b", "c"] {
1171 fs::create_dir(root.path().join(name)).unwrap();
1172 }
1173
1174 let mut visited = Vec::new();
1178 let traversal = visit_subdirectories(root.path(), |path| {
1179 visited.push(path.to_path_buf());
1180 if visited.len() == 2 {
1181 fs::remove_dir(&visited[0]).unwrap();
1182 }
1183 None
1184 })
1185 .unwrap();
1186
1187 assert_eq!(traversal.skipped, 1);
1190
1191 assert_eq!(traversal.obscured, 0);
1193 assert!(traversal.is_complete());
1194 assert_eq!(visited_names(root.path(), &visited), names(&["a", "b", "c"]));
1195 }
1196
1197 #[test]
1198 fn visit_subdirectories_reports_obscured_paths_for_unexpected_errors() {
1199 let root = tempdir().unwrap();
1200 for name in ["a", "b"] {
1201 fs::create_dir(root.path().join(name)).unwrap();
1202 }
1203
1204 let mut visited = Vec::new();
1207 let traversal = visit_subdirectories(root.path(), |path| {
1208 visited.push(path.to_path_buf());
1209 if visited.len() == 2 {
1210 fs::remove_dir(&visited[0]).unwrap();
1211 fs::write(&visited[0], "no longer a directory").unwrap();
1212 }
1213 None
1214 })
1215 .unwrap();
1216
1217 assert_eq!(traversal.skipped, 1);
1219 assert_eq!(traversal.obscured, 1);
1220 assert!(!traversal.is_complete());
1221 }
1222
1223 #[test]
1224 fn visit_subdirectories_skips_unreadable_directories() {
1225 let root = tempdir().unwrap();
1226 let unreadable = root.path().join("unreadable");
1227 fs::create_dir(&unreadable).unwrap();
1228 fs::create_dir_all(root.path().join("readable/nested")).unwrap();
1229
1230 if !make_unreadable(&unreadable) {
1231 return;
1232 }
1233
1234 let mut visited = Vec::new();
1235 let traversal = visit_subdirectories(root.path(), |path| {
1236 visited.push(path.to_path_buf());
1237 None
1238 });
1239
1240 make_readable(&unreadable);
1241
1242 let traversal = traversal.unwrap();
1243 assert_eq!(traversal.skipped, 1);
1244
1245 assert_eq!(traversal.obscured, 0);
1249 assert!(traversal.is_complete());
1250
1251 assert_eq!(
1253 visited_names(root.path(), &visited),
1254 names(&["readable", "readable/nested", "unreadable"])
1255 );
1256 }
1257
1258 #[test]
1259 fn get_child_cgroups_reports_complete_traversal() {
1260 let root = tempdir().unwrap();
1261 fs::create_dir(root.path().join(format!("cri-containerd-{}.scope", CONTAINER_ID_A))).unwrap();
1262
1263 let traversal = reader_rooted_at(root.path()).get_child_cgroups();
1264
1265 assert!(traversal.is_complete());
1266 assert_eq!(traversal.skipped, 0);
1267 assert_eq!(traversal.cgroups.len(), 1);
1268 assert_eq!(traversal.cgroups[0].container_id, MetaString::from(CONTAINER_ID_A));
1269 }
1270
1271 #[test]
1272 fn get_child_cgroups_stays_complete_when_subdirectory_is_unreadable() {
1273 let root = tempdir().unwrap();
1274 fs::create_dir(root.path().join(format!("cri-containerd-{}.scope", CONTAINER_ID_A))).unwrap();
1275
1276 let unreadable = root.path().join("unreadable");
1277 fs::create_dir(&unreadable).unwrap();
1278 if !make_unreadable(&unreadable) {
1279 return;
1280 }
1281
1282 let traversal = reader_rooted_at(root.path()).get_child_cgroups();
1283
1284 make_readable(&unreadable);
1285
1286 assert!(traversal.is_complete());
1290 assert_eq!(traversal.skipped, 1);
1291 assert_eq!(traversal.cgroups.len(), 1);
1292 assert_eq!(traversal.cgroups[0].container_id, MetaString::from(CONTAINER_ID_A));
1293 }
1294
1295 #[test]
1296 fn get_child_cgroups_reports_incomplete_traversal_when_root_is_missing() {
1297 let root = tempdir().unwrap();
1298
1299 let traversal = reader_rooted_at(&root.path().join("missing")).get_child_cgroups();
1300
1301 assert!(!traversal.is_complete());
1302 assert_eq!(traversal.skipped, 0);
1303 assert!(traversal.cgroups.is_empty());
1304 }
1305
1306 #[test]
1307 fn get_child_cgroups_reports_incomplete_traversal_when_root_is_unreadable() {
1308 let parent = tempdir().unwrap();
1309 let root = parent.path().join("root");
1310 fs::create_dir(&root).unwrap();
1311 fs::create_dir(root.join(format!("cri-containerd-{}.scope", CONTAINER_ID_A))).unwrap();
1312
1313 if !make_unreadable(&root) {
1314 return;
1315 }
1316
1317 let traversal = reader_rooted_at(&root).get_child_cgroups();
1318
1319 make_readable(&root);
1320
1321 assert!(!traversal.is_complete());
1324 assert!(traversal.cgroups.is_empty());
1325 }
1326
1327 fn cgroups_config_with(detected: Feature) -> CgroupsConfiguration {
1328 CgroupsConfiguration::new(None, None, &FeatureDetector::from_detected_features(detected))
1329 }
1330
1331 #[test]
1332 fn cgroupfs_root_defaults_to_local_when_nothing_is_host_mapped() {
1333 let config = cgroups_config_with(Feature::none());
1334
1335 assert_eq!(config.procfs_path(), Path::new(DEFAULT_PROCFS_ROOT));
1336 assert_eq!(config.cgroupfs_path(), Path::new(DEFAULT_CGROUPFS_ROOT));
1337 }
1338
1339 #[test]
1340 fn cgroupfs_root_follows_host_mapped_cgroupfs() {
1341 let config = cgroups_config_with(Feature::HostMappedCgroupfs);
1342
1343 assert_eq!(config.cgroupfs_path(), Path::new(DEFAULT_HOST_MAPPED_CGROUPFS_ROOT));
1344 }
1345
1346 #[test]
1347 fn cgroupfs_root_ignores_host_mapped_procfs() {
1348 let config = cgroups_config_with(Feature::HostMappedProcfs);
1351
1352 assert_eq!(config.procfs_path(), Path::new(DEFAULT_HOST_MAPPED_PROCFS_ROOT));
1353 assert_eq!(config.cgroupfs_path(), Path::new(DEFAULT_CGROUPFS_ROOT));
1354 }
1355
1356 #[test]
1357 fn cgroupfs_root_follows_legacy_root() {
1358 let config = cgroups_config_with(Feature::LegacyCgroupfsRoot);
1359
1360 assert_eq!(config.cgroupfs_path(), Path::new(DEFAULT_LEGACY_CGROUPFS_ROOT));
1361 assert_eq!(config.procfs_path(), Path::new(DEFAULT_PROCFS_ROOT));
1362 }
1363
1364 #[test]
1365 fn host_mapped_cgroupfs_takes_precedence_over_legacy_root() {
1366 let config = cgroups_config_with(Feature::HostMappedCgroupfs | Feature::LegacyCgroupfsRoot);
1371
1372 assert_eq!(config.cgroupfs_path(), Path::new(DEFAULT_HOST_MAPPED_CGROUPFS_ROOT));
1373 }
1374
1375 #[test]
1376 fn procfs_root_ignores_host_mapped_cgroupfs() {
1377 let config = cgroups_config_with(Feature::HostMappedCgroupfs);
1378
1379 assert_eq!(config.procfs_path(), Path::new(DEFAULT_PROCFS_ROOT));
1380 }
1381}