Skip to content

Commit

Permalink
greatly simplify the logic by not parsing /proc/mounts, only support …
Browse files Browse the repository at this point in the history
…/sys/fs/cgroup
  • Loading branch information
AliDatadog committed Dec 4, 2023
1 parent 58be3f0 commit 8f2dd4e
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 213 deletions.
110 changes: 26 additions & 84 deletions internal/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -89,65 +92,46 @@ 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::<path> should exist. In cgroupv1, we should have 0::<path> and [1-9]:memory:<path>
// 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()
tokens := strings.Split(line, ":")
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 ""
}
Expand All @@ -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 != "" {
Expand Down
157 changes: 28 additions & 129 deletions internal/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -180,35 +113,25 @@ 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{},
},
}

// Run test cases
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)
})
}
Expand All @@ -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>
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
Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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)
})
}
Expand Down

0 comments on commit 8f2dd4e

Please sign in to comment.