diff --git a/internal/container.go b/internal/container.go index c61d534ca6..1ce883942f 100644 --- a/internal/container.go +++ b/internal/container.go @@ -22,6 +22,12 @@ const ( // mountsPath is the path to the mounts file where we can find the cgroup v2 mount point. mountsPath = "/proc/mounts" + + // cgroupV2Key is the key used to store the cgroup v2 identify the cgroupv2 mount point in the cgroupMounts map. + cgroupV2Key = "cgroupv2" + + // cgroupV1BaseController is the base controller used to identify the cgroup v1 mount point in the cgroupMounts map. + cgroupV1BaseController = "memory" ) const ( @@ -83,49 +89,50 @@ func ContainerID() string { return containerID } -// parseCgroupV2MountPath parses the cgroup mount path from /proc/mounts -// It returns an empty string if cgroup v2 is not used -func parseCgroupV2MountPath(r io.Reader) string { - scn := bufio.NewScanner(r) - for scn.Scan() { - line := scn.Text() - // a correct line line should be formatted as `cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate 0 0` - tokens := strings.Fields(line) - if len(tokens) >= 3 { - fsType := tokens[2] - if fsType == "cgroup2" { - return tokens[1] - } - } +// 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 "", "" } - return "" + if cgroupRoot, ok := mountPoints[cgroupV1BaseController]; ok { + return cgroupRoot, cgroupV1BaseController + } + return mountPoints[cgroupV2Key], "" } -// parseCgroupV2NodePath parses the cgroup node path from /proc/self/cgroup +// parseCgroupNodePath parses the cgroup controller path from /proc/self/cgroup // It returns an empty string if cgroup v2 is not used -// With respect to https://man7.org/linux/man-pages/man7/cgroups.7.html#top_of_page, in cgroupv2, only 0:: should exist -func parseCgroupV2NodePath(r io.Reader) string { +// 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 { scn := bufio.NewScanner(r) for scn.Scan() { line := scn.Text() - // The cgroup node path is the last element of the line starting with "0::" - if strings.HasPrefix(line, "0::") { - return line[3:] + tokens := strings.Split(line, ":") + if len(tokens) != 3 { + continue + } + if tokens[1] != controller { + continue } + return tokens[2] } return "" } -// getCgroupV2Inode returns the cgroup v2 node inode if it exists otherwise an empty string. +// 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 getCgroupV2Inode(mountsPath, cgroupPath string) string { +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 := parseCgroupV2MountPath(f) + cgroupMountPath, controller := parseCgroupMountPathAndController(f) if cgroupMountPath == "" { return "" } @@ -135,7 +142,7 @@ func getCgroupV2Inode(mountsPath, cgroupPath string) string { return "" } defer f.Close() - cgroupNodePath := parseCgroupV2NodePath(f) + cgroupNodePath := parseCgroupNodePath(f, controller) if cgroupNodePath == "" { return "" } @@ -151,12 +158,54 @@ func getCgroupV2Inode(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 != "" { return "cid-" + containerID } - return getCgroupV2Inode(mountsPath, cgroupPath) + return getCgroupInode(mountsPath, cgroupPath) } // EntityID attempts to return the container ID or the cgroup v2 node inode if the container ID is not available. diff --git a/internal/container_test.go b/internal/container_test.go index aeff1e8b09..e52d4a46dd 100644 --- a/internal/container_test.go +++ b/internal/container_test.go @@ -96,20 +96,33 @@ func TestPrioritizeContainerID(t *testing.T) { func TestParseCgroupMountPath(t *testing.T) { // Test cases cases := []struct { - name string - content string - expected string + name string + content string + expectedMountPath string + expectedController string }{ { - name: "cgroup2 and cgroup mounts found", + 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 -cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 `, - expected: "/sys/fs/cgroup/cgroup2", + 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", @@ -117,9 +130,10 @@ cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct 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/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 +cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,cpuacct,cpu 0 0 `, - expected: "", + expectedMountPath: "/sys/fs/cgroup/memory", + expectedController: cgroupV1BaseController, }, { name: "cgroup mount not found", @@ -128,7 +142,8 @@ 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 `, - expected: "", + expectedMountPath: "", + expectedController: "", }, } @@ -136,41 +151,56 @@ tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755 0 0 for _, c := range cases { t.Run(c.name, func(t *testing.T) { reader := strings.NewReader(c.content) - result := parseCgroupV2MountPath(reader) - require.Equal(t, c.expected, result) + mountPath, controller := parseCgroupMountPathAndController(reader) + require.Equal(t, c.expectedMountPath, mountPath) + require.Equal(t, c.expectedController, controller) }) } } -func TestParseCgroupNodePath(t *testing.T) { +func TestParsegroupControllerPath(t *testing.T) { // Test cases cases := []struct { - name string - content string - expected string + name string + content string + expected string + controller string }{ { - name: "cgroup2 normal case", - content: `0::/`, - expected: "/", + name: "cgroup2 normal case", + content: `0::/`, + expected: "/", + controller: "", }, { - name: "cgroup node path found", + name: "with other controllers", content: `other_line 12:pids:/docker/abc123 11:hugetlb:/docker/abc123 10:net_cls,net_prio:/docker/abc123 0::/docker/abc123 `, - expected: "/docker/abc123", + expected: "/docker/abc123", + controller: "", }, { - name: "cgroup node path not found", + name: "controller not found", content: `12:pids:/docker/abc123 11:hugetlb:/docker/abc123 10:net_cls,net_prio:/docker/abc123 `, - expected: "", + expected: "", + controller: "", + }, + { + 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, }, } @@ -178,7 +208,7 @@ func TestParseCgroupNodePath(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { reader := strings.NewReader(c.content) - result := parseCgroupV2NodePath(reader) + result := parseCgroupNodePath(reader, c.controller) require.Equal(t, c.expected, result) }) } @@ -191,30 +221,43 @@ func TestGetCgroupInode(t *testing.T) { cgroupNodeDir string procSelfCgroupContent string expectedResult string + controller string // used to create a file in /tmp/sysfscgroup/ }{ { - description: "default case - matching entry in /proc/self/cgroup and /proc/mounts", + 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: "should not match cgroup v1", - procMountsContent: "cgroup %s cgroup 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: "", + 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", + procSelfCgroupContent: ` +3:memory:/system.slice/docker-abcdef0123456789abcdef0123456789.scope +2:net_cls,net_prio:c +1:name=systemd:b +0::a +`, + expectedResult: "in-%d", }, { - description: "hybrid cgroup - should match only cgroup2", + description: "hybrid cgroup - should match only cgroup", procMountsContent: `other_line -cgroup /sys/fs/cgroup/memory cgroup foo,bar 0 0 -cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 +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 +3:memory:/system.slice/docker-abcdef0123456789abcdef0123456789.scope +2:net_cls,net_prio:c +1:name=systemd:b +0::a `, - cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", - procSelfCgroupContent: "0::/system.slice/docker-abcdef0123456789abcdef0123456789.scope\n", - expectedResult: "in-%d", // Will be formatted with inode number + expectedResult: "in-%d", // Will be formatted with inode number + controller: cgroupV1BaseController, }, { description: "Non-matching entry in /proc/self/cgroup", @@ -224,7 +267,7 @@ cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 expectedResult: "", }, { - description: "No cgroup2 entry in /proc/mounts", + description: "No cgroup entry in /proc/mounts", procMountsContent: "tmpfs %s tmpfs rw,nosuid,nodev,noexec,relatime 0 0\n", cgroupNodeDir: "system.slice/docker-abcdef0123456789abcdef0123456789.scope", procSelfCgroupContent: "0::/system.slice/docker-abcdef0123456789abcdef0123456789.scope\n", @@ -234,19 +277,14 @@ cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { - // Setup - cgroupMountPath, err := os.MkdirTemp(os.TempDir(), "sysfscgroup") + groupControllerPath := path.Join(os.TempDir(), "sysfscgroup", tc.controller, tc.cgroupNodeDir) + t.Log(groupControllerPath) + err := os.MkdirAll(groupControllerPath, 0755) require.NoError(t, err) - defer os.RemoveAll(cgroupMountPath) + defer os.RemoveAll(groupControllerPath) - cgroupNodePath := path.Join(cgroupMountPath, tc.cgroupNodeDir) - err = os.MkdirAll(cgroupNodePath, 0755) + stat, err := os.Stat(groupControllerPath) require.NoError(t, err) - defer os.RemoveAll(cgroupNodePath) - - stat, err := os.Stat(cgroupNodePath) - require.NoError(t, err) - expectedInode := "" if tc.expectedResult != "" { expectedInode = fmt.Sprintf(tc.expectedResult, stat.Sys().(*syscall.Stat_t).Ino) @@ -255,7 +293,7 @@ cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 procMounts, err := os.CreateTemp("", "procmounts") require.NoError(t, err) defer os.Remove(procMounts.Name()) - _, err = procMounts.WriteString(fmt.Sprintf(tc.procMountsContent, cgroupMountPath)) + _, 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) @@ -269,7 +307,7 @@ cgroup2 %s cgroup2 rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0 require.NoError(t, err) // Test - result := getCgroupV2Inode(procMounts.Name(), procSelfCgroup.Name()) + result := getCgroupInode(procMounts.Name(), procSelfCgroup.Name()) require.Equal(t, expectedInode, result) }) }