diff --git a/internal/container.go b/internal/container.go index 1ce883942f..604ccfaa2a 100644 --- a/internal/container.go +++ b/internal/container.go @@ -28,6 +28,9 @@ const ( // cgroupV1BaseController is the base controller used to identify the cgroup v1 mount point in the cgroupMounts map. cgroupV1BaseController = "memory" + + // defaultCgroupMountPath is the path to the cgroup mount point. + defaultCgroupMountPath = "/sys/fs/cgroup" ) const ( @@ -89,25 +92,9 @@ func ContainerID() string { return containerID } -// parseCgroupMountPath parses the cgroup controller mount path from /proc/mounts and returns the chosen controller. -// It selects the cgroupv1 "memory" controller mount path if it exists, otherwise it selects the cgroupv2 mount point. -// If cgroup mount points are not detected, it returns an empty string. -func parseCgroupMountPathAndController(r io.Reader) (string, string) { - mountPoints, err := discoverCgroupMountPoints(r) - if err != nil { - return "", "" - } - if cgroupRoot, ok := mountPoints[cgroupV1BaseController]; ok { - return cgroupRoot, cgroupV1BaseController - } - return mountPoints[cgroupV2Key], "" -} - -// parseCgroupNodePath parses the cgroup controller path from /proc/self/cgroup -// It returns an empty string if cgroup v2 is not used -// In cgroupv2, only 0:: should exist. In cgroupv1, we should have 0:: and [1-9]:memory: -// Refer to https://man7.org/linux/man-pages/man7/cgroups.7.html#top_of_page -func parseCgroupNodePath(r io.Reader, controller string) string { +// parseCgroupNodePath parses /proc/self/cgroup and returns a map of controller to its associated cgroup node path. +func parseCgroupNodePath(r io.Reader) map[string]string { + res := make(map[string]string) scn := bufio.NewScanner(r) for scn.Scan() { line := scn.Text() @@ -115,39 +102,36 @@ func parseCgroupNodePath(r io.Reader, controller string) string { if len(tokens) != 3 { continue } - if tokens[1] != controller { - continue - } - return tokens[2] + res[tokens[1]] = tokens[2] } - return "" + return res } // getCgroupInode returns the cgroup controller inode if it exists otherwise an empty string. // The inode is prefixed by "in-" and is used by the agent to retrieve the container ID. -func getCgroupInode(mountsPath, cgroupPath string) string { - // Retrieve a cgroup mount point from /proc/mounts - f, err := os.Open(mountsPath) - if err != nil { - return "" - } - defer f.Close() - cgroupMountPath, controller := parseCgroupMountPathAndController(f) - if cgroupMountPath == "" { - return "" - } - // Parse /proc/self/cgroup to retrieve the cgroup node path - f, err = os.Open(cgroupPath) +// For cgroup v1, we use the memory controller. +func getCgroupInode(cgroupMountPath, procSelfCgroupPath string) string { + // Parse /proc/self/cgroup to retrieve the paths to the memory controller (cgroupv1) and the cgroup node (cgroupv2) + f, err := os.Open(procSelfCgroupPath) if err != nil { return "" } defer f.Close() - cgroupNodePath := parseCgroupNodePath(f, controller) - if cgroupNodePath == "" { - return "" + cgroupControllersPaths := parseCgroupNodePath(f) + + // Retrieve the cgroup inode from /sys/fs/cgroup+cgroupNodePath + for _, cgroupNodePath := range cgroupControllersPaths { + inode := inodeForPath(path.Clean(cgroupMountPath + cgroupNodePath)) + if inode != "" { + return inode + } } - // Retrieve the cgroup inode from the cgroup mount and cgroup node path - fi, err := os.Stat(path.Clean(cgroupMountPath + cgroupNodePath)) + return "" +} + +// inodeForPath returns the inode for the provided path or empty on failure. +func inodeForPath(path string) string { + fi, err := os.Stat(path) if err != nil { return "" } @@ -158,48 +142,6 @@ func getCgroupInode(mountsPath, cgroupPath string) string { return fmt.Sprintf("in-%d", stats.Ino) } -// discoverCgroupMountPoints returns a map of cgroup controllers to their mount points. -// ported from https://github.com/DataDog/datadog-agent/blob/38b4788d6f19b3660cb7310ff33c4d352a4993a9/pkg/util/cgroups/reader_detector.go#L22 -func discoverCgroupMountPoints(r io.Reader) (map[string]string, error) { - mountPointsv1 := make(map[string]string) - var mountPointsv2 string - - s := bufio.NewScanner(r) - for s.Scan() { - line := s.Text() - - tokens := strings.Fields(line) - if len(tokens) >= 3 { - // Check if the filesystem type is 'cgroup' or 'cgroup2' - fsType := tokens[2] - if !strings.HasPrefix(fsType, "cgroup") { - continue - } - - cgroupPath := tokens[1] - if fsType == "cgroup" { - // Target can be comma-separate values like cpu,cpuacct - tsp := strings.Split(path.Base(cgroupPath), ",") - for _, target := range tsp { - // In case multiple paths are mounted for a single controller, take the shortest one - previousPath := mountPointsv1[target] - if previousPath == "" || len(cgroupPath) < len(previousPath) { - mountPointsv1[target] = cgroupPath - } - } - } else if tokens[2] == "cgroup2" { - mountPointsv2 = cgroupPath - } - } - } - - if len(mountPointsv1) == 0 && mountPointsv2 != "" { - return map[string]string{cgroupV2Key: mountPointsv2}, nil - } - - return mountPointsv1, nil -} - // readEntityID attempts to return the cgroup v2 node inode or empty on failure. func readEntityID() string { if containerID != "" { diff --git a/internal/container_test.go b/internal/container_test.go index e52d4a46dd..6218ff1a81 100644 --- a/internal/container_test.go +++ b/internal/container_test.go @@ -93,84 +93,17 @@ func TestPrioritizeContainerID(t *testing.T) { assert.Equal(t, "cid-fakeContainerID", eid) } -func TestParseCgroupMountPath(t *testing.T) { - // Test cases - cases := []struct { - name string - content string - expectedMountPath string - expectedController string - }{ - { - name: "cgroup2 only", - content: ` -none /proc proc rw,nosuid,nodev,noexec,relatime 0 0 -sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0 -cgroup2 /sys/fs/cgroup/cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 -tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0 -`, - expectedMountPath: "/sys/fs/cgroup/cgroup2", - expectedController: "", - }, - { - name: "cgroup2 and cgroup memory mounts found", - content: ` -none /proc proc rw,nosuid,nodev,noexec,relatime 0 0 -sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0 -cgroup2 /sys/fs/cgroup/cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 -tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0 -cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 -`, - expectedMountPath: "/sys/fs/cgroup/memory", - expectedController: cgroupV1BaseController, - }, - { - name: "only cgroup found", - content: ` -none /proc proc rw,nosuid,nodev,noexec,relatime 0 0 -sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0 -tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0 -cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 -`, - expectedMountPath: "/sys/fs/cgroup/memory", - expectedController: cgroupV1BaseController, - }, - { - name: "cgroup mount not found", - content: ` -none /proc proc rw,nosuid,nodev,noexec,relatime 0 0 -sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0 -tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755 0 0 -`, - expectedMountPath: "", - expectedController: "", - }, - } - - // Run test cases - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - reader := strings.NewReader(c.content) - mountPath, controller := parseCgroupMountPathAndController(reader) - require.Equal(t, c.expectedMountPath, mountPath) - require.Equal(t, c.expectedController, controller) - }) - } -} - func TestParsegroupControllerPath(t *testing.T) { // Test cases cases := []struct { - name string - content string - expected string - controller string + name string + content string + expected map[string]string }{ { - name: "cgroup2 normal case", - content: `0::/`, - expected: "/", - controller: "", + name: "cgroup2 normal case", + content: `0::/`, + expected: map[string]string{"": "/"}, }, { name: "with other controllers", @@ -180,27 +113,17 @@ func TestParsegroupControllerPath(t *testing.T) { 10:net_cls,net_prio:/docker/abc123 0::/docker/abc123 `, - expected: "/docker/abc123", - controller: "", - }, - { - name: "controller not found", - content: `12:pids:/docker/abc123 -11:hugetlb:/docker/abc123 -10:net_cls,net_prio:/docker/abc123 -`, - expected: "", - controller: "", + expected: map[string]string{ + "": "/docker/abc123", + "pids": "/docker/abc123", + "hugetlb": "/docker/abc123", + "net_cls,net_prio": "/docker/abc123", + }, }, { - name: "cgroupv1 memory controller", - content: `3:memory:/user.slice/user-1000.slice/session-8.scope -2:net_cls,net_prio:c -1:name=systemd:b -0::a -`, - expected: "/user.slice/user-1000.slice/session-8.scope", - controller: cgroupV1BaseController, + name: "no controller", + content: "empty", + expected: map[string]string{}, }, } @@ -208,7 +131,7 @@ func TestParsegroupControllerPath(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { reader := strings.NewReader(c.content) - result := parseCgroupNodePath(reader, c.controller) + result := parseCgroupNodePath(reader) require.Equal(t, c.expected, result) }) } @@ -217,24 +140,20 @@ func TestParsegroupControllerPath(t *testing.T) { func TestGetCgroupInode(t *testing.T) { tests := []struct { description string - procMountsContent string cgroupNodeDir string procSelfCgroupContent string expectedResult string - controller string // used to create a file in /tmp/sysfscgroup/ + controller string }{ { description: "matching entry in /proc/self/cgroup and /proc/mounts - cgroup2 only", - procMountsContent: "cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0\n", cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", procSelfCgroupContent: "0::/system.slice/docker-abcdef0123456789abcdef0123456789.scope\n", expectedResult: "in-%d", // Will be formatted with inode number }, { - description: "matching entry in /proc/self/cgroup and /proc/mounts - cgroup only", - controller: cgroupV1BaseController, - procMountsContent: "cgroup %s cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0\n", - cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", + description: "matching entry in /proc/self/cgroup and /proc/mounts - cgroup/hybrid only", + cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", procSelfCgroupContent: ` 3:memory:/system.slice/docker-abcdef0123456789abcdef0123456789.scope 2:net_cls,net_prio:c @@ -244,40 +163,28 @@ func TestGetCgroupInode(t *testing.T) { expectedResult: "in-%d", }, { - description: "hybrid cgroup - should match only cgroup", - procMountsContent: `other_line -cgroup %s cgroup foo,bar 0 0 -cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 - `, - cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", - procSelfCgroupContent: `other_line + description: "path does not exist", + cgroupNodeDir: "dummy.scope", + procSelfCgroupContent: ` 3:memory:/system.slice/docker-abcdef0123456789abcdef0123456789.scope 2:net_cls,net_prio:c 1:name=systemd:b 0::a `, - expectedResult: "in-%d", // Will be formatted with inode number - controller: cgroupV1BaseController, - }, - { - description: "Non-matching entry in /proc/self/cgroup", - procMountsContent: "cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0\n", - cgroupNodeDir: "system.slice/nonmatching-scope.scope", - procSelfCgroupContent: "0::/system.slice/docker-abcdef0123456789abcdef0123456789.scope\n", - expectedResult: "", + expectedResult: "", }, { - description: "No cgroup entry in /proc/mounts", - procMountsContent: "tmpfs %s tmpfs rw,nosuid,nodev,noexec,relatime 0 0\n", + description: "no entry in /proc/self/cgroup", cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", - procSelfCgroupContent: "0::/system.slice/docker-abcdef0123456789abcdef0123456789.scope\n", + procSelfCgroupContent: "nothing", expectedResult: "", }, } for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { - groupControllerPath := path.Join(os.TempDir(), "sysfscgroup", tc.controller, tc.cgroupNodeDir) + sysFsCgroupPath := path.Join(os.TempDir(), "sysfscgroup") + groupControllerPath := path.Join(sysFsCgroupPath, tc.controller, tc.cgroupNodeDir) t.Log(groupControllerPath) err := os.MkdirAll(groupControllerPath, 0755) require.NoError(t, err) @@ -290,14 +197,6 @@ cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 expectedInode = fmt.Sprintf(tc.expectedResult, stat.Sys().(*syscall.Stat_t).Ino) } - procMounts, err := os.CreateTemp("", "procmounts") - require.NoError(t, err) - defer os.Remove(procMounts.Name()) - _, err = procMounts.WriteString(fmt.Sprintf(tc.procMountsContent, path.Join(os.TempDir(), "sysfscgroup", tc.controller))) - require.NoError(t, err) - err = procMounts.Close() - require.NoError(t, err) - procSelfCgroup, err := os.CreateTemp("", "procselfcgroup") require.NoError(t, err) defer os.Remove(procSelfCgroup.Name()) @@ -307,7 +206,7 @@ cgroup2 /sys/fs/cgroup cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 require.NoError(t, err) // Test - result := getCgroupInode(procMounts.Name(), procSelfCgroup.Name()) + result := getCgroupInode(sysFsCgroupPath, procSelfCgroup.Name()) require.Equal(t, expectedInode, result) }) }