Skip to content

Commit

Permalink
Merge pull request #7 from vimeo/import_cgroup2_and_type_changes_2024…
Browse files Browse the repository at this point in the history
…-12-06

Import cgroup2 support & use generics to improve pparser's interface
  • Loading branch information
dfinkel authored Dec 6, 2024
2 parents 167284a + 869f771 commit ef52e45
Show file tree
Hide file tree
Showing 19 changed files with 2,640 additions and 176 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ jobs:
strategy:
matrix:
os: [macOS-latest, ubuntu-latest]
goversion: [1.17, 1.18, 1.19]
goversion: ['1.22', '1.23']
steps:

- name: Set up Go ${{matrix.goversion}} on ${{matrix.os}}
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ${{matrix.goversion}}
id: go

- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v4

- name: gofmt
run: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/staticcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ jobs:
name: "staticcheck"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: dominikh/staticcheck-action@v1.1.0
- uses: dominikh/staticcheck-action@v1.3.1
with:
version: "2022.1.3"
version: "2024.1.1"
106 changes: 106 additions & 0 deletions cgresolver/cg_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// package cgresolver contains helpers and types for resolving the CGroup associated with specific subsystems
// If you don't know what cgroup subsystems are, you probably want one of the higher-level interfaces in the parent package.
package cgresolver

import (
"fmt"
"os"
"slices"
"strconv"
"strings"
)

// CGMode is an enum indicating which cgroup type is active for the returned controller
type CGMode uint8

const (
CGModeUnknown CGMode = iota
// CGroup V1
CGModeV1
// CGroup V2
CGModeV2
)

func cgroup2Mode(iscg2 bool) CGMode {
if iscg2 {
return CGModeV2
}
return CGModeV1
}

// CGroupPath includes information about a cgroup.
type CGroupPath struct {
AbsPath string
MountPath string
Mode CGMode
}

// Parent returns a CGroupPath for the parent directory as long as it wouldn't pass the root of the mountpoint.
// second return indicates whether a new path was returned.
func (c *CGroupPath) Parent() (CGroupPath, bool) {
// Remove any trailing slash
path := strings.TrimSuffix(c.AbsPath, string(os.PathSeparator))
mnt := strings.TrimSuffix(c.MountPath, string(os.PathSeparator))
if mnt == path {
return CGroupPath{
AbsPath: path,
MountPath: mnt,
Mode: c.Mode,
}, false
}
lastSlashIdx := strings.LastIndexByte(path, byte(os.PathSeparator))
if lastSlashIdx == -1 {
// This shouldn't happen
panic("invalid state: path \"" + path + "\" has no slashes and doesn't match the mountpoint")
}
return CGroupPath{
AbsPath: path[:lastSlashIdx],
MountPath: mnt, // Strip any trailing slash in case one snuck in
Mode: c.Mode,
}, true
}

// SelfSubsystemPath returns a CGroupPath for the cgroup associated with a specific subsystem for the current process.
func SelfSubsystemPath(subsystem string) (CGroupPath, error) {
return subsystemPath("self", subsystem)
}

// PIDSubsystemPath returns a CGroupPath for the cgroup associated with a specific subsystem for the specified PID
func PIDSubsystemPath(pid int, subsystem string) (CGroupPath, error) {
return subsystemPath(strconv.Itoa(pid), subsystem)
}

func subsystemPath(procSubDir string, subsystem string) (CGroupPath, error) {
cgSubSyses, cgSubSysReadErr := ParseReadCGSubsystems()
if cgSubSysReadErr != nil {
return CGroupPath{}, fmt.Errorf("failed to resolve subsystems to hierarchies: %w", cgSubSysReadErr)
}
cgIdx := slices.IndexFunc(cgSubSyses, func(c CGroupSubsystem) bool {
return c.Subsys == subsystem
})
if cgIdx == -1 {
return CGroupPath{}, fmt.Errorf("no cgroup hierarchy associated with subsystem %q", subsystem)
}
cgHierID := cgSubSyses[cgIdx].Hierarchy

procCGs, procCGsErr := resolveProcCGControllers(procSubDir)
if procCGsErr != nil {
return CGroupPath{}, fmt.Errorf("failed to resolve cgroup controllers: %w", procCGsErr)
}

procCGIdx := slices.IndexFunc(procCGs, func(cg CGProcHierarchy) bool { return cg.HierarchyID == cgHierID })
if procCGIdx == -1 {
return CGroupPath{}, fmt.Errorf("failed to resolve process cgroup controllers: %w", procCGsErr)
}

cgMountInfo, mountInfoParseErr := CGroupMountInfo()
if mountInfoParseErr != nil {
return CGroupPath{}, fmt.Errorf("failed to parse mountinfo: %w", mountInfoParseErr)
}

cgPath, cgPathErr := procCGs[procCGIdx].cgPath(cgMountInfo)
if cgPathErr != nil {
return CGroupPath{}, fmt.Errorf("failed to resolve filesystem path for cgroup %+v: %w", procCGs[procCGIdx], cgPathErr)
}
return cgPath, nil
}
93 changes: 93 additions & 0 deletions cgresolver/cg_path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package cgresolver

import "testing"

func TestCGroupPathParent(t *testing.T) {
for _, tbl := range []struct {
name string
in CGroupPath
expParent CGroupPath
expNewParent bool
}{
{
name: "cgroup_mount_root",
in: CGroupPath{
AbsPath: "/sys/fs/cgroup",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expParent: CGroupPath{
AbsPath: "/sys/fs/cgroup",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expNewParent: false,
},
{
name: "cgroup_mount_root_strip_trailing_slashes",
in: CGroupPath{
AbsPath: "/sys/fs/cgroup/",
MountPath: "/sys/fs/cgroup/",
Mode: CGModeV2,
},
expParent: CGroupPath{
AbsPath: "/sys/fs/cgroup",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expNewParent: false,
},
{
name: "cgroup_mount_sub_cgroup_cgv1",
in: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b/c",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV1,
},
expParent: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV1,
},
expNewParent: true,
},
{
name: "cgroup_mount_sub_cgroup_cgv2",
in: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b/c",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expParent: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expNewParent: true,
},
{
name: "cgroup_mount_sub_cgroup_strip_trailing_slash",
in: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b/c/",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expParent: CGroupPath{
AbsPath: "/sys/fs/cgroup/a/b",
MountPath: "/sys/fs/cgroup",
Mode: CGModeV2,
},
expNewParent: true,
},
} {
t.Run(tbl.name, func(t *testing.T) {
par, np := tbl.in.Parent()
if np != tbl.expNewParent {
t.Errorf("unexpected OK value: %t; expected %t", np, tbl.expNewParent)
}
if par != tbl.expParent {
t.Errorf("unexpected parent CGroupPath:\n got %+v\n want %+v", par, tbl.expParent)
}
})
}
}
137 changes: 137 additions & 0 deletions cgresolver/mountinfo_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cgresolver

import (
"fmt"
"os"
"strconv"
"strings"
)

// Mount represents a cgroup or cgroup2 mount.
// Subsystems will be nil if the mount is for a unified hierarchy/cgroup v2
// in that case, CGroupV2 will be true.
type Mount struct {
Mountpoint string
Root string
Subsystems []string
CGroupV2 bool // true if this is a cgroup2 mount
}

const (
mountinfoPath = "/proc/self/mountinfo"
)

// CGroupMountInfo parses /proc/self/mountinfo and returns info about all cgroup and cgroup2 mounts
func CGroupMountInfo() ([]Mount, error) {
mountinfoContents, mntInfoReadErr := os.ReadFile(mountinfoPath)
if mntInfoReadErr != nil {
return nil, fmt.Errorf("failed to read contents of %s: %w",
mountinfoPath, mntInfoReadErr)
}

mounts, mntsErr := getCGroupMountsFromMountinfo(string(mountinfoContents))
if mntsErr != nil {
return nil, fmt.Errorf("failed to list cgroupfs mounts: %w", mntsErr)
}

return mounts, nil
}

func getCGroupMountsFromMountinfo(mountinfo string) ([]Mount, error) {
// mountinfo is line-delimited, then space-delimited
mountinfoLines := strings.Split(mountinfo, "\n")
if len(mountinfoLines) == 0 {
return nil, fmt.Errorf("unexpectedly empty mountinfo (one line): %q", mountinfo)
}
out := make([]Mount, 0, len(mountinfoLines))
for _, line := range mountinfoLines {
if len(line) == 0 {
continue
}
sections := strings.SplitN(line, " - ", 2)
if len(sections) < 2 {
return nil, fmt.Errorf("missing section separator in line %q", line)
}
s2Fields := strings.SplitN(sections[1], " ", 3)
if len(s2Fields) < 3 {
return nil, fmt.Errorf("line %q contains %d fields in second section, expected 3",
line, len(s2Fields))

}
isCG2 := false
switch s2Fields[0] {
case "cgroup":
isCG2 = false
case "cgroup2":
isCG2 = true
default:
// skip anything that's not a cgroup
continue
}
s1Fields := strings.Split(sections[0], " ")
if len(s1Fields) < 5 {
return nil, fmt.Errorf("too few fields in line %q before optional separator: %d; expected 5",
line, len(s1Fields))
}
mntpnt, mntPntUnescapeErr := unOctalEscape(s1Fields[4])
if mntPntUnescapeErr != nil {
return nil, fmt.Errorf("failed to unescape mountpoint %q: %w", s1Fields[4], mntPntUnescapeErr)
}
rootPath, rootUnescErr := unOctalEscape(s1Fields[3])
if rootUnescErr != nil {
return nil, fmt.Errorf("failed to unescape mount root %q: %w", s1Fields[3], rootUnescErr)
}
mnt := Mount{
CGroupV2: isCG2,
Mountpoint: mntpnt,
Root: rootPath,
Subsystems: nil,
}
// only bother with the mount options to find subsystems if cgroup v1
if !isCG2 {
for _, mntOpt := range strings.Split(s2Fields[2], ",") {
switch mntOpt {
case "ro", "rw":
// These mount options are lies, (or at least
// only reflect the original mount, without
// considering the layering of later bind-mounts)
continue
case "":
continue
default:
mnt.Subsystems = append(mnt.Subsystems, mntOpt)
}
}
}

out = append(out, mnt)

}
return out, nil
}

func unOctalEscape(str string) (string, error) {
b := strings.Builder{}
b.Grow(len(str))
for {
backslashIdx := strings.IndexByte(str, byte('\\'))
if backslashIdx == -1 {
b.WriteString(str)
return b.String(), nil
}
b.WriteString(str[:backslashIdx])
// if the end of the escape is beyond the end of the string, abort!
if backslashIdx+3 >= len(str) {
return "", fmt.Errorf("invalid offset: %d+3 >= len %d", backslashIdx, len(str))
}
// slice out the octal 3-digit component
esc := str[backslashIdx+1 : backslashIdx+4]
asciiVal, parseUintErr := strconv.ParseUint(esc, 8, 8)
if parseUintErr != nil {
return "", fmt.Errorf("failed to parse escape value %q: %w", esc, parseUintErr)
}
b.WriteByte(byte(asciiVal))
str = str[backslashIdx+4:]
}

}
Loading

0 comments on commit ef52e45

Please sign in to comment.