From 8be3a9633dae9ab7eb783404759244d9091e8dbf Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:30:42 +0100 Subject: [PATCH 1/9] Add cgroup package --- actions/config.go | 24 ++- cgroup/cgroup.go | 356 +++++++++++++++++++++++++++++++++ cgroup/cgroup_test.go | 455 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 832 insertions(+), 3 deletions(-) create mode 100644 cgroup/cgroup.go create mode 100644 cgroup/cgroup_test.go diff --git a/actions/config.go b/actions/config.go index 7c7c0e61..f2abf959 100644 --- a/actions/config.go +++ b/actions/config.go @@ -24,6 +24,7 @@ import ( "bytes" "fmt" "log" + "math" "os" "runtime" "time" @@ -31,6 +32,7 @@ import ( "golang.org/x/sys/unix" "google.golang.org/protobuf/proto" + "github.com/google/fscrypt/cgroup" "github.com/google/fscrypt/crypto" "github.com/google/fscrypt/filesystem" "github.com/google/fscrypt/metadata" @@ -186,8 +188,9 @@ func getConfig() (*metadata.Config, error) { func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) { log.Printf("Finding hashing costs that take %v\n", target) - // Start out with the minimal possible costs that use all the CPUs. - parallelism := int64(runtime.NumCPU()) + // Start out with the minimal possible costs that use all the available + // CPUs, respecting cgroup limits when present. + parallelism := int64(effectiveCPUCount()) // golang.org/x/crypto/argon2 only supports parallelism up to 255. // For compatibility, don't use more than that amount. if parallelism > metadata.MaxParallelism { @@ -248,9 +251,21 @@ func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) { } } +// effectiveCPUCount returns the number of CPUs available to this process, +// taking cgroup limits into account. Falls back to runtime.NumCPU() when +// cgroup information is unavailable. +func effectiveCPUCount() int { + quota, err := cgroup.CPUQuota() + if err != nil || quota <= 0 { + return runtime.NumCPU() + } + cpus := int(math.Ceil(quota)) + return min(cpus, runtime.NumCPU()) +} + // memoryBytesLimit returns the maximum amount of memory we will use for // passphrase hashing. This will never be more than a reasonable maximum (for -// compatibility) or an 8th the available system RAM. +// compatibility) or an 8th the available RAM (considering cgroup limits). func memoryBytesLimit() int64 { // The sysinfo syscall only fails if given a bad address var info unix.Sysinfo_t @@ -258,6 +273,9 @@ func memoryBytesLimit() int64 { util.NeverError(err) totalRAMBytes := int64(info.Totalram) + if cgroupMem, err := cgroup.MemoryLimit(); err == nil && cgroupMem > 0 { + totalRAMBytes = util.MinInt64(totalRAMBytes, cgroupMem) + } return util.MinInt64(totalRAMBytes/8, maxMemoryBytes) } diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go new file mode 100644 index 00000000..fe6fe50e --- /dev/null +++ b/cgroup/cgroup.go @@ -0,0 +1,356 @@ +/* + * cgroup.go - Read CPU and memory limits from Linux cgroups (v1 and v2). + * + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// Package cgroup reads CPU and memory resource limits from Linux control +// groups. Both cgroup v1 and v2 are supported. +// +// References: +// - cgroups(7): https://man7.org/linux/man-pages/man7/cgroups.7.html +// - cgroup v2 (cpu.max, memory.max): https://docs.kernel.org/admin-guide/cgroup-v2.html +// - cgroup v1 CPU bandwidth (cpu.cfs_quota_us, cpu.cfs_period_us): +// https://docs.kernel.org/scheduler/sched-bwc.html +// - cgroup v1 memory (memory.limit_in_bytes): +// https://docs.kernel.org/admin-guide/cgroup-v1/memory.html +// - /proc/self/cgroup: https://man7.org/linux/man-pages/man7/cgroups.7.html (see "/proc files") +// - /proc/self/mountinfo: https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html +package cgroup + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +// ErrNoLimit indicates that no cgroup limit is set. +var ErrNoLimit = errors.New("no cgroup limit set") + +// CPUQuota returns the CPU quota as a fractional number of CPUs (e.g. 0.5 +// means half a core). Returns ErrNoLimit if no CPU limit is configured. +func CPUQuota() (float64, error) { + return CPUQuotaWithRoot("/") +} + +// CPUQuotaWithRoot is like CPUQuota but resolves all filesystem paths +// relative to root instead of "/". This is useful for testing with a +// mock filesystem. +func CPUQuotaWithRoot(root string) (float64, error) { + ver, err := detectVersion(filepath.Join(root, "proc/self/cgroup")) + if err != nil { + return 0, err + } + if ver.version == 2 { + return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", ver.v2GroupPath)) + } + return cpuQuotaV1(root) +} + +// MemoryLimit returns the cgroup memory limit in bytes. Returns ErrNoLimit +// if no memory limit is configured. +func MemoryLimit() (int64, error) { + return MemoryLimitWithRoot("/") +} + +// MemoryLimitWithRoot is like MemoryLimit but resolves all filesystem paths +// relative to root instead of "/". This is useful for testing with a +// mock filesystem. +func MemoryLimitWithRoot(root string) (int64, error) { + ver, err := detectVersion(filepath.Join(root, "proc/self/cgroup")) + if err != nil { + return 0, err + } + if ver.version == 2 { + return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", ver.v2GroupPath)) + } + return memoryLimitV1(root) +} + +// cgroupVersion holds the result of detecting which cgroup version is in use. +type cgroupVersion struct { + // version is 1 or 2. + version int + // v2GroupPath is the cgroup path from /proc/self/cgroup (only set when version is 2). + v2GroupPath string +} + +// detectVersion reads /proc/self/cgroup and determines whether cgroup v2 is +// in use. For v2, it also returns the group path. In v2, the file contains a +// single line like "0::/path". In v1, lines have non-zero hierarchy IDs. +func detectVersion(procCgroup string) (cgroupVersion, error) { + f, err := os.Open(procCgroup) + if err != nil { + return cgroupVersion{}, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, ":", 3) + if len(parts) != 3 { + continue + } + // v2 entry: hierarchy-ID is 0 and controllers field is empty. + if parts[0] == "0" && parts[1] == "" { + return cgroupVersion{version: 2, v2GroupPath: parts[2]}, nil + } + } + return cgroupVersion{version: 1}, scanner.Err() +} + +// cpuQuotaV2 reads cpu.max from the given cgroup v2 directory. +// Format: "$MAX $PERIOD" or "max $PERIOD". +// https://docs.kernel.org/admin-guide/cgroup-v2.html +func cpuQuotaV2(cgroupDir string) (float64, error) { + data, err := os.ReadFile(filepath.Join(cgroupDir, "cpu.max")) + if err != nil { + if os.IsNotExist(err) { + return 0, ErrNoLimit + } + return 0, err + } + return parseCPUMax(strings.TrimSpace(string(data))) +} + +func parseCPUMax(content string) (float64, error) { + fields := strings.Fields(content) + if len(fields) == 0 || len(fields) > 2 { + return 0, fmt.Errorf("unexpected cpu.max format: %q", content) + } + if fields[0] == "max" { + return 0, ErrNoLimit + } + quota, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return 0, fmt.Errorf("parsing cpu.max quota: %w", err) + } + period := 100000.0 + if len(fields) == 2 { + period, err = strconv.ParseFloat(fields[1], 64) + if err != nil { + return 0, fmt.Errorf("parsing cpu.max period: %w", err) + } + if period == 0 { + return 0, fmt.Errorf("cpu.max period is zero") + } + } + return quota / period, nil +} + +// memoryLimitV2 reads memory.max from the given cgroup v2 directory. +func memoryLimitV2(cgroupDir string) (int64, error) { + data, err := os.ReadFile(filepath.Join(cgroupDir, "memory.max")) + if err != nil { + if os.IsNotExist(err) { + return 0, ErrNoLimit + } + return 0, err + } + return parseMemoryMax(strings.TrimSpace(string(data))) +} + +func parseMemoryMax(content string) (int64, error) { + if content == "max" { + return 0, ErrNoLimit + } + v, err := strconv.ParseInt(content, 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing memory.max: %w", err) + } + return v, nil +} + +// cpuQuotaV1 reads cpu.cfs_quota_us and cpu.cfs_period_us from the cpu +// cgroup v1 subsystem. +func cpuQuotaV1(root string) (float64, error) { + cgroupPath, err := v1SubsystemPath(root, "cpu") + if err != nil { + return 0, err + } + quotaData, err := os.ReadFile(filepath.Join(cgroupPath, "cpu.cfs_quota_us")) + if err != nil { + return 0, err + } + quota, err := strconv.ParseInt(strings.TrimSpace(string(quotaData)), 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing cpu.cfs_quota_us: %w", err) + } + if quota < 0 { + return 0, ErrNoLimit + } + periodData, err := os.ReadFile(filepath.Join(cgroupPath, "cpu.cfs_period_us")) + if err != nil { + return 0, err + } + period, err := strconv.ParseInt(strings.TrimSpace(string(periodData)), 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing cpu.cfs_period_us: %w", err) + } + if period == 0 { + return 0, fmt.Errorf("cpu.cfs_period_us is zero") + } + return float64(quota) / float64(period), nil +} + +// memoryLimitV1 reads memory.limit_in_bytes from the memory cgroup v1 +// subsystem. +func memoryLimitV1(root string) (int64, error) { + cgroupPath, err := v1SubsystemPath(root, "memory") + if err != nil { + return 0, err + } + data, err := os.ReadFile(filepath.Join(cgroupPath, "memory.limit_in_bytes")) + if err != nil { + return 0, err + } + v, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) + if err != nil { + return 0, fmt.Errorf("parsing memory.limit_in_bytes: %w", err) + } + // The kernel uses a very large value (close to max int64) to indicate + // "no limit". Treat anything above 1 EiB as unlimited. + const oneEiB = 1 << 60 + if v >= oneEiB { + return 0, ErrNoLimit + } + return v, nil +} + +// v1SubsystemPath finds the filesystem path for a cgroup v1 subsystem by +// correlating /proc/self/cgroup with /proc/self/mountinfo. All paths are +// resolved relative to root. +func v1SubsystemPath(root, subsystem string) (string, error) { + procCgroup := filepath.Join(root, "proc/self/cgroup") + procMountInfo := filepath.Join(root, "proc/self/mountinfo") + + relPath, err := v1CgroupRelPath(procCgroup, subsystem) + if err != nil { + return "", err + } + // mountinfo contains absolute paths (e.g. /sys/fs/cgroup/cpu). We + // prepend root so that file reads go to the right place. + mountPoint, mountRoot, err := v1MountPoint(procMountInfo, subsystem) + if err != nil { + return "", err + } + rel, err := filepath.Rel(mountRoot, relPath) + if err != nil { + return "", err + } + return filepath.Join(root, mountPoint, rel), nil +} + +// v1CgroupRelPath returns the cgroup-relative path for the given subsystem +// from /proc/self/cgroup. +func v1CgroupRelPath(procCgroup, subsystem string) (string, error) { + f, err := os.Open(procCgroup) + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + // Format: hierarchy-ID:controller-list:cgroup-path + parts := strings.SplitN(scanner.Text(), ":", 3) + if len(parts) != 3 { + continue + } + for ctrl := range strings.SplitSeq(parts[1], ",") { + if ctrl == subsystem { + return parts[2], nil + } + } + } + if err := scanner.Err(); err != nil { + return "", err + } + return "", fmt.Errorf("cgroup v1 subsystem %q not found in %s", subsystem, procCgroup) +} + +// v1MountPoint finds the mount point and root for a cgroup v1 subsystem +// from /proc/self/mountinfo. The returned paths are absolute (as written +// in mountinfo); the caller is responsible for prepending the root. +func v1MountPoint(procMountInfo, subsystem string) (mountPoint, root string, err error) { + f, err := os.Open(procMountInfo) + if err != nil { + return "", "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + mp, err := parseMountInfoLine(scanner.Text()) + if err != nil { + continue + } + if mp.fsType != "cgroup" { + continue + } + for _, opt := range mp.superOptions { + if opt == subsystem { + return mp.mountPoint, mp.root, nil + } + } + } + if err := scanner.Err(); err != nil { + return "", "", err + } + return "", "", fmt.Errorf("cgroup v1 mount for %q not found in %s", subsystem, procMountInfo) +} + +type mountInfo struct { + root string + mountPoint string + fsType string + superOptions []string +} + +// parseMountInfoLine parses a single line from /proc/self/mountinfo. +// Format: id parent devid root mount opts [optional...] - fstype source superopts +func parseMountInfoLine(line string) (mountInfo, error) { + fields := strings.Split(line, " ") + if len(fields) < 7 { + return mountInfo{}, fmt.Errorf("too few fields in mountinfo line") + } + + root := fields[3] + mp := fields[4] + + // Find the separator "-" that marks the end of optional fields. + sepIdx := -1 + for i := 6; i < len(fields); i++ { + if fields[i] == "-" { + sepIdx = i + break + } + } + if sepIdx == -1 || sepIdx+3 > len(fields) { + return mountInfo{}, fmt.Errorf("no separator in mountinfo line") + } + + return mountInfo{ + root: root, + mountPoint: mp, + fsType: fields[sepIdx+1], + superOptions: strings.Split(fields[sepIdx+3], ","), + }, nil +} diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go new file mode 100644 index 00000000..dcf1583d --- /dev/null +++ b/cgroup/cgroup_test.go @@ -0,0 +1,455 @@ +/* + * cgroup_test.go - Tests for cgroup CPU and memory limit reading. + * + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package cgroup + +import ( + "errors" + "math" + "os" + "path/filepath" + "strings" + "testing" +) + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } +} + +func TestParseCPUMax(t *testing.T) { + tests := []struct { + name string + content string + want float64 + wantErr string + }{ + {"half core", "50000 100000", 0.5, ""}, + {"two cores", "200000 100000", 2.0, ""}, + {"quota only with default period", "50000", 0.5, ""}, + {"unlimited", "max 100000", 0, "no cgroup limit set"}, + {"empty", "", 0, "unexpected cpu.max format"}, + {"zero period", "50000 0", 0, "period is zero"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseCPUMax(tt.content) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("parseCPUMax(%q) error = %v, want error containing %q", tt.content, err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("parseCPUMax(%q) unexpected error: %v", tt.content, err) + } + if math.Abs(got-tt.want) > 0.001 { + t.Errorf("parseCPUMax(%q) = %v, want %v", tt.content, got, tt.want) + } + }) + } +} + +func TestParseMemoryMax(t *testing.T) { + tests := []struct { + name string + content string + want int64 + wantErr string + }{ + {"128 MiB", "134217728", 134217728, ""}, + {"unlimited", "max", 0, "no cgroup limit set"}, + {"invalid", "abc", 0, "parsing memory.max"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseMemoryMax(tt.content) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("parseMemoryMax(%q) error = %v, want error containing %q", tt.content, err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("parseMemoryMax(%q) unexpected error: %v", tt.content, err) + } + if got != tt.want { + t.Errorf("parseMemoryMax(%q) = %v, want %v", tt.content, got, tt.want) + } + }) + } +} + +func TestParseMountInfoLine(t *testing.T) { + tests := []struct { + name string + line string + wantFSType string + wantRoot string + wantMount string + wantSuperOpt string + wantErr bool + }{ + { + name: "cgroup v1 cpu", + line: "35 26 0:30 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct", + wantFSType: "cgroup", + wantRoot: "/", + wantMount: "/sys/fs/cgroup/cpu,cpuacct", + wantSuperOpt: "cpu", + }, + { + name: "cgroup v2", + line: "30 23 0:26 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot", + wantFSType: "cgroup2", + wantRoot: "/", + wantMount: "/sys/fs/cgroup", + wantSuperOpt: "nsdelegate", + }, + { + name: "too short", + line: "a b c", + wantErr: true, + }, + { + name: "no separator", + line: "35 26 0:30 / /mnt rw,relatime shared:1 cgroup cgroup rw", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseMountInfoLine(tt.line) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.fsType != tt.wantFSType { + t.Errorf("fsType = %q, want %q", got.fsType, tt.wantFSType) + } + if got.root != tt.wantRoot { + t.Errorf("root = %q, want %q", got.root, tt.wantRoot) + } + if got.mountPoint != tt.wantMount { + t.Errorf("mountPoint = %q, want %q", got.mountPoint, tt.wantMount) + } + found := false + for _, opt := range got.superOptions { + if opt == tt.wantSuperOpt { + found = true + break + } + } + if !found { + t.Errorf("superOptions = %v, want to contain %q", got.superOptions, tt.wantSuperOpt) + } + }) + } +} + +func TestDetectVersion(t *testing.T) { + tests := []struct { + name string + content string + wantVersion int + wantGroup string + }{ + { + name: "cgroup v2", + content: "0::/user.slice/user-1000.slice/session-1.scope\n", + wantVersion: 2, + wantGroup: "/user.slice/user-1000.slice/session-1.scope", + }, + { + name: "cgroup v1 only", + content: "12:memory:/docker/abc123\n11:cpu,cpuacct:/docker/abc123\n", + wantVersion: 1, + }, + { + name: "hybrid v1 and v2", + content: "12:memory:/docker/abc123\n0::/docker/abc123\n", + wantVersion: 2, + wantGroup: "/docker/abc123", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := filepath.Join(t.TempDir(), "cgroup") + writeFile(t, f, tt.content) + + ver, err := detectVersion(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ver.version != tt.wantVersion { + t.Errorf("version = %d, want %d", ver.version, tt.wantVersion) + } + if tt.wantVersion == 2 && ver.v2GroupPath != tt.wantGroup { + t.Errorf("v2GroupPath = %q, want %q", ver.v2GroupPath, tt.wantGroup) + } + }) + } +} + +func TestCPUQuotaV2(t *testing.T) { + tests := []struct { + name string + cpuMax string + want float64 + wantErr string + }{ + {"half core", "50000 100000\n", 0.5, ""}, + {"four cores", "400000 100000\n", 4.0, ""}, + {"unlimited", "max 100000\n", 0, "no cgroup limit set"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "cpu.max"), tt.cpuMax) + + got, err := cpuQuotaV2(dir) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if math.Abs(got-tt.want) > 0.001 { + t.Errorf("cpuQuotaV2() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCPUQuotaV2MissingFile(t *testing.T) { + _, err := cpuQuotaV2(t.TempDir()) + if !errors.Is(err, ErrNoLimit) { + t.Errorf("error = %v, want ErrNoLimit", err) + } +} + +func TestMemoryLimitV2(t *testing.T) { + tests := []struct { + name string + memoryMax string + want int64 + wantErr string + }{ + {"128 MiB", "134217728\n", 134217728, ""}, + {"unlimited", "max\n", 0, "no cgroup limit set"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "memory.max"), tt.memoryMax) + + got, err := memoryLimitV2(dir) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("memoryLimitV2() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMemoryLimitV2MissingFile(t *testing.T) { + _, err := memoryLimitV2(t.TempDir()) + if !errors.Is(err, ErrNoLimit) { + t.Errorf("error = %v, want ErrNoLimit", err) + } +} + +// setupV1Root creates a mock cgroup v1 filesystem under a temp directory. +// The mountinfo file contains absolute paths (as the kernel writes them), +// and the code prepends root when reading. +func setupV1Root(t *testing.T, subsystem, cgroupRelPath string, files map[string]string) string { + t.Helper() + root := t.TempDir() + + // /proc/self/cgroup + var cgroupLine string + switch subsystem { + case "cpu": + cgroupLine = "11:cpu,cpuacct:" + cgroupRelPath + "\n" + case "memory": + cgroupLine = "12:memory:" + cgroupRelPath + "\n" + } + writeFile(t, filepath.Join(root, "proc/self/cgroup"), cgroupLine) + + // /proc/self/mountinfo with absolute mount point + mountPoint := "/sys/fs/cgroup/" + subsystem + mountInfoLine := "35 26 0:30 / " + mountPoint + " rw,nosuid - cgroup cgroup rw," + subsystem + "\n" + writeFile(t, filepath.Join(root, "proc/self/mountinfo"), mountInfoLine) + + // cgroup control files under root + mountPoint + cgroupRelPath + cgroupDir := filepath.Join(root, mountPoint, cgroupRelPath) + for name, content := range files { + writeFile(t, filepath.Join(cgroupDir, name), content) + } + + return root +} + +func TestCPUQuotaV1(t *testing.T) { + tests := []struct { + name string + quota string + period string + want float64 + wantErr string + }{ + {"half core", "50000\n", "100000\n", 0.5, ""}, + {"two cores", "200000\n", "100000\n", 2.0, ""}, + {"unlimited", "-1\n", "100000\n", 0, "no cgroup limit set"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := setupV1Root(t, "cpu", "/docker/abc123", map[string]string{ + "cpu.cfs_quota_us": tt.quota, + "cpu.cfs_period_us": tt.period, + }) + + got, err := cpuQuotaV1(root) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if math.Abs(got-tt.want) > 0.001 { + t.Errorf("cpuQuotaV1() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMemoryLimitV1(t *testing.T) { + tests := []struct { + name string + limit string + want int64 + wantErr string + }{ + {"128 MiB", "134217728\n", 134217728, ""}, + {"unlimited (large value)", "9223372036854771712\n", 0, "no cgroup limit set"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := setupV1Root(t, "memory", "/docker/abc123", map[string]string{ + "memory.limit_in_bytes": tt.limit, + }) + + got, err := memoryLimitV1(root) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Errorf("memoryLimitV1() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCPUQuotaWithRootV2(t *testing.T) { + root := t.TempDir() + + writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") + writeFile(t, filepath.Join(root, "sys/fs/cgroup/kubepods/pod123/cpu.max"), "50000 100000\n") + + got, err := CPUQuotaWithRoot(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if math.Abs(got-0.5) > 0.001 { + t.Errorf("CPUQuotaWithRoot() = %v, want 0.5", got) + } +} + +func TestMemoryLimitWithRootV2(t *testing.T) { + root := t.TempDir() + + writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") + writeFile(t, filepath.Join(root, "sys/fs/cgroup/kubepods/pod123/memory.max"), "134217728\n") + + got, err := MemoryLimitWithRoot(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != 134217728 { + t.Errorf("MemoryLimitWithRoot() = %v, want 134217728", got) + } +} + +func TestCPUQuotaWithRootV1(t *testing.T) { + root := setupV1Root(t, "cpu", "/docker/abc123", map[string]string{ + "cpu.cfs_quota_us": "150000\n", + "cpu.cfs_period_us": "100000\n", + }) + + got, err := CPUQuotaWithRoot(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if math.Abs(got-1.5) > 0.001 { + t.Errorf("CPUQuotaWithRoot() = %v, want 1.5", got) + } +} + +func TestMemoryLimitWithRootV1(t *testing.T) { + root := setupV1Root(t, "memory", "/docker/abc123", map[string]string{ + "memory.limit_in_bytes": "268435456\n", + }) + + got, err := MemoryLimitWithRoot(root) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != 268435456 { + t.Errorf("MemoryLimitWithRoot() = %v, want 268435456", got) + } +} From f5e3508c460b134aa766748ce7d1203c67049f8c Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:58:56 +0100 Subject: [PATCH 2/9] Refactor procGgroup --- cgroup/cgroup.go | 124 ++++++++++++++++++++---------------------- cgroup/cgroup_test.go | 49 +++++++++++++---- 2 files changed, 95 insertions(+), 78 deletions(-) diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go index fe6fe50e..8e49d2db 100644 --- a/cgroup/cgroup.go +++ b/cgroup/cgroup.go @@ -36,6 +36,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "strconv" "strings" ) @@ -53,14 +54,14 @@ func CPUQuota() (float64, error) { // relative to root instead of "/". This is useful for testing with a // mock filesystem. func CPUQuotaWithRoot(root string) (float64, error) { - ver, err := detectVersion(filepath.Join(root, "proc/self/cgroup")) + cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) if err != nil { return 0, err } - if ver.version == 2 { - return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", ver.v2GroupPath)) + if cg.version == 2 { + return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", cg.v2GroupPath)) } - return cpuQuotaV1(root) + return cpuQuotaV1(root, cg) } // MemoryLimit returns the cgroup memory limit in bytes. Returns ErrNoLimit @@ -73,47 +74,69 @@ func MemoryLimit() (int64, error) { // relative to root instead of "/". This is useful for testing with a // mock filesystem. func MemoryLimitWithRoot(root string) (int64, error) { - ver, err := detectVersion(filepath.Join(root, "proc/self/cgroup")) + cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) if err != nil { return 0, err } - if ver.version == 2 { - return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", ver.v2GroupPath)) + if cg.version == 2 { + return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", cg.v2GroupPath)) } - return memoryLimitV1(root) + return memoryLimitV1(root, cg) } -// cgroupVersion holds the result of detecting which cgroup version is in use. -type cgroupVersion struct { +// procCgroup holds the parsed contents of /proc/self/cgroup. +// +// The file format is documented in cgroups(7). Each line has the format: +// +// hierarchy-ID:controller-list:cgroup-path +// +// For v2, there is a single line "0::". For v1, each line has a +// non-zero hierarchy ID and a comma-separated list of controllers. +type procCgroup struct { // version is 1 or 2. version int - // v2GroupPath is the cgroup path from /proc/self/cgroup (only set when version is 2). + // v2GroupPath is the cgroup path (only set when version is 2). v2GroupPath string + // v1Subsystems maps controller names to their cgroup paths (only + // populated when version is 1). + v1Subsystems map[string]string } -// detectVersion reads /proc/self/cgroup and determines whether cgroup v2 is -// in use. For v2, it also returns the group path. In v2, the file contains a -// single line like "0::/path". In v1, lines have non-zero hierarchy IDs. -func detectVersion(procCgroup string) (cgroupVersion, error) { - f, err := os.Open(procCgroup) +// parseProcCgroup parses /proc/self/cgroup in a single pass, extracting +// both v2 and v1 information. +// https://man7.org/linux/man-pages/man7/cgroups.7.html +func parseProcCgroup(path string) (procCgroup, error) { + f, err := os.Open(path) if err != nil { - return cgroupVersion{}, err + return procCgroup{}, err } defer f.Close() + result := procCgroup{ + version: 1, + v1Subsystems: make(map[string]string), + } + scanner := bufio.NewScanner(f) for scanner.Scan() { - line := scanner.Text() - parts := strings.SplitN(line, ":", 3) + parts := strings.SplitN(scanner.Text(), ":", 3) if len(parts) != 3 { continue } // v2 entry: hierarchy-ID is 0 and controllers field is empty. if parts[0] == "0" && parts[1] == "" { - return cgroupVersion{version: 2, v2GroupPath: parts[2]}, nil + result.version = 2 + result.v2GroupPath = parts[2] + continue + } + // v1 entry: map each controller to its cgroup path. + for _, ctrl := range strings.Split(parts[1], ",") { + if ctrl != "" { + result.v1Subsystems[ctrl] = parts[2] + } } } - return cgroupVersion{version: 1}, scanner.Err() + return result, scanner.Err() } // cpuQuotaV2 reads cpu.max from the given cgroup v2 directory. @@ -180,8 +203,8 @@ func parseMemoryMax(content string) (int64, error) { // cpuQuotaV1 reads cpu.cfs_quota_us and cpu.cfs_period_us from the cpu // cgroup v1 subsystem. -func cpuQuotaV1(root string) (float64, error) { - cgroupPath, err := v1SubsystemPath(root, "cpu") +func cpuQuotaV1(root string, cg procCgroup) (float64, error) { + cgroupPath, err := v1SubsystemPath(root, "cpu", cg) if err != nil { return 0, err } @@ -212,8 +235,8 @@ func cpuQuotaV1(root string) (float64, error) { // memoryLimitV1 reads memory.limit_in_bytes from the memory cgroup v1 // subsystem. -func memoryLimitV1(root string) (int64, error) { - cgroupPath, err := v1SubsystemPath(root, "memory") +func memoryLimitV1(root string, cg procCgroup) (int64, error) { + cgroupPath, err := v1SubsystemPath(root, "memory", cg) if err != nil { return 0, err } @@ -235,18 +258,16 @@ func memoryLimitV1(root string) (int64, error) { } // v1SubsystemPath finds the filesystem path for a cgroup v1 subsystem by -// correlating /proc/self/cgroup with /proc/self/mountinfo. All paths are -// resolved relative to root. -func v1SubsystemPath(root, subsystem string) (string, error) { - procCgroup := filepath.Join(root, "proc/self/cgroup") - procMountInfo := filepath.Join(root, "proc/self/mountinfo") - - relPath, err := v1CgroupRelPath(procCgroup, subsystem) - if err != nil { - return "", err +// looking up the already-parsed /proc/self/cgroup data and correlating it +// with /proc/self/mountinfo. All paths are resolved relative to root. +func v1SubsystemPath(root, subsystem string, cg procCgroup) (string, error) { + relPath, ok := cg.v1Subsystems[subsystem] + if !ok { + return "", fmt.Errorf("cgroup v1 subsystem %q not found", subsystem) } // mountinfo contains absolute paths (e.g. /sys/fs/cgroup/cpu). We // prepend root so that file reads go to the right place. + procMountInfo := filepath.Join(root, "proc/self/mountinfo") mountPoint, mountRoot, err := v1MountPoint(procMountInfo, subsystem) if err != nil { return "", err @@ -258,34 +279,6 @@ func v1SubsystemPath(root, subsystem string) (string, error) { return filepath.Join(root, mountPoint, rel), nil } -// v1CgroupRelPath returns the cgroup-relative path for the given subsystem -// from /proc/self/cgroup. -func v1CgroupRelPath(procCgroup, subsystem string) (string, error) { - f, err := os.Open(procCgroup) - if err != nil { - return "", err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - // Format: hierarchy-ID:controller-list:cgroup-path - parts := strings.SplitN(scanner.Text(), ":", 3) - if len(parts) != 3 { - continue - } - for ctrl := range strings.SplitSeq(parts[1], ",") { - if ctrl == subsystem { - return parts[2], nil - } - } - } - if err := scanner.Err(); err != nil { - return "", err - } - return "", fmt.Errorf("cgroup v1 subsystem %q not found in %s", subsystem, procCgroup) -} - // v1MountPoint finds the mount point and root for a cgroup v1 subsystem // from /proc/self/mountinfo. The returned paths are absolute (as written // in mountinfo); the caller is responsible for prepending the root. @@ -305,10 +298,8 @@ func v1MountPoint(procMountInfo, subsystem string) (mountPoint, root string, err if mp.fsType != "cgroup" { continue } - for _, opt := range mp.superOptions { - if opt == subsystem { - return mp.mountPoint, mp.root, nil - } + if slices.Contains(mp.superOptions, subsystem) { + return mp.mountPoint, mp.root, nil } } if err := scanner.Err(); err != nil { @@ -326,6 +317,7 @@ type mountInfo struct { // parseMountInfoLine parses a single line from /proc/self/mountinfo. // Format: id parent devid root mount opts [optional...] - fstype source superopts +// https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html func parseMountInfoLine(line string) (mountInfo, error) { fields := strings.Split(line, " ") if len(fields) < 7 { diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index dcf1583d..e8b4e7bf 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -172,12 +172,13 @@ func TestParseMountInfoLine(t *testing.T) { } } -func TestDetectVersion(t *testing.T) { +func TestParseProcCgroup(t *testing.T) { tests := []struct { - name string - content string - wantVersion int - wantGroup string + name string + content string + wantVersion int + wantGroup string + wantSubsystems map[string]string }{ { name: "cgroup v2", @@ -189,12 +190,20 @@ func TestDetectVersion(t *testing.T) { name: "cgroup v1 only", content: "12:memory:/docker/abc123\n11:cpu,cpuacct:/docker/abc123\n", wantVersion: 1, + wantSubsystems: map[string]string{ + "memory": "/docker/abc123", + "cpu": "/docker/abc123", + "cpuacct": "/docker/abc123", + }, }, { name: "hybrid v1 and v2", content: "12:memory:/docker/abc123\n0::/docker/abc123\n", wantVersion: 2, wantGroup: "/docker/abc123", + wantSubsystems: map[string]string{ + "memory": "/docker/abc123", + }, }, } for _, tt := range tests { @@ -202,15 +211,23 @@ func TestDetectVersion(t *testing.T) { f := filepath.Join(t.TempDir(), "cgroup") writeFile(t, f, tt.content) - ver, err := detectVersion(f) + cg, err := parseProcCgroup(f) if err != nil { t.Fatalf("unexpected error: %v", err) } - if ver.version != tt.wantVersion { - t.Errorf("version = %d, want %d", ver.version, tt.wantVersion) + if cg.version != tt.wantVersion { + t.Errorf("version = %d, want %d", cg.version, tt.wantVersion) } - if tt.wantVersion == 2 && ver.v2GroupPath != tt.wantGroup { - t.Errorf("v2GroupPath = %q, want %q", ver.v2GroupPath, tt.wantGroup) + if tt.wantVersion == 2 && cg.v2GroupPath != tt.wantGroup { + t.Errorf("v2GroupPath = %q, want %q", cg.v2GroupPath, tt.wantGroup) + } + for k, want := range tt.wantSubsystems { + got, ok := cg.v1Subsystems[k] + if !ok { + t.Errorf("v1Subsystems missing key %q", k) + } else if got != want { + t.Errorf("v1Subsystems[%q] = %q, want %q", k, got, want) + } } }) } @@ -344,8 +361,12 @@ func TestCPUQuotaV1(t *testing.T) { "cpu.cfs_quota_us": tt.quota, "cpu.cfs_period_us": tt.period, }) + cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) + if err != nil { + t.Fatal(err) + } - got, err := cpuQuotaV1(root) + got, err := cpuQuotaV1(root, cg) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) @@ -377,8 +398,12 @@ func TestMemoryLimitV1(t *testing.T) { root := setupV1Root(t, "memory", "/docker/abc123", map[string]string{ "memory.limit_in_bytes": tt.limit, }) + cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) + if err != nil { + t.Fatal(err) + } - got, err := memoryLimitV1(root) + got, err := memoryLimitV1(root, cg) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) From e60b91c4bd8176d1538774a36541bf64de0eefd5 Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:31:02 +0100 Subject: [PATCH 3/9] Add testdata generation --- Makefile | 5 + bin/generate-cgroup-testdata | 59 ++++++++ bin/snapshot-cgroup | 128 ++++++++++++++++++ cgroup/cgroup_test.go | 62 +++++++++ .../testdata/v2-half-core-128m/expected.json | 1 + .../v2-half-core-128m/proc/self/cgroup | 1 + .../v2-half-core-128m/sys/fs/cgroup/cpu.max | 1 + .../sys/fs/cgroup/memory.max | 1 + cgroup/testdata/v2-no-limit/expected.json | 1 + cgroup/testdata/v2-no-limit/proc/self/cgroup | 1 + .../v2-no-limit/sys/fs/cgroup/cpu.max | 1 + .../v2-no-limit/sys/fs/cgroup/memory.max | 1 + .../v2-quarter-core-64m/expected.json | 1 + .../v2-quarter-core-64m/proc/self/cgroup | 1 + .../v2-quarter-core-64m/sys/fs/cgroup/cpu.max | 1 + .../sys/fs/cgroup/memory.max | 1 + .../testdata/v2-two-cores-256m/expected.json | 1 + .../v2-two-cores-256m/proc/self/cgroup | 1 + .../v2-two-cores-256m/sys/fs/cgroup/cpu.max | 1 + .../sys/fs/cgroup/memory.max | 1 + 20 files changed, 270 insertions(+) create mode 100755 bin/generate-cgroup-testdata create mode 100755 bin/snapshot-cgroup create mode 100644 cgroup/testdata/v2-half-core-128m/expected.json create mode 100644 cgroup/testdata/v2-half-core-128m/proc/self/cgroup create mode 100644 cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max create mode 100644 cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max create mode 100644 cgroup/testdata/v2-no-limit/expected.json create mode 100644 cgroup/testdata/v2-no-limit/proc/self/cgroup create mode 100644 cgroup/testdata/v2-no-limit/sys/fs/cgroup/cpu.max create mode 100644 cgroup/testdata/v2-no-limit/sys/fs/cgroup/memory.max create mode 100644 cgroup/testdata/v2-quarter-core-64m/expected.json create mode 100644 cgroup/testdata/v2-quarter-core-64m/proc/self/cgroup create mode 100644 cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/cpu.max create mode 100644 cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/memory.max create mode 100644 cgroup/testdata/v2-two-cores-256m/expected.json create mode 100644 cgroup/testdata/v2-two-cores-256m/proc/self/cgroup create mode 100644 cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/cpu.max create mode 100644 cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/memory.max diff --git a/Makefile b/Makefile index a9cc9687..cfc964cd 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,11 @@ lint: $(BIN)/staticcheck $(BIN)/misspell clean: rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG) +###### Cgroup testdata ###### +.PHONY: generate-cgroup-testdata +generate-cgroup-testdata: + bin/generate-cgroup-testdata + ###### Go tests ###### .PHONY: test test-setup test-teardown diff --git a/bin/generate-cgroup-testdata b/bin/generate-cgroup-testdata new file mode 100755 index 00000000..b194571a --- /dev/null +++ b/bin/generate-cgroup-testdata @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# generate-cgroup-testdata - Generate cgroup testdata by running +# bin/snapshot-cgroup inside Docker containers with known resource limits. +# +# Usage: generate-cgroup-testdata +# +# Produces directories under cgroup/testdata/, each containing a filesystem +# snapshot and an expected.json with the expected CPU quota and memory limit. +# +# cgroup v2 snapshots are generated automatically via Docker. cgroup v1 +# snapshots must be generated on a host (or VM) running cgroup v1 by +# running bin/snapshot-cgroup manually. +# +# Each testdata directory contains: +# expected.json - {"cpu_quota": , "memory_limit": } +# proc/ - snapshot of /proc/self/cgroup (and mountinfo for v1) +# sys/ - snapshot of cgroup control files + +set -euo pipefail + +cd "$(dirname "$0")/.." + +testdata="cgroup/testdata" +snapshot_script="bin/snapshot-cgroup" + +generate_v2() { + local name="$1" cpu_quota="$2" memory_limit="$3" + shift 3 + local outdir="$testdata/$name" + + echo "Generating $name..." + rm -rf "$outdir" + mkdir -p "$outdir" + + docker run --rm \ + --user "$(id -u):$(id -g)" \ + "$@" \ + -v "$PWD/$snapshot_script:/snapshot:ro" \ + -v "$PWD/$outdir:/out" \ + debian:bookworm-slim \ + /snapshot /out + + cat > "$outdir/expected.json" <" +echo " Then create cgroup/testdata/v1-/expected.json manually." diff --git a/bin/snapshot-cgroup b/bin/snapshot-cgroup new file mode 100755 index 00000000..997c9e1c --- /dev/null +++ b/bin/snapshot-cgroup @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# +# snapshot-cgroup - Copy cgroup-related files from the live system into a +# directory tree suitable for use with CPUQuotaWithRoot / MemoryLimitWithRoot. +# +# Usage: snapshot-cgroup +# +# The script reads /proc/self/cgroup to determine the cgroup version and +# copies exactly the files that the cgroup package needs: +# +# Always: +# proc/self/cgroup +# +# cgroup v2: +# sys/fs/cgroup//cpu.max +# sys/fs/cgroup//memory.max +# +# cgroup v1: +# proc/self/mountinfo +# //cpu.cfs_quota_us +# //cpu.cfs_period_us +# //memory.limit_in_bytes + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +out="$1" +mkdir -p "$out" + +copy_file() { + local src="$1" dst="$out/$2" + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" +} + +# v1_cgroup_rel extracts the cgroup relative path for a given controller +# from /proc/self/cgroup. The controller list field is comma-separated, +# so we split and match exactly. +v1_cgroup_rel() { + local controller="$1" + awk -F: -v ctrl="$controller" ' + $1 != "0" { + n = split($2, ctrls, ",") + for (i = 1; i <= n; i++) { + if (ctrls[i] == ctrl) { print $3; exit } + } + }' /proc/self/cgroup +} + +# v1_mount_point finds the mount point for a cgroup v1 controller from +# /proc/self/mountinfo. It looks for lines with fstype "cgroup" whose +# super-options contain the controller name. +v1_mount_point() { + local controller="$1" + awk -v ctrl="$controller" ' + { + sep = 0 + for (i = 1; i <= NF; i++) { + if ($i == "-") { sep = i; break } + } + if (sep == 0) next + fstype = $(sep + 1) + if (fstype != "cgroup") next + superopts = $(sep + 3) + n = split(superopts, opts, ",") + for (i = 1; i <= n; i++) { + if (opts[i] == ctrl) { print $5; exit } + } + }' /proc/self/mountinfo +} + +snapshot_v2() { + local group cgdir + group=$(awk -F: '/^0::/ { print $3 }' /proc/self/cgroup) + cgdir="/sys/fs/cgroup${group}" + + for f in cpu.max memory.max; do + if [[ -f "$cgdir/$f" ]]; then + copy_file "$cgdir/$f" "sys/fs/cgroup${group}/$f" + fi + done +} + +snapshot_v1() { + copy_file /proc/self/mountinfo proc/self/mountinfo + + # Copy cpu subsystem files. + local cpu_rel cpu_mount cpu_dir + cpu_rel=$(v1_cgroup_rel cpu) + if [[ -n "$cpu_rel" ]]; then + cpu_mount=$(v1_mount_point cpu) + if [[ -n "$cpu_mount" ]]; then + cpu_dir="${cpu_mount}${cpu_rel}" + for f in cpu.cfs_quota_us cpu.cfs_period_us; do + if [[ -f "$cpu_dir/$f" ]]; then + copy_file "$cpu_dir/$f" "${cpu_dir#/}/$f" + fi + done + fi + fi + + # Copy memory subsystem files. + local mem_rel mem_mount mem_dir + mem_rel=$(v1_cgroup_rel memory) + if [[ -n "$mem_rel" ]]; then + mem_mount=$(v1_mount_point memory) + if [[ -n "$mem_mount" ]]; then + mem_dir="${mem_mount}${mem_rel}" + if [[ -f "$mem_dir/memory.limit_in_bytes" ]]; then + copy_file "$mem_dir/memory.limit_in_bytes" "${mem_dir#/}/memory.limit_in_bytes" + fi + fi + fi +} + +copy_file /proc/self/cgroup proc/self/cgroup + +if grep -q '^0::' /proc/self/cgroup; then + snapshot_v2 +else + snapshot_v1 +fi + +echo "Snapshot written to $out" diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index e8b4e7bf..3a7282ed 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -19,6 +19,7 @@ package cgroup import ( + "encoding/json" "errors" "math" "os" @@ -478,3 +479,64 @@ func TestMemoryLimitWithRootV1(t *testing.T) { t.Errorf("MemoryLimitWithRoot() = %v, want 268435456", got) } } + +// testdataExpected holds the expected values from a testdata/*/expected.json. +// Null fields indicate that ErrNoLimit is expected. +type testdataExpected struct { + CPUQuota *float64 `json:"cpu_quota"` + MemoryLimit *int64 `json:"memory_limit"` +} + +// TestWithRootFromTestdata runs CPUQuotaWithRoot and MemoryLimitWithRoot +// against filesystem snapshots captured from real Docker containers (or VMs) +// by bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test +// case containing a proc/ and sys/ tree plus an expected.json. +// +// Regenerate with: bin/generate-cgroup-testdata +func TestWithRootFromTestdata(t *testing.T) { + entries, err := os.ReadDir("testdata") + if err != nil { + t.Fatalf("no testdata directory: %v", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + name := entry.Name() + root := filepath.Join("testdata", name) + + t.Run(name, func(t *testing.T) { + data, err := os.ReadFile(filepath.Join(root, "expected.json")) + if err != nil { + t.Fatalf("reading expected.json: %v", err) + } + var want testdataExpected + if err := json.Unmarshal(data, &want); err != nil { + t.Fatalf("parsing expected.json: %v", err) + } + + gotCPU, err := CPUQuotaWithRoot(root) + if want.CPUQuota == nil { + if !errors.Is(err, ErrNoLimit) { + t.Errorf("CPUQuotaWithRoot() error = %v, want ErrNoLimit", err) + } + } else if err != nil { + t.Fatalf("CPUQuotaWithRoot(%q): %v", root, err) + } else if math.Abs(gotCPU-*want.CPUQuota) > 0.001 { + t.Errorf("CPUQuotaWithRoot() = %v, want %v", gotCPU, *want.CPUQuota) + } + + gotMem, err := MemoryLimitWithRoot(root) + if want.MemoryLimit == nil { + if !errors.Is(err, ErrNoLimit) { + t.Errorf("MemoryLimitWithRoot() error = %v, want ErrNoLimit", err) + } + } else if err != nil { + t.Fatalf("MemoryLimitWithRoot(%q): %v", root, err) + } else if gotMem != *want.MemoryLimit { + t.Errorf("MemoryLimitWithRoot() = %v, want %v", gotMem, *want.MemoryLimit) + } + }) + } +} diff --git a/cgroup/testdata/v2-half-core-128m/expected.json b/cgroup/testdata/v2-half-core-128m/expected.json new file mode 100644 index 00000000..8d190583 --- /dev/null +++ b/cgroup/testdata/v2-half-core-128m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 0.5, "memory_limit": 134217728} diff --git a/cgroup/testdata/v2-half-core-128m/proc/self/cgroup b/cgroup/testdata/v2-half-core-128m/proc/self/cgroup new file mode 100644 index 00000000..1e027b2a --- /dev/null +++ b/cgroup/testdata/v2-half-core-128m/proc/self/cgroup @@ -0,0 +1 @@ +0::/ diff --git a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max new file mode 100644 index 00000000..e4e48b99 --- /dev/null +++ b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max @@ -0,0 +1 @@ +50000 100000 diff --git a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max new file mode 100644 index 00000000..7a758363 --- /dev/null +++ b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max @@ -0,0 +1 @@ +134217728 diff --git a/cgroup/testdata/v2-no-limit/expected.json b/cgroup/testdata/v2-no-limit/expected.json new file mode 100644 index 00000000..3a6d7edc --- /dev/null +++ b/cgroup/testdata/v2-no-limit/expected.json @@ -0,0 +1 @@ +{"cpu_quota": null, "memory_limit": null} diff --git a/cgroup/testdata/v2-no-limit/proc/self/cgroup b/cgroup/testdata/v2-no-limit/proc/self/cgroup new file mode 100644 index 00000000..1e027b2a --- /dev/null +++ b/cgroup/testdata/v2-no-limit/proc/self/cgroup @@ -0,0 +1 @@ +0::/ diff --git a/cgroup/testdata/v2-no-limit/sys/fs/cgroup/cpu.max b/cgroup/testdata/v2-no-limit/sys/fs/cgroup/cpu.max new file mode 100644 index 00000000..1c1d3e7c --- /dev/null +++ b/cgroup/testdata/v2-no-limit/sys/fs/cgroup/cpu.max @@ -0,0 +1 @@ +max 100000 diff --git a/cgroup/testdata/v2-no-limit/sys/fs/cgroup/memory.max b/cgroup/testdata/v2-no-limit/sys/fs/cgroup/memory.max new file mode 100644 index 00000000..355295a0 --- /dev/null +++ b/cgroup/testdata/v2-no-limit/sys/fs/cgroup/memory.max @@ -0,0 +1 @@ +max diff --git a/cgroup/testdata/v2-quarter-core-64m/expected.json b/cgroup/testdata/v2-quarter-core-64m/expected.json new file mode 100644 index 00000000..41ec96fa --- /dev/null +++ b/cgroup/testdata/v2-quarter-core-64m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 0.25, "memory_limit": 67108864} diff --git a/cgroup/testdata/v2-quarter-core-64m/proc/self/cgroup b/cgroup/testdata/v2-quarter-core-64m/proc/self/cgroup new file mode 100644 index 00000000..1e027b2a --- /dev/null +++ b/cgroup/testdata/v2-quarter-core-64m/proc/self/cgroup @@ -0,0 +1 @@ +0::/ diff --git a/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/cpu.max b/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/cpu.max new file mode 100644 index 00000000..6fe34582 --- /dev/null +++ b/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/cpu.max @@ -0,0 +1 @@ +25000 100000 diff --git a/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/memory.max b/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/memory.max new file mode 100644 index 00000000..e6c68622 --- /dev/null +++ b/cgroup/testdata/v2-quarter-core-64m/sys/fs/cgroup/memory.max @@ -0,0 +1 @@ +67108864 diff --git a/cgroup/testdata/v2-two-cores-256m/expected.json b/cgroup/testdata/v2-two-cores-256m/expected.json new file mode 100644 index 00000000..04ce067c --- /dev/null +++ b/cgroup/testdata/v2-two-cores-256m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 2.0, "memory_limit": 268435456} diff --git a/cgroup/testdata/v2-two-cores-256m/proc/self/cgroup b/cgroup/testdata/v2-two-cores-256m/proc/self/cgroup new file mode 100644 index 00000000..1e027b2a --- /dev/null +++ b/cgroup/testdata/v2-two-cores-256m/proc/self/cgroup @@ -0,0 +1 @@ +0::/ diff --git a/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/cpu.max b/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/cpu.max new file mode 100644 index 00000000..96856569 --- /dev/null +++ b/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/cpu.max @@ -0,0 +1 @@ +200000 100000 diff --git a/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/memory.max b/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/memory.max new file mode 100644 index 00000000..853f47e7 --- /dev/null +++ b/cgroup/testdata/v2-two-cores-256m/sys/fs/cgroup/memory.max @@ -0,0 +1 @@ +268435456 From 5b52f6ad170a166a38eb41cca4c427e9624e3c50 Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:15:55 +0100 Subject: [PATCH 4/9] Add v1 testdata generation --- Makefile | 8 +- bin/cgroupv1.yaml | 62 ++++++++++++++ bin/generate-cgroup-testdata | 59 -------------- bin/generate-cgroupv1-testdata | 65 +++++++++++++++ bin/generate-cgroupv2-testdata | 49 +++++++++++ bin/snapshot-cgroup | 81 ++++++++++++------- cgroup/cgroup.go | 31 +++++-- cgroup/cgroup_test.go | 5 +- .../testdata/v1-half-core-128m/expected.json | 1 + .../v1-half-core-128m/proc/self/cgroup | 13 +++ .../v1-half-core-128m/proc/self/mountinfo | 38 +++++++++ .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 + .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 + .../fs/cgroup/memory/memory.limit_in_bytes | 1 + cgroup/testdata/v1-no-limit/expected.json | 1 + cgroup/testdata/v1-no-limit/proc/self/cgroup | 13 +++ .../testdata/v1-no-limit/proc/self/mountinfo | 38 +++++++++ .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 + .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 + .../fs/cgroup/memory/memory.limit_in_bytes | 1 + .../v1-quarter-core-64m/expected.json | 1 + .../v1-quarter-core-64m/proc/self/cgroup | 13 +++ .../v1-quarter-core-64m/proc/self/mountinfo | 38 +++++++++ .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 + .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 + .../fs/cgroup/memory/memory.limit_in_bytes | 1 + .../testdata/v1-two-cores-256m/expected.json | 1 + .../v1-two-cores-256m/proc/self/cgroup | 13 +++ .../v1-two-cores-256m/proc/self/mountinfo | 38 +++++++++ .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 + .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 + .../fs/cgroup/memory/memory.limit_in_bytes | 1 + 32 files changed, 479 insertions(+), 101 deletions(-) create mode 100644 bin/cgroupv1.yaml delete mode 100755 bin/generate-cgroup-testdata create mode 100755 bin/generate-cgroupv1-testdata create mode 100755 bin/generate-cgroupv2-testdata create mode 100644 cgroup/testdata/v1-half-core-128m/expected.json create mode 100644 cgroup/testdata/v1-half-core-128m/proc/self/cgroup create mode 100644 cgroup/testdata/v1-half-core-128m/proc/self/mountinfo create mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us create mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us create mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes create mode 100644 cgroup/testdata/v1-no-limit/expected.json create mode 100644 cgroup/testdata/v1-no-limit/proc/self/cgroup create mode 100644 cgroup/testdata/v1-no-limit/proc/self/mountinfo create mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us create mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us create mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes create mode 100644 cgroup/testdata/v1-quarter-core-64m/expected.json create mode 100644 cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup create mode 100644 cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo create mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us create mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us create mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes create mode 100644 cgroup/testdata/v1-two-cores-256m/expected.json create mode 100644 cgroup/testdata/v1-two-cores-256m/proc/self/cgroup create mode 100644 cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo create mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us create mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us create mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes diff --git a/Makefile b/Makefile index cfc964cd..b6a6faa9 100644 --- a/Makefile +++ b/Makefile @@ -116,9 +116,11 @@ clean: rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG) ###### Cgroup testdata ###### -.PHONY: generate-cgroup-testdata -generate-cgroup-testdata: - bin/generate-cgroup-testdata +.PHONY: generate-cgroupv1-testdata generate-cgroupv2-testdata +generate-cgroupv1-testdata: + bin/generate-cgroupv1-testdata +generate-cgroupv2-testdata: + bin/generate-cgroupv2-testdata ###### Go tests ###### .PHONY: test test-setup test-teardown diff --git a/bin/cgroupv1.yaml b/bin/cgroupv1.yaml new file mode 100644 index 00000000..bd4011f3 --- /dev/null +++ b/bin/cgroupv1.yaml @@ -0,0 +1,62 @@ +# Minimal Lima VM running Ubuntu 20.04 (Focal) for cgroup v1 testdata generation. +# +# Ubuntu 20.04 uses cgroup v1 by default, so no kernel parameter changes are +# needed. Docker is installed via a provisioning script so that containers can +# be launched with --cpus / --memory resource limits. +# +# Usage: +# limactl start --name=cgroupv1 bin/cgroupv1.yaml +# bin/generate-cgroupv1-testdata +# limactl stop cgroupv1 # when done + +images: + - location: "https://cloud-images.ubuntu.com/releases/20.04/release/ubuntu-20.04-server-cloudimg-amd64.img" + arch: "x86_64" + - location: "https://cloud-images.ubuntu.com/releases/20.04/release/ubuntu-20.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 2 +memory: "2GiB" +disk: "10GiB" + +mounts: + - location: "~" + writable: true + +containerd: + system: false + user: false + +provision: + - mode: system + script: | + #!/bin/bash + set -eux -o pipefail + if command -v docker &>/dev/null; then + exit 0 + fi + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y -qq ca-certificates curl gnupg lsb-release >/dev/null + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ + gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ + > /etc/apt/sources.list.d/docker.list + apt-get update -qq + apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null + usermod -aG docker "{{.User}}" + +probes: + - mode: readiness + description: docker to be ready + script: | + #!/bin/bash + set -eux -o pipefail + if ! timeout 60s bash -c "until sudo docker info &>/dev/null; do sleep 3; done"; then + echo >&2 "docker is not ready yet" + exit 1 + fi diff --git a/bin/generate-cgroup-testdata b/bin/generate-cgroup-testdata deleted file mode 100755 index b194571a..00000000 --- a/bin/generate-cgroup-testdata +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# -# generate-cgroup-testdata - Generate cgroup testdata by running -# bin/snapshot-cgroup inside Docker containers with known resource limits. -# -# Usage: generate-cgroup-testdata -# -# Produces directories under cgroup/testdata/, each containing a filesystem -# snapshot and an expected.json with the expected CPU quota and memory limit. -# -# cgroup v2 snapshots are generated automatically via Docker. cgroup v1 -# snapshots must be generated on a host (or VM) running cgroup v1 by -# running bin/snapshot-cgroup manually. -# -# Each testdata directory contains: -# expected.json - {"cpu_quota": , "memory_limit": } -# proc/ - snapshot of /proc/self/cgroup (and mountinfo for v1) -# sys/ - snapshot of cgroup control files - -set -euo pipefail - -cd "$(dirname "$0")/.." - -testdata="cgroup/testdata" -snapshot_script="bin/snapshot-cgroup" - -generate_v2() { - local name="$1" cpu_quota="$2" memory_limit="$3" - shift 3 - local outdir="$testdata/$name" - - echo "Generating $name..." - rm -rf "$outdir" - mkdir -p "$outdir" - - docker run --rm \ - --user "$(id -u):$(id -g)" \ - "$@" \ - -v "$PWD/$snapshot_script:/snapshot:ro" \ - -v "$PWD/$outdir:/out" \ - debian:bookworm-slim \ - /snapshot /out - - cat > "$outdir/expected.json" <" -echo " Then create cgroup/testdata/v1-/expected.json manually." diff --git a/bin/generate-cgroupv1-testdata b/bin/generate-cgroupv1-testdata new file mode 100755 index 00000000..4f92f5ee --- /dev/null +++ b/bin/generate-cgroupv1-testdata @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# generate-cgroupv1-testdata - Generate cgroup v1 testdata by running +# bin/snapshot-cgroup inside Docker containers with known resource limits, +# inside a Lima VM running Ubuntu 20.04 (which uses cgroup v1 by default). +# +# Usage: generate-cgroupv1-testdata +# +# Prerequisites: +# Lima with a running "cgroupv1" instance: +# limactl start --name=cgroupv1 bin/cgroupv1.yaml +# +# Each testdata directory contains: +# expected.json - {"cpu_quota": , "memory_limit": } +# proc/ - snapshot of /proc/self/cgroup and /proc/self/mountinfo +# sys/ - snapshot of cgroup control files + +set -euo pipefail + +cd "$(dirname "$0")/.." + +testdata="cgroup/testdata" +snapshot_script="bin/snapshot-cgroup" +lima_instance="cgroupv1" + +ensure_lima_vm() { + if ! limactl list --json 2>/dev/null \ + | jq -e "select(.name == \"$lima_instance\" and .status == \"Running\")" \ + >/dev/null 2>&1; then + echo "Error: Lima instance '$lima_instance' is not running." >&2 + echo "Start it with: limactl start --name=$lima_instance bin/cgroupv1.yaml" >&2 + exit 1 + fi +} + +generate() { + local name="$1" cpu_quota="$2" memory_limit="$3" + shift 3 + local outdir="$testdata/$name" + + echo "Generating $name..." + rm -rf "$outdir" + mkdir -p "$outdir" + + limactl shell "$lima_instance" \ + sudo docker run --rm \ + "$@" \ + -v "$PWD/$snapshot_script:/snapshot:ro" \ + -v "$PWD/$outdir:/out" \ + debian:bookworm-slim \ + /snapshot /out + + cat > "$outdir/expected.json" <, "memory_limit": } +# proc/ - snapshot of /proc/self/cgroup +# sys/ - snapshot of cgroup control files + +set -euo pipefail + +cd "$(dirname "$0")/.." + +testdata="cgroup/testdata" +snapshot_script="bin/snapshot-cgroup" + +generate() { + local name="$1" cpu_quota="$2" memory_limit="$3" + shift 3 + local outdir="$testdata/$name" + + echo "Generating $name..." + rm -rf "$outdir" + mkdir -p "$outdir" + + docker run --rm \ + --user "$(id -u):$(id -g)" \ + "$@" \ + -v "$PWD/$snapshot_script:/snapshot:ro" \ + -v "$PWD/$outdir:/out" \ + debian:bookworm-slim \ + /snapshot /out + + cat > "$outdir/expected.json" < 0 { + result.version = 1 + } else if result.v2GroupPath != "" { + result.version = 2 + } else { + result.version = 1 + } + return result, nil } // cpuQuotaV2 reads cpu.max from the given cgroup v2 directory. diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index 3a7282ed..d7de0b8f 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -200,8 +200,7 @@ func TestParseProcCgroup(t *testing.T) { { name: "hybrid v1 and v2", content: "12:memory:/docker/abc123\n0::/docker/abc123\n", - wantVersion: 2, - wantGroup: "/docker/abc123", + wantVersion: 1, wantSubsystems: map[string]string{ "memory": "/docker/abc123", }, @@ -492,7 +491,7 @@ type testdataExpected struct { // by bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test // case containing a proc/ and sys/ tree plus an expected.json. // -// Regenerate with: bin/generate-cgroup-testdata +// Regenerate with: bin/generate-cgroupv1-testdata and bin/generate-cgroupv2-testdata func TestWithRootFromTestdata(t *testing.T) { entries, err := os.ReadDir("testdata") if err != nil { diff --git a/cgroup/testdata/v1-half-core-128m/expected.json b/cgroup/testdata/v1-half-core-128m/expected.json new file mode 100644 index 00000000..8d190583 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 0.5, "memory_limit": 134217728} diff --git a/cgroup/testdata/v1-half-core-128m/proc/self/cgroup b/cgroup/testdata/v1-half-core-128m/proc/self/cgroup new file mode 100644 index 00000000..4e3f5b44 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/proc/self/cgroup @@ -0,0 +1,13 @@ +12:pids:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +11:cpuset:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +10:freezer:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +9:cpu,cpuacct:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +8:perf_event:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +7:blkio:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +6:hugetlb:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +5:devices:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +4:memory:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +3:net_cls,net_prio:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +2:rdma:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +1:name=systemd:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 +0::/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 diff --git a/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo b/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo new file mode 100644 index 00000000..fd3ad158 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo @@ -0,0 +1,38 @@ +871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/3QUPKHUTWAY2BZUBSN3T2R2PWF:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/7ca228548702b25c7e85de3ef3d26f9926cfc4e4744cbcf499cf9f2873298bc1/diff,workdir=/var/lib/docker/overlay2/7ca228548702b25c7e85de3ef3d26f9926cfc4e4744cbcf499cf9f2873298bc1/work,xino=off +873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +878 877 0:31 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +879 877 0:35 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma +880 877 0:36 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio +881 877 0:37 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory +882 877 0:38 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices +883 877 0:39 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb +884 877 0:40 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio +885 877 0:41 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event +886 877 0:42 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct +887 877 0:43 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer +888 877 0:44 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +889 877 0:45 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids +890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-half-core-128m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +894 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw +895 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw +896 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw +558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro +569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro +575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us new file mode 100644 index 00000000..f7393e84 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us @@ -0,0 +1 @@ +100000 diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us new file mode 100644 index 00000000..ccfc37a1 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us @@ -0,0 +1 @@ +50000 diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes new file mode 100644 index 00000000..7a758363 --- /dev/null +++ b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +134217728 diff --git a/cgroup/testdata/v1-no-limit/expected.json b/cgroup/testdata/v1-no-limit/expected.json new file mode 100644 index 00000000..3a6d7edc --- /dev/null +++ b/cgroup/testdata/v1-no-limit/expected.json @@ -0,0 +1 @@ +{"cpu_quota": null, "memory_limit": null} diff --git a/cgroup/testdata/v1-no-limit/proc/self/cgroup b/cgroup/testdata/v1-no-limit/proc/self/cgroup new file mode 100644 index 00000000..ccf55785 --- /dev/null +++ b/cgroup/testdata/v1-no-limit/proc/self/cgroup @@ -0,0 +1,13 @@ +12:pids:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +11:cpuset:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +10:freezer:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +9:cpu,cpuacct:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +8:perf_event:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +7:blkio:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +6:hugetlb:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +5:devices:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +4:memory:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +3:net_cls,net_prio:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +2:rdma:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +1:name=systemd:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 +0::/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 diff --git a/cgroup/testdata/v1-no-limit/proc/self/mountinfo b/cgroup/testdata/v1-no-limit/proc/self/mountinfo new file mode 100644 index 00000000..2a3acfd4 --- /dev/null +++ b/cgroup/testdata/v1-no-limit/proc/self/mountinfo @@ -0,0 +1,38 @@ +871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/J2XQFXRCNRAXPQQY6DIPC5SVWZ:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/2cfe4ceb28a58c6866b45004ab74875b38ccc7b1396cddd4bf9da9908679bbb9/diff,workdir=/var/lib/docker/overlay2/2cfe4ceb28a58c6866b45004ab74875b38ccc7b1396cddd4bf9da9908679bbb9/work,xino=off +873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +878 877 0:31 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +879 877 0:35 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma +880 877 0:36 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio +881 877 0:37 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory +882 877 0:38 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices +883 877 0:39 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb +884 877 0:40 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio +885 877 0:41 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event +886 877 0:42 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct +887 877 0:43 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer +888 877 0:44 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +889 877 0:45 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids +890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-no-limit /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +894 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw +895 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw +896 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw +558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro +569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro +575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us new file mode 100644 index 00000000..f7393e84 --- /dev/null +++ b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us @@ -0,0 +1 @@ +100000 diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us new file mode 100644 index 00000000..3a2e3f49 --- /dev/null +++ b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us @@ -0,0 +1 @@ +-1 diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes new file mode 100644 index 00000000..564113cf --- /dev/null +++ b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +9223372036854771712 diff --git a/cgroup/testdata/v1-quarter-core-64m/expected.json b/cgroup/testdata/v1-quarter-core-64m/expected.json new file mode 100644 index 00000000..41ec96fa --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 0.25, "memory_limit": 67108864} diff --git a/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup b/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup new file mode 100644 index 00000000..0f95e57c --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup @@ -0,0 +1,13 @@ +12:pids:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +11:cpuset:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +10:freezer:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +9:cpu,cpuacct:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +8:perf_event:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +7:blkio:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +6:hugetlb:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +5:devices:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +4:memory:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +3:net_cls,net_prio:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +2:rdma:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +1:name=systemd:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 +0::/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 diff --git a/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo b/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo new file mode 100644 index 00000000..cf35c163 --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo @@ -0,0 +1,38 @@ +871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/3H65BZBNFDRRM7Y44R52363AQT:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/632fa95736287ba3cb0c70e5b398faf66993b843a2fee9135939efb82cbb1ebd/diff,workdir=/var/lib/docker/overlay2/632fa95736287ba3cb0c70e5b398faf66993b843a2fee9135939efb82cbb1ebd/work,xino=off +873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +878 877 0:31 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +879 877 0:35 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma +880 877 0:36 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio +881 877 0:37 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory +882 877 0:38 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices +883 877 0:39 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb +884 877 0:40 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio +885 877 0:41 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event +886 877 0:42 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct +887 877 0:43 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer +888 877 0:44 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +889 877 0:45 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids +890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +892 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-quarter-core-64m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +893 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +894 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw +895 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw +896 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw +558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro +569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro +575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us new file mode 100644 index 00000000..f7393e84 --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us @@ -0,0 +1 @@ +100000 diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us new file mode 100644 index 00000000..e87f3b8e --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us @@ -0,0 +1 @@ +25000 diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes new file mode 100644 index 00000000..e6c68622 --- /dev/null +++ b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +67108864 diff --git a/cgroup/testdata/v1-two-cores-256m/expected.json b/cgroup/testdata/v1-two-cores-256m/expected.json new file mode 100644 index 00000000..04ce067c --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/expected.json @@ -0,0 +1 @@ +{"cpu_quota": 2.0, "memory_limit": 268435456} diff --git a/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup b/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup new file mode 100644 index 00000000..fb4461ee --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup @@ -0,0 +1,13 @@ +12:pids:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +11:cpuset:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +10:freezer:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +9:cpu,cpuacct:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +8:perf_event:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +7:blkio:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +6:hugetlb:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +5:devices:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +4:memory:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +3:net_cls,net_prio:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +2:rdma:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +1:name=systemd:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 +0::/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 diff --git a/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo b/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo new file mode 100644 index 00000000..2b1a8702 --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo @@ -0,0 +1,38 @@ +871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/RXYCY5MVMMC6NWBBRBFODFMAS5:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/c54155e4d9fedcbaa545064f3b5a78d68c760132cd9ea23152cf62ff485f9756/diff,workdir=/var/lib/docker/overlay2/c54155e4d9fedcbaa545064f3b5a78d68c760132cd9ea23152cf62ff485f9756/work,xino=off +873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw +874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro +877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 +878 877 0:31 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd +879 877 0:35 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma +880 877 0:36 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio +881 877 0:37 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory +882 877 0:38 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices +883 877 0:39 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb +884 877 0:40 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio +885 877 0:41 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event +886 877 0:42 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct +887 877 0:43 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer +888 877 0:44 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +889 877 0:45 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids +890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw +891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k +892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-two-cores-256m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio +894 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw +895 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw +896 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw +558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw +559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw +560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw +566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw +567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw +568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro +569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 +574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro +575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us new file mode 100644 index 00000000..f7393e84 --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us @@ -0,0 +1 @@ +100000 diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us new file mode 100644 index 00000000..87766d88 --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us @@ -0,0 +1 @@ +200000 diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes new file mode 100644 index 00000000..853f47e7 --- /dev/null +++ b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +268435456 From 5d605cb609f60d48c1639f400a4b0b534271d28d Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:20:10 +0100 Subject: [PATCH 5/9] Move scripts around --- Makefile | 10 +++++----- ...enerate-cgroupv1-testdata => gen-cgroupv1-testdata} | 8 ++++---- ...enerate-cgroupv2-testdata => gen-cgroupv2-testdata} | 4 ++-- cgroup/cgroup_test.go | 2 +- {bin => cgroup/testdata}/cgroupv1.yaml | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) rename bin/{generate-cgroupv1-testdata => gen-cgroupv1-testdata} (90%) rename bin/{generate-cgroupv2-testdata => gen-cgroupv2-testdata} (92%) rename {bin => cgroup/testdata}/cgroupv1.yaml (95%) diff --git a/Makefile b/Makefile index b6a6faa9..6014df3b 100644 --- a/Makefile +++ b/Makefile @@ -116,11 +116,11 @@ clean: rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG) ###### Cgroup testdata ###### -.PHONY: generate-cgroupv1-testdata generate-cgroupv2-testdata -generate-cgroupv1-testdata: - bin/generate-cgroupv1-testdata -generate-cgroupv2-testdata: - bin/generate-cgroupv2-testdata +.PHONY: gen-cgroupv1-testdata gen-cgroupv2-testdata +gen-cgroupv1-testdata: + bin/gen-cgroupv1-testdata +gen-cgroupv2-testdata: + bin/gen-cgroupv2-testdata ###### Go tests ###### .PHONY: test test-setup test-teardown diff --git a/bin/generate-cgroupv1-testdata b/bin/gen-cgroupv1-testdata similarity index 90% rename from bin/generate-cgroupv1-testdata rename to bin/gen-cgroupv1-testdata index 4f92f5ee..1a298d06 100755 --- a/bin/generate-cgroupv1-testdata +++ b/bin/gen-cgroupv1-testdata @@ -1,14 +1,14 @@ #!/usr/bin/env bash # -# generate-cgroupv1-testdata - Generate cgroup v1 testdata by running +# gen-cgroupv1-testdata - Generate cgroup v1 testdata by running # bin/snapshot-cgroup inside Docker containers with known resource limits, # inside a Lima VM running Ubuntu 20.04 (which uses cgroup v1 by default). # -# Usage: generate-cgroupv1-testdata +# Usage: gen-cgroupv1-testdata # # Prerequisites: # Lima with a running "cgroupv1" instance: -# limactl start --name=cgroupv1 bin/cgroupv1.yaml +# limactl start --name=cgroupv1 cgroup/testdata/cgroupv1.yaml # # Each testdata directory contains: # expected.json - {"cpu_quota": , "memory_limit": } @@ -28,7 +28,7 @@ ensure_lima_vm() { | jq -e "select(.name == \"$lima_instance\" and .status == \"Running\")" \ >/dev/null 2>&1; then echo "Error: Lima instance '$lima_instance' is not running." >&2 - echo "Start it with: limactl start --name=$lima_instance bin/cgroupv1.yaml" >&2 + echo "Start it with: limactl start --name=$lima_instance cgroup/testdata/cgroupv1.yaml" >&2 exit 1 fi } diff --git a/bin/generate-cgroupv2-testdata b/bin/gen-cgroupv2-testdata similarity index 92% rename from bin/generate-cgroupv2-testdata rename to bin/gen-cgroupv2-testdata index d6392c01..89fea1c1 100755 --- a/bin/generate-cgroupv2-testdata +++ b/bin/gen-cgroupv2-testdata @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# generate-cgroupv2-testdata - Generate cgroup v2 testdata by running +# gen-cgroupv2-testdata - Generate cgroup v2 testdata by running # bin/snapshot-cgroup inside Docker containers with known resource limits. # -# Usage: generate-cgroupv2-testdata +# Usage: gen-cgroupv2-testdata # # Prerequisites: Docker on a host running cgroup v2. # diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index d7de0b8f..a4480ea0 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -491,7 +491,7 @@ type testdataExpected struct { // by bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test // case containing a proc/ and sys/ tree plus an expected.json. // -// Regenerate with: bin/generate-cgroupv1-testdata and bin/generate-cgroupv2-testdata +// Regenerate with: bin/gen-cgroupv1-testdata and bin/gen-cgroupv2-testdata func TestWithRootFromTestdata(t *testing.T) { entries, err := os.ReadDir("testdata") if err != nil { diff --git a/bin/cgroupv1.yaml b/cgroup/testdata/cgroupv1.yaml similarity index 95% rename from bin/cgroupv1.yaml rename to cgroup/testdata/cgroupv1.yaml index bd4011f3..a04ae313 100644 --- a/bin/cgroupv1.yaml +++ b/cgroup/testdata/cgroupv1.yaml @@ -5,8 +5,8 @@ # be launched with --cpus / --memory resource limits. # # Usage: -# limactl start --name=cgroupv1 bin/cgroupv1.yaml -# bin/generate-cgroupv1-testdata +# limactl start --name=cgroupv1 cgroup/testdata/cgroupv1.yaml +# bin/gen-cgroupv1-testdata # limactl stop cgroupv1 # when done images: From 6e584c2046313f15b6cde09dd2d5864e21973cb9 Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:07:53 +0100 Subject: [PATCH 6/9] Add integration test in CI --- .github/workflows/ci.yml | 20 +++++++++++++++++++ cgroup/cgroup_test.go | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1975e0c..973666ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,6 +106,26 @@ jobs: # make test # make test-teardown + test-cgroup-integration: + name: Test cgroup integration + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v2 + with: + go-version: '1.25' + - name: Build test binary + run: go test -c -o cgroup.test ./cgroup/ + - name: Run cgroup integration test + run: | + docker run --rm \ + --cpus=0.5 --memory=128m \ + -e CGROUP_EXPECTED_CPU_QUOTA=0.5 \ + -e CGROUP_EXPECTED_MEMORY_LIMIT=134217728 \ + -v "$PWD/cgroup.test":/cgroup.test:ro \ + debian:bookworm-slim \ + /cgroup.test -test.run TestIntegrationCgroupLimits -test.v + run-cli-tests: name: Run command-line interface tests runs-on: ubuntu-latest diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index a4480ea0..374a4d42 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -24,6 +24,7 @@ import ( "math" "os" "path/filepath" + "strconv" "strings" "testing" ) @@ -539,3 +540,45 @@ func TestWithRootFromTestdata(t *testing.T) { }) } } + +// TestIntegrationCgroupLimits calls the real CPUQuota() and MemoryLimit() +// functions against the live kernel cgroup interface. It is intended to run +// inside a Docker container started with --cpus and --memory flags. +// +// The test is skipped unless CGROUP_EXPECTED_CPU_QUOTA and +// CGROUP_EXPECTED_MEMORY_LIMIT are set in the environment. +func TestIntegrationCgroupLimits(t *testing.T) { + cpuStr := os.Getenv("CGROUP_EXPECTED_CPU_QUOTA") + memStr := os.Getenv("CGROUP_EXPECTED_MEMORY_LIMIT") + if cpuStr == "" && memStr == "" { + t.Skip("set CGROUP_EXPECTED_CPU_QUOTA and CGROUP_EXPECTED_MEMORY_LIMIT to run") + } + + if cpuStr != "" { + wantCPU, err := strconv.ParseFloat(cpuStr, 64) + if err != nil { + t.Fatalf("bad CGROUP_EXPECTED_CPU_QUOTA %q: %v", cpuStr, err) + } + gotCPU, err := CPUQuota() + if err != nil { + t.Fatalf("CPUQuota() error: %v", err) + } + if math.Abs(gotCPU-wantCPU) > 0.001 { + t.Errorf("CPUQuota() = %v, want %v", gotCPU, wantCPU) + } + } + + if memStr != "" { + wantMem, err := strconv.ParseInt(memStr, 10, 64) + if err != nil { + t.Fatalf("bad CGROUP_EXPECTED_MEMORY_LIMIT %q: %v", memStr, err) + } + gotMem, err := MemoryLimit() + if err != nil { + t.Fatalf("MemoryLimit() error: %v", err) + } + if gotMem != wantMem { + t.Errorf("MemoryLimit() = %v, want %v", gotMem, wantMem) + } + } +} From 4c92746994e21c0f083581daea7fbbc1efebf400 Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:52:38 +0100 Subject: [PATCH 7/9] Remove cgroup v1 --- Makefile | 8 +- ...-cgroupv2-testdata => gen-cgroup-testdata} | 4 +- bin/gen-cgroupv1-testdata | 65 ---- bin/snapshot-cgroup | 137 +-------- cgroup/cgroup.go | 228 +------------- cgroup/cgroup_test.go | 278 ++---------------- cgroup/testdata/cgroupv1.yaml | 62 ---- .../testdata/v1-half-core-128m/expected.json | 1 - .../v1-half-core-128m/proc/self/cgroup | 13 - .../v1-half-core-128m/proc/self/mountinfo | 38 --- .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 - .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 - .../fs/cgroup/memory/memory.limit_in_bytes | 1 - cgroup/testdata/v1-no-limit/expected.json | 1 - cgroup/testdata/v1-no-limit/proc/self/cgroup | 13 - .../testdata/v1-no-limit/proc/self/mountinfo | 38 --- .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 - .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 - .../fs/cgroup/memory/memory.limit_in_bytes | 1 - .../v1-quarter-core-64m/expected.json | 1 - .../v1-quarter-core-64m/proc/self/cgroup | 13 - .../v1-quarter-core-64m/proc/self/mountinfo | 38 --- .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 - .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 - .../fs/cgroup/memory/memory.limit_in_bytes | 1 - .../testdata/v1-two-cores-256m/expected.json | 1 - .../v1-two-cores-256m/proc/self/cgroup | 13 - .../v1-two-cores-256m/proc/self/mountinfo | 38 --- .../fs/cgroup/cpu,cpuacct/cpu.cfs_period_us | 1 - .../fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us | 1 - .../fs/cgroup/memory/memory.limit_in_bytes | 1 - 31 files changed, 60 insertions(+), 942 deletions(-) rename bin/{gen-cgroupv2-testdata => gen-cgroup-testdata} (93%) delete mode 100755 bin/gen-cgroupv1-testdata delete mode 100644 cgroup/testdata/cgroupv1.yaml delete mode 100644 cgroup/testdata/v1-half-core-128m/expected.json delete mode 100644 cgroup/testdata/v1-half-core-128m/proc/self/cgroup delete mode 100644 cgroup/testdata/v1-half-core-128m/proc/self/mountinfo delete mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us delete mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us delete mode 100644 cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes delete mode 100644 cgroup/testdata/v1-no-limit/expected.json delete mode 100644 cgroup/testdata/v1-no-limit/proc/self/cgroup delete mode 100644 cgroup/testdata/v1-no-limit/proc/self/mountinfo delete mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us delete mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us delete mode 100644 cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes delete mode 100644 cgroup/testdata/v1-quarter-core-64m/expected.json delete mode 100644 cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup delete mode 100644 cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo delete mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us delete mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us delete mode 100644 cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes delete mode 100644 cgroup/testdata/v1-two-cores-256m/expected.json delete mode 100644 cgroup/testdata/v1-two-cores-256m/proc/self/cgroup delete mode 100644 cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo delete mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us delete mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us delete mode 100644 cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes diff --git a/Makefile b/Makefile index 6014df3b..d5422484 100644 --- a/Makefile +++ b/Makefile @@ -116,11 +116,9 @@ clean: rm -f $(BIN)/$(NAME) $(PAM_MODULE) $(TOOLS) coverage.out $(COVERAGE_FILES) $(PAM_CONFIG) ###### Cgroup testdata ###### -.PHONY: gen-cgroupv1-testdata gen-cgroupv2-testdata -gen-cgroupv1-testdata: - bin/gen-cgroupv1-testdata -gen-cgroupv2-testdata: - bin/gen-cgroupv2-testdata +.PHONY: gen-cgroup-testdata +gen-cgroup-testdata: + bin/gen-cgroup-testdata ###### Go tests ###### .PHONY: test test-setup test-teardown diff --git a/bin/gen-cgroupv2-testdata b/bin/gen-cgroup-testdata similarity index 93% rename from bin/gen-cgroupv2-testdata rename to bin/gen-cgroup-testdata index 89fea1c1..6f0a15f7 100755 --- a/bin/gen-cgroupv2-testdata +++ b/bin/gen-cgroup-testdata @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# gen-cgroupv2-testdata - Generate cgroup v2 testdata by running +# gen-cgroup-testdata - Generate cgroup testdata by running # bin/snapshot-cgroup inside Docker containers with known resource limits. # -# Usage: gen-cgroupv2-testdata +# Usage: gen-cgroup-testdata # # Prerequisites: Docker on a host running cgroup v2. # diff --git a/bin/gen-cgroupv1-testdata b/bin/gen-cgroupv1-testdata deleted file mode 100755 index 1a298d06..00000000 --- a/bin/gen-cgroupv1-testdata +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# -# gen-cgroupv1-testdata - Generate cgroup v1 testdata by running -# bin/snapshot-cgroup inside Docker containers with known resource limits, -# inside a Lima VM running Ubuntu 20.04 (which uses cgroup v1 by default). -# -# Usage: gen-cgroupv1-testdata -# -# Prerequisites: -# Lima with a running "cgroupv1" instance: -# limactl start --name=cgroupv1 cgroup/testdata/cgroupv1.yaml -# -# Each testdata directory contains: -# expected.json - {"cpu_quota": , "memory_limit": } -# proc/ - snapshot of /proc/self/cgroup and /proc/self/mountinfo -# sys/ - snapshot of cgroup control files - -set -euo pipefail - -cd "$(dirname "$0")/.." - -testdata="cgroup/testdata" -snapshot_script="bin/snapshot-cgroup" -lima_instance="cgroupv1" - -ensure_lima_vm() { - if ! limactl list --json 2>/dev/null \ - | jq -e "select(.name == \"$lima_instance\" and .status == \"Running\")" \ - >/dev/null 2>&1; then - echo "Error: Lima instance '$lima_instance' is not running." >&2 - echo "Start it with: limactl start --name=$lima_instance cgroup/testdata/cgroupv1.yaml" >&2 - exit 1 - fi -} - -generate() { - local name="$1" cpu_quota="$2" memory_limit="$3" - shift 3 - local outdir="$testdata/$name" - - echo "Generating $name..." - rm -rf "$outdir" - mkdir -p "$outdir" - - limactl shell "$lima_instance" \ - sudo docker run --rm \ - "$@" \ - -v "$PWD/$snapshot_script:/snapshot:ro" \ - -v "$PWD/$outdir:/out" \ - debian:bookworm-slim \ - /snapshot /out - - cat > "$outdir/expected.json" < # -# The script reads /proc/self/cgroup to determine the cgroup version and -# copies exactly the files that the cgroup package needs: +# The script reads /proc/self/cgroup to find the v2 group path and copies +# exactly the files that the cgroup package needs: # -# Always: -# proc/self/cgroup -# -# cgroup v2: -# sys/fs/cgroup//cpu.max -# sys/fs/cgroup//memory.max -# -# cgroup v1: -# proc/self/mountinfo -# //cpu.cfs_quota_us -# //cpu.cfs_period_us -# //memory.limit_in_bytes +# proc/self/cgroup +# sys/fs/cgroup//cpu.max +# sys/fs/cgroup//memory.max set -euo pipefail @@ -37,117 +28,15 @@ copy_file() { cp "$src" "$dst" } -# v1_cgroup_rel extracts the cgroup relative path for a given controller -# from /proc/self/cgroup. The controller list field is comma-separated, -# so we split and match exactly. -v1_cgroup_rel() { - local controller="$1" - awk -F: -v ctrl="$controller" ' - $1 != "0" { - n = split($2, ctrls, ",") - for (i = 1; i <= n; i++) { - if (ctrls[i] == ctrl) { print $3; exit } - } - }' /proc/self/cgroup -} - -# v1_mount_info prints "mount_point mount_root" for a cgroup v1 controller -# from /proc/self/mountinfo. It looks for lines with fstype "cgroup" whose -# super-options contain the controller name. -v1_mount_info() { - local controller="$1" - awk -v ctrl="$controller" ' - { - sep = 0 - for (i = 1; i <= NF; i++) { - if ($i == "-") { sep = i; break } - } - if (sep == 0) next - fstype = $(sep + 1) - if (fstype != "cgroup") next - superopts = $(sep + 3) - n = split(superopts, opts, ",") - for (i = 1; i <= n; i++) { - if (opts[i] == ctrl) { print $5, $4; exit } - } - }' /proc/self/mountinfo -} - -snapshot_v2() { - local group cgdir - group=$(awk -F: '/^0::/ { print $3 }' /proc/self/cgroup) - cgdir="/sys/fs/cgroup${group}" - - for f in cpu.max memory.max; do - if [[ -f "$cgdir/$f" ]]; then - copy_file "$cgdir/$f" "sys/fs/cgroup${group}/$f" - fi - done -} - -# v1_resolve_path computes the live filesystem path and the snapshot output -# path for a cgroup v1 controller, matching the Go code's v1SubsystemPath: -# live_path = mount_point + relpath(mount_root, cgroup_rel) -# out_path = strip leading "/" from live_path -# Prints "live_path out_path". -# -# Parsing is a PITA, but it seems there's no better way. -# See also https://lwn.net/Articles/934469/ -v1_resolve_path() { - local controller="$1" - local cgroup_rel mount_point mount_root info rel - - cgroup_rel=$(v1_cgroup_rel "$controller") - [[ -n "$cgroup_rel" ]] || return 0 - - info=$(v1_mount_info "$controller") - [[ -n "$info" ]] || return 0 - mount_point=${info%% *} - mount_root=${info#* } - - rel=$(realpath -m --relative-to="$mount_root" "$cgroup_rel") - local live="${mount_point}/${rel}" - echo "$live" "${live#/}" -} - -snapshot_v1() { - copy_file /proc/self/mountinfo proc/self/mountinfo +copy_file /proc/self/cgroup proc/self/cgroup - # Copy cpu subsystem files. - local cpu_paths - cpu_paths=$(v1_resolve_path cpu) - if [[ -n "$cpu_paths" ]]; then - local cpu_live=${cpu_paths%% *} cpu_out=${cpu_paths#* } - for f in cpu.cfs_quota_us cpu.cfs_period_us; do - if [[ -f "$cpu_live/$f" ]]; then - copy_file "$cpu_live/$f" "$cpu_out/$f" - fi - done - fi +group=$(awk -F: '/^0::/ { print $3 }' /proc/self/cgroup) +cgdir="/sys/fs/cgroup${group}" - # Copy memory subsystem files. - local mem_paths - mem_paths=$(v1_resolve_path memory) - if [[ -n "$mem_paths" ]]; then - local mem_live=${mem_paths%% *} mem_out=${mem_paths#* } - if [[ -f "$mem_live/memory.limit_in_bytes" ]]; then - copy_file "$mem_live/memory.limit_in_bytes" "$mem_out/memory.limit_in_bytes" - fi +for f in cpu.max memory.max; do + if [[ -f "$cgdir/$f" ]]; then + copy_file "$cgdir/$f" "sys/fs/cgroup${group}/$f" fi -} - -# Detect cgroup version. In hybrid setups (v1 controllers present alongside -# a v2 "0::" line), we treat the system as v1. -has_v1_controllers() { - awk -F: '$1 != "0" && $2 != "" { found=1 } END { exit !found }' /proc/self/cgroup -} - -copy_file /proc/self/cgroup proc/self/cgroup - -if has_v1_controllers; then - snapshot_v1 -else - snapshot_v2 -fi +done echo "Snapshot written to $out" diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go index 176eb29d..1c41d545 100644 --- a/cgroup/cgroup.go +++ b/cgroup/cgroup.go @@ -1,5 +1,5 @@ /* - * cgroup.go - Read CPU and memory limits from Linux cgroups (v1 and v2). + * cgroup.go - Read CPU and memory limits from Linux cgroups v2. * * Copyright 2026 Google LLC * @@ -17,17 +17,12 @@ */ // Package cgroup reads CPU and memory resource limits from Linux control -// groups. Both cgroup v1 and v2 are supported. +// groups (cgroup v2). // // References: // - cgroups(7): https://man7.org/linux/man-pages/man7/cgroups.7.html // - cgroup v2 (cpu.max, memory.max): https://docs.kernel.org/admin-guide/cgroup-v2.html -// - cgroup v1 CPU bandwidth (cpu.cfs_quota_us, cpu.cfs_period_us): -// https://docs.kernel.org/scheduler/sched-bwc.html -// - cgroup v1 memory (memory.limit_in_bytes): -// https://docs.kernel.org/admin-guide/cgroup-v1/memory.html // - /proc/self/cgroup: https://man7.org/linux/man-pages/man7/cgroups.7.html (see "/proc files") -// - /proc/self/mountinfo: https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html package cgroup import ( @@ -36,7 +31,6 @@ import ( "fmt" "os" "path/filepath" - "slices" "strconv" "strings" ) @@ -54,14 +48,11 @@ func CPUQuota() (float64, error) { // relative to root instead of "/". This is useful for testing with a // mock filesystem. func CPUQuotaWithRoot(root string) (float64, error) { - cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) + groupPath, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) if err != nil { return 0, err } - if cg.version == 2 { - return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", cg.v2GroupPath)) - } - return cpuQuotaV1(root, cg) + return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", groupPath)) } // MemoryLimit returns the cgroup memory limit in bytes. Returns ErrNoLimit @@ -74,84 +65,41 @@ func MemoryLimit() (int64, error) { // relative to root instead of "/". This is useful for testing with a // mock filesystem. func MemoryLimitWithRoot(root string) (int64, error) { - cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) + groupPath, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) if err != nil { return 0, err } - if cg.version == 2 { - return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", cg.v2GroupPath)) - } - return memoryLimitV1(root, cg) + return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", groupPath)) } -// procCgroup holds the parsed contents of /proc/self/cgroup. -// -// The file format is documented in cgroups(7). Each line has the format: +// parseProcCgroup parses /proc/self/cgroup and returns the cgroup v2 group +// path. The v2 entry is the line with hierarchy-ID "0" and an empty +// controller list: "0::". // -// hierarchy-ID:controller-list:cgroup-path -// -// For v2, there is a single line "0::". For v1, each line has a -// non-zero hierarchy ID and a comma-separated list of controllers. -type procCgroup struct { - // version is 1 or 2. - version int - // v2GroupPath is the cgroup path (only set when version is 2). - v2GroupPath string - // v1Subsystems maps controller names to their cgroup paths (only - // populated when version is 1). - v1Subsystems map[string]string -} - -// parseProcCgroup parses /proc/self/cgroup, extracting both v2 and v1 -// information. -// -// In hybrid setups (e.g. Ubuntu 20.04 with Docker) the file contains both -// v1 controller lines and a v2 "0::" line. Resource limits are set via the -// v1 controllers in that case, so we treat the system as v1 whenever v1 -// controllers are present. +// Returns an error if no v2 entry is found. // // https://man7.org/linux/man-pages/man7/cgroups.7.html -func parseProcCgroup(path string) (procCgroup, error) { +func parseProcCgroup(path string) (string, error) { f, err := os.Open(path) if err != nil { - return procCgroup{}, err + return "", err } defer f.Close() - var result procCgroup - result.v1Subsystems = make(map[string]string) - scanner := bufio.NewScanner(f) for scanner.Scan() { parts := strings.SplitN(scanner.Text(), ":", 3) if len(parts) != 3 { continue } - // v2 entry: hierarchy-ID is 0 and controllers field is empty. if parts[0] == "0" && parts[1] == "" { - result.v2GroupPath = parts[2] - continue - } - // v1 entry: map each controller to its cgroup path. - for _, ctrl := range strings.Split(parts[1], ",") { - if ctrl != "" { - result.v1Subsystems[ctrl] = parts[2] - } + return parts[2], nil } } if err := scanner.Err(); err != nil { - return procCgroup{}, err - } - - // Treat as v1 when any v1 controllers are present (covers hybrid). - if len(result.v1Subsystems) > 0 { - result.version = 1 - } else if result.v2GroupPath != "" { - result.version = 2 - } else { - result.version = 1 + return "", err } - return result, nil + return "", fmt.Errorf("no cgroup v2 entry found in %s", path) } // cpuQuotaV2 reads cpu.max from the given cgroup v2 directory. @@ -215,149 +163,3 @@ func parseMemoryMax(content string) (int64, error) { } return v, nil } - -// cpuQuotaV1 reads cpu.cfs_quota_us and cpu.cfs_period_us from the cpu -// cgroup v1 subsystem. -func cpuQuotaV1(root string, cg procCgroup) (float64, error) { - cgroupPath, err := v1SubsystemPath(root, "cpu", cg) - if err != nil { - return 0, err - } - quotaData, err := os.ReadFile(filepath.Join(cgroupPath, "cpu.cfs_quota_us")) - if err != nil { - return 0, err - } - quota, err := strconv.ParseInt(strings.TrimSpace(string(quotaData)), 10, 64) - if err != nil { - return 0, fmt.Errorf("parsing cpu.cfs_quota_us: %w", err) - } - if quota < 0 { - return 0, ErrNoLimit - } - periodData, err := os.ReadFile(filepath.Join(cgroupPath, "cpu.cfs_period_us")) - if err != nil { - return 0, err - } - period, err := strconv.ParseInt(strings.TrimSpace(string(periodData)), 10, 64) - if err != nil { - return 0, fmt.Errorf("parsing cpu.cfs_period_us: %w", err) - } - if period == 0 { - return 0, fmt.Errorf("cpu.cfs_period_us is zero") - } - return float64(quota) / float64(period), nil -} - -// memoryLimitV1 reads memory.limit_in_bytes from the memory cgroup v1 -// subsystem. -func memoryLimitV1(root string, cg procCgroup) (int64, error) { - cgroupPath, err := v1SubsystemPath(root, "memory", cg) - if err != nil { - return 0, err - } - data, err := os.ReadFile(filepath.Join(cgroupPath, "memory.limit_in_bytes")) - if err != nil { - return 0, err - } - v, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) - if err != nil { - return 0, fmt.Errorf("parsing memory.limit_in_bytes: %w", err) - } - // The kernel uses a very large value (close to max int64) to indicate - // "no limit". Treat anything above 1 EiB as unlimited. - const oneEiB = 1 << 60 - if v >= oneEiB { - return 0, ErrNoLimit - } - return v, nil -} - -// v1SubsystemPath finds the filesystem path for a cgroup v1 subsystem by -// looking up the already-parsed /proc/self/cgroup data and correlating it -// with /proc/self/mountinfo. All paths are resolved relative to root. -func v1SubsystemPath(root, subsystem string, cg procCgroup) (string, error) { - relPath, ok := cg.v1Subsystems[subsystem] - if !ok { - return "", fmt.Errorf("cgroup v1 subsystem %q not found", subsystem) - } - // mountinfo contains absolute paths (e.g. /sys/fs/cgroup/cpu). We - // prepend root so that file reads go to the right place. - procMountInfo := filepath.Join(root, "proc/self/mountinfo") - mountPoint, mountRoot, err := v1MountPoint(procMountInfo, subsystem) - if err != nil { - return "", err - } - rel, err := filepath.Rel(mountRoot, relPath) - if err != nil { - return "", err - } - return filepath.Join(root, mountPoint, rel), nil -} - -// v1MountPoint finds the mount point and root for a cgroup v1 subsystem -// from /proc/self/mountinfo. The returned paths are absolute (as written -// in mountinfo); the caller is responsible for prepending the root. -func v1MountPoint(procMountInfo, subsystem string) (mountPoint, root string, err error) { - f, err := os.Open(procMountInfo) - if err != nil { - return "", "", err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - mp, err := parseMountInfoLine(scanner.Text()) - if err != nil { - continue - } - if mp.fsType != "cgroup" { - continue - } - if slices.Contains(mp.superOptions, subsystem) { - return mp.mountPoint, mp.root, nil - } - } - if err := scanner.Err(); err != nil { - return "", "", err - } - return "", "", fmt.Errorf("cgroup v1 mount for %q not found in %s", subsystem, procMountInfo) -} - -type mountInfo struct { - root string - mountPoint string - fsType string - superOptions []string -} - -// parseMountInfoLine parses a single line from /proc/self/mountinfo. -// Format: id parent devid root mount opts [optional...] - fstype source superopts -// https://man7.org/linux/man-pages/man5/proc_pid_mountinfo.5.html -func parseMountInfoLine(line string) (mountInfo, error) { - fields := strings.Split(line, " ") - if len(fields) < 7 { - return mountInfo{}, fmt.Errorf("too few fields in mountinfo line") - } - - root := fields[3] - mp := fields[4] - - // Find the separator "-" that marks the end of optional fields. - sepIdx := -1 - for i := 6; i < len(fields); i++ { - if fields[i] == "-" { - sepIdx = i - break - } - } - if sepIdx == -1 || sepIdx+3 > len(fields) { - return mountInfo{}, fmt.Errorf("no separator in mountinfo line") - } - - return mountInfo{ - root: root, - mountPoint: mp, - fsType: fields[sepIdx+1], - superOptions: strings.Split(fields[sepIdx+3], ","), - }, nil -} diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index 374a4d42..669df9c5 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -102,109 +102,27 @@ func TestParseMemoryMax(t *testing.T) { } } -func TestParseMountInfoLine(t *testing.T) { - tests := []struct { - name string - line string - wantFSType string - wantRoot string - wantMount string - wantSuperOpt string - wantErr bool - }{ - { - name: "cgroup v1 cpu", - line: "35 26 0:30 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,cpu,cpuacct", - wantFSType: "cgroup", - wantRoot: "/", - wantMount: "/sys/fs/cgroup/cpu,cpuacct", - wantSuperOpt: "cpu", - }, - { - name: "cgroup v2", - line: "30 23 0:26 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - cgroup2 cgroup2 rw,nsdelegate,memory_recursiveprot", - wantFSType: "cgroup2", - wantRoot: "/", - wantMount: "/sys/fs/cgroup", - wantSuperOpt: "nsdelegate", - }, - { - name: "too short", - line: "a b c", - wantErr: true, - }, - { - name: "no separator", - line: "35 26 0:30 / /mnt rw,relatime shared:1 cgroup cgroup rw", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseMountInfoLine(tt.line) - if tt.wantErr { - if err == nil { - t.Fatal("expected error, got nil") - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got.fsType != tt.wantFSType { - t.Errorf("fsType = %q, want %q", got.fsType, tt.wantFSType) - } - if got.root != tt.wantRoot { - t.Errorf("root = %q, want %q", got.root, tt.wantRoot) - } - if got.mountPoint != tt.wantMount { - t.Errorf("mountPoint = %q, want %q", got.mountPoint, tt.wantMount) - } - found := false - for _, opt := range got.superOptions { - if opt == tt.wantSuperOpt { - found = true - break - } - } - if !found { - t.Errorf("superOptions = %v, want to contain %q", got.superOptions, tt.wantSuperOpt) - } - }) - } -} - func TestParseProcCgroup(t *testing.T) { tests := []struct { - name string - content string - wantVersion int - wantGroup string - wantSubsystems map[string]string + name string + content string + wantGroup string + wantErr string }{ { - name: "cgroup v2", - content: "0::/user.slice/user-1000.slice/session-1.scope\n", - wantVersion: 2, - wantGroup: "/user.slice/user-1000.slice/session-1.scope", + name: "cgroup v2", + content: "0::/user.slice/user-1000.slice/session-1.scope\n", + wantGroup: "/user.slice/user-1000.slice/session-1.scope", }, { - name: "cgroup v1 only", - content: "12:memory:/docker/abc123\n11:cpu,cpuacct:/docker/abc123\n", - wantVersion: 1, - wantSubsystems: map[string]string{ - "memory": "/docker/abc123", - "cpu": "/docker/abc123", - "cpuacct": "/docker/abc123", - }, + name: "v1 only (no v2 entry)", + content: "12:memory:/docker/abc123\n11:cpu,cpuacct:/docker/abc123\n", + wantErr: "no cgroup v2 entry", }, { - name: "hybrid v1 and v2", - content: "12:memory:/docker/abc123\n0::/docker/abc123\n", - wantVersion: 1, - wantSubsystems: map[string]string{ - "memory": "/docker/abc123", - }, + name: "hybrid v1 and v2", + content: "12:memory:/docker/abc123\n0::/docker/abc123\n", + wantGroup: "/docker/abc123", }, } for _, tt := range tests { @@ -212,23 +130,18 @@ func TestParseProcCgroup(t *testing.T) { f := filepath.Join(t.TempDir(), "cgroup") writeFile(t, f, tt.content) - cg, err := parseProcCgroup(f) + groupPath, err := parseProcCgroup(f) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("parseProcCgroup() error = %v, want error containing %q", err, tt.wantErr) + } + return + } if err != nil { t.Fatalf("unexpected error: %v", err) } - if cg.version != tt.wantVersion { - t.Errorf("version = %d, want %d", cg.version, tt.wantVersion) - } - if tt.wantVersion == 2 && cg.v2GroupPath != tt.wantGroup { - t.Errorf("v2GroupPath = %q, want %q", cg.v2GroupPath, tt.wantGroup) - } - for k, want := range tt.wantSubsystems { - got, ok := cg.v1Subsystems[k] - if !ok { - t.Errorf("v1Subsystems missing key %q", k) - } else if got != want { - t.Errorf("v1Subsystems[%q] = %q, want %q", k, got, want) - } + if groupPath != tt.wantGroup { + t.Errorf("parseProcCgroup() = %q, want %q", groupPath, tt.wantGroup) } }) } @@ -313,115 +226,7 @@ func TestMemoryLimitV2MissingFile(t *testing.T) { } } -// setupV1Root creates a mock cgroup v1 filesystem under a temp directory. -// The mountinfo file contains absolute paths (as the kernel writes them), -// and the code prepends root when reading. -func setupV1Root(t *testing.T, subsystem, cgroupRelPath string, files map[string]string) string { - t.Helper() - root := t.TempDir() - - // /proc/self/cgroup - var cgroupLine string - switch subsystem { - case "cpu": - cgroupLine = "11:cpu,cpuacct:" + cgroupRelPath + "\n" - case "memory": - cgroupLine = "12:memory:" + cgroupRelPath + "\n" - } - writeFile(t, filepath.Join(root, "proc/self/cgroup"), cgroupLine) - - // /proc/self/mountinfo with absolute mount point - mountPoint := "/sys/fs/cgroup/" + subsystem - mountInfoLine := "35 26 0:30 / " + mountPoint + " rw,nosuid - cgroup cgroup rw," + subsystem + "\n" - writeFile(t, filepath.Join(root, "proc/self/mountinfo"), mountInfoLine) - - // cgroup control files under root + mountPoint + cgroupRelPath - cgroupDir := filepath.Join(root, mountPoint, cgroupRelPath) - for name, content := range files { - writeFile(t, filepath.Join(cgroupDir, name), content) - } - - return root -} - -func TestCPUQuotaV1(t *testing.T) { - tests := []struct { - name string - quota string - period string - want float64 - wantErr string - }{ - {"half core", "50000\n", "100000\n", 0.5, ""}, - {"two cores", "200000\n", "100000\n", 2.0, ""}, - {"unlimited", "-1\n", "100000\n", 0, "no cgroup limit set"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - root := setupV1Root(t, "cpu", "/docker/abc123", map[string]string{ - "cpu.cfs_quota_us": tt.quota, - "cpu.cfs_period_us": tt.period, - }) - cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) - if err != nil { - t.Fatal(err) - } - - got, err := cpuQuotaV1(root, cg) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if math.Abs(got-tt.want) > 0.001 { - t.Errorf("cpuQuotaV1() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMemoryLimitV1(t *testing.T) { - tests := []struct { - name string - limit string - want int64 - wantErr string - }{ - {"128 MiB", "134217728\n", 134217728, ""}, - {"unlimited (large value)", "9223372036854771712\n", 0, "no cgroup limit set"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - root := setupV1Root(t, "memory", "/docker/abc123", map[string]string{ - "memory.limit_in_bytes": tt.limit, - }) - cg, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) - if err != nil { - t.Fatal(err) - } - - got, err := memoryLimitV1(root, cg) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != tt.want { - t.Errorf("memoryLimitV1() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestCPUQuotaWithRootV2(t *testing.T) { +func TestCPUQuotaWithRoot(t *testing.T) { root := t.TempDir() writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") @@ -436,7 +241,7 @@ func TestCPUQuotaWithRootV2(t *testing.T) { } } -func TestMemoryLimitWithRootV2(t *testing.T) { +func TestMemoryLimitWithRoot(t *testing.T) { root := t.TempDir() writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") @@ -451,35 +256,6 @@ func TestMemoryLimitWithRootV2(t *testing.T) { } } -func TestCPUQuotaWithRootV1(t *testing.T) { - root := setupV1Root(t, "cpu", "/docker/abc123", map[string]string{ - "cpu.cfs_quota_us": "150000\n", - "cpu.cfs_period_us": "100000\n", - }) - - got, err := CPUQuotaWithRoot(root) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if math.Abs(got-1.5) > 0.001 { - t.Errorf("CPUQuotaWithRoot() = %v, want 1.5", got) - } -} - -func TestMemoryLimitWithRootV1(t *testing.T) { - root := setupV1Root(t, "memory", "/docker/abc123", map[string]string{ - "memory.limit_in_bytes": "268435456\n", - }) - - got, err := MemoryLimitWithRoot(root) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != 268435456 { - t.Errorf("MemoryLimitWithRoot() = %v, want 268435456", got) - } -} - // testdataExpected holds the expected values from a testdata/*/expected.json. // Null fields indicate that ErrNoLimit is expected. type testdataExpected struct { @@ -488,11 +264,11 @@ type testdataExpected struct { } // TestWithRootFromTestdata runs CPUQuotaWithRoot and MemoryLimitWithRoot -// against filesystem snapshots captured from real Docker containers (or VMs) -// by bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test +// against filesystem snapshots captured from real Docker containers by +// bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test // case containing a proc/ and sys/ tree plus an expected.json. // -// Regenerate with: bin/gen-cgroupv1-testdata and bin/gen-cgroupv2-testdata +// Regenerate with: bin/gen-cgroup-testdata func TestWithRootFromTestdata(t *testing.T) { entries, err := os.ReadDir("testdata") if err != nil { diff --git a/cgroup/testdata/cgroupv1.yaml b/cgroup/testdata/cgroupv1.yaml deleted file mode 100644 index a04ae313..00000000 --- a/cgroup/testdata/cgroupv1.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# Minimal Lima VM running Ubuntu 20.04 (Focal) for cgroup v1 testdata generation. -# -# Ubuntu 20.04 uses cgroup v1 by default, so no kernel parameter changes are -# needed. Docker is installed via a provisioning script so that containers can -# be launched with --cpus / --memory resource limits. -# -# Usage: -# limactl start --name=cgroupv1 cgroup/testdata/cgroupv1.yaml -# bin/gen-cgroupv1-testdata -# limactl stop cgroupv1 # when done - -images: - - location: "https://cloud-images.ubuntu.com/releases/20.04/release/ubuntu-20.04-server-cloudimg-amd64.img" - arch: "x86_64" - - location: "https://cloud-images.ubuntu.com/releases/20.04/release/ubuntu-20.04-server-cloudimg-arm64.img" - arch: "aarch64" - -cpus: 2 -memory: "2GiB" -disk: "10GiB" - -mounts: - - location: "~" - writable: true - -containerd: - system: false - user: false - -provision: - - mode: system - script: | - #!/bin/bash - set -eux -o pipefail - if command -v docker &>/dev/null; then - exit 0 - fi - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq - apt-get install -y -qq ca-certificates curl gnupg lsb-release >/dev/null - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ - gpg --dearmor -o /etc/apt/keyrings/docker.gpg - chmod a+r /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ - https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ - > /etc/apt/sources.list.d/docker.list - apt-get update -qq - apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null - usermod -aG docker "{{.User}}" - -probes: - - mode: readiness - description: docker to be ready - script: | - #!/bin/bash - set -eux -o pipefail - if ! timeout 60s bash -c "until sudo docker info &>/dev/null; do sleep 3; done"; then - echo >&2 "docker is not ready yet" - exit 1 - fi diff --git a/cgroup/testdata/v1-half-core-128m/expected.json b/cgroup/testdata/v1-half-core-128m/expected.json deleted file mode 100644 index 8d190583..00000000 --- a/cgroup/testdata/v1-half-core-128m/expected.json +++ /dev/null @@ -1 +0,0 @@ -{"cpu_quota": 0.5, "memory_limit": 134217728} diff --git a/cgroup/testdata/v1-half-core-128m/proc/self/cgroup b/cgroup/testdata/v1-half-core-128m/proc/self/cgroup deleted file mode 100644 index 4e3f5b44..00000000 --- a/cgroup/testdata/v1-half-core-128m/proc/self/cgroup +++ /dev/null @@ -1,13 +0,0 @@ -12:pids:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -11:cpuset:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -10:freezer:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -9:cpu,cpuacct:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -8:perf_event:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -7:blkio:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -6:hugetlb:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -5:devices:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -4:memory:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -3:net_cls,net_prio:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -2:rdma:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -1:name=systemd:/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 -0::/docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 diff --git a/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo b/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo deleted file mode 100644 index fd3ad158..00000000 --- a/cgroup/testdata/v1-half-core-128m/proc/self/mountinfo +++ /dev/null @@ -1,38 +0,0 @@ -871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/3QUPKHUTWAY2BZUBSN3T2R2PWF:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/7ca228548702b25c7e85de3ef3d26f9926cfc4e4744cbcf499cf9f2873298bc1/diff,workdir=/var/lib/docker/overlay2/7ca228548702b25c7e85de3ef3d26f9926cfc4e4744cbcf499cf9f2873298bc1/work,xino=off -873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw -874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 -876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro -877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -878 877 0:31 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd -879 877 0:35 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma -880 877 0:36 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio -881 877 0:37 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory -882 877 0:38 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices -883 877 0:39 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb -884 877 0:40 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio -885 877 0:41 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event -886 877 0:42 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct -887 877 0:43 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer -888 877 0:44 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset -889 877 0:45 /docker/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids -890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw -891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k -892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-half-core-128m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -894 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw -895 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw -896 871 252:1 /var/lib/docker/containers/bb732e6dc831451c5260af16754ccdf02534f09ddf999bcf6e9425e642fe71f1/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw -558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw -559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw -560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw -566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw -567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw -568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro -569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro -575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us deleted file mode 100644 index f7393e84..00000000 --- a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us +++ /dev/null @@ -1 +0,0 @@ -100000 diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us deleted file mode 100644 index ccfc37a1..00000000 --- a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us +++ /dev/null @@ -1 +0,0 @@ -50000 diff --git a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes deleted file mode 100644 index 7a758363..00000000 --- a/cgroup/testdata/v1-half-core-128m/sys/fs/cgroup/memory/memory.limit_in_bytes +++ /dev/null @@ -1 +0,0 @@ -134217728 diff --git a/cgroup/testdata/v1-no-limit/expected.json b/cgroup/testdata/v1-no-limit/expected.json deleted file mode 100644 index 3a6d7edc..00000000 --- a/cgroup/testdata/v1-no-limit/expected.json +++ /dev/null @@ -1 +0,0 @@ -{"cpu_quota": null, "memory_limit": null} diff --git a/cgroup/testdata/v1-no-limit/proc/self/cgroup b/cgroup/testdata/v1-no-limit/proc/self/cgroup deleted file mode 100644 index ccf55785..00000000 --- a/cgroup/testdata/v1-no-limit/proc/self/cgroup +++ /dev/null @@ -1,13 +0,0 @@ -12:pids:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -11:cpuset:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -10:freezer:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -9:cpu,cpuacct:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -8:perf_event:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -7:blkio:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -6:hugetlb:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -5:devices:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -4:memory:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -3:net_cls,net_prio:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -2:rdma:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -1:name=systemd:/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 -0::/docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 diff --git a/cgroup/testdata/v1-no-limit/proc/self/mountinfo b/cgroup/testdata/v1-no-limit/proc/self/mountinfo deleted file mode 100644 index 2a3acfd4..00000000 --- a/cgroup/testdata/v1-no-limit/proc/self/mountinfo +++ /dev/null @@ -1,38 +0,0 @@ -871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/J2XQFXRCNRAXPQQY6DIPC5SVWZ:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/2cfe4ceb28a58c6866b45004ab74875b38ccc7b1396cddd4bf9da9908679bbb9/diff,workdir=/var/lib/docker/overlay2/2cfe4ceb28a58c6866b45004ab74875b38ccc7b1396cddd4bf9da9908679bbb9/work,xino=off -873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw -874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 -876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro -877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -878 877 0:31 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd -879 877 0:35 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma -880 877 0:36 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio -881 877 0:37 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory -882 877 0:38 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices -883 877 0:39 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb -884 877 0:40 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio -885 877 0:41 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event -886 877 0:42 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct -887 877 0:43 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer -888 877 0:44 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset -889 877 0:45 /docker/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids -890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw -891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k -892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-no-limit /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -894 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw -895 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw -896 871 252:1 /var/lib/docker/containers/bfe0e389d60119b93049080aab8b7998a8bdd662ce42ae4b83176cc33b275231/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw -558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw -559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw -560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw -566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw -567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw -568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro -569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro -575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us deleted file mode 100644 index f7393e84..00000000 --- a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us +++ /dev/null @@ -1 +0,0 @@ -100000 diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us deleted file mode 100644 index 3a2e3f49..00000000 --- a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us +++ /dev/null @@ -1 +0,0 @@ --1 diff --git a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes deleted file mode 100644 index 564113cf..00000000 --- a/cgroup/testdata/v1-no-limit/sys/fs/cgroup/memory/memory.limit_in_bytes +++ /dev/null @@ -1 +0,0 @@ -9223372036854771712 diff --git a/cgroup/testdata/v1-quarter-core-64m/expected.json b/cgroup/testdata/v1-quarter-core-64m/expected.json deleted file mode 100644 index 41ec96fa..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/expected.json +++ /dev/null @@ -1 +0,0 @@ -{"cpu_quota": 0.25, "memory_limit": 67108864} diff --git a/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup b/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup deleted file mode 100644 index 0f95e57c..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/proc/self/cgroup +++ /dev/null @@ -1,13 +0,0 @@ -12:pids:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -11:cpuset:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -10:freezer:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -9:cpu,cpuacct:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -8:perf_event:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -7:blkio:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -6:hugetlb:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -5:devices:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -4:memory:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -3:net_cls,net_prio:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -2:rdma:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -1:name=systemd:/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 -0::/docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 diff --git a/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo b/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo deleted file mode 100644 index cf35c163..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/proc/self/mountinfo +++ /dev/null @@ -1,38 +0,0 @@ -871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/3H65BZBNFDRRM7Y44R52363AQT:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/632fa95736287ba3cb0c70e5b398faf66993b843a2fee9135939efb82cbb1ebd/diff,workdir=/var/lib/docker/overlay2/632fa95736287ba3cb0c70e5b398faf66993b843a2fee9135939efb82cbb1ebd/work,xino=off -873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw -874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 -876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro -877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -878 877 0:31 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd -879 877 0:35 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma -880 877 0:36 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio -881 877 0:37 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory -882 877 0:38 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices -883 877 0:39 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb -884 877 0:40 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio -885 877 0:41 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event -886 877 0:42 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct -887 877 0:43 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer -888 877 0:44 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset -889 877 0:45 /docker/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids -890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw -891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k -892 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-quarter-core-64m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -893 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -894 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw -895 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw -896 871 252:1 /var/lib/docker/containers/589a71c907d96210ca5cb2f5ea6104b12fbf96ee1a6f5169f20991de2c1ae8d6/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw -558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw -559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw -560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw -566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw -567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw -568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro -569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro -575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us deleted file mode 100644 index f7393e84..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us +++ /dev/null @@ -1 +0,0 @@ -100000 diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us deleted file mode 100644 index e87f3b8e..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us +++ /dev/null @@ -1 +0,0 @@ -25000 diff --git a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes deleted file mode 100644 index e6c68622..00000000 --- a/cgroup/testdata/v1-quarter-core-64m/sys/fs/cgroup/memory/memory.limit_in_bytes +++ /dev/null @@ -1 +0,0 @@ -67108864 diff --git a/cgroup/testdata/v1-two-cores-256m/expected.json b/cgroup/testdata/v1-two-cores-256m/expected.json deleted file mode 100644 index 04ce067c..00000000 --- a/cgroup/testdata/v1-two-cores-256m/expected.json +++ /dev/null @@ -1 +0,0 @@ -{"cpu_quota": 2.0, "memory_limit": 268435456} diff --git a/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup b/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup deleted file mode 100644 index fb4461ee..00000000 --- a/cgroup/testdata/v1-two-cores-256m/proc/self/cgroup +++ /dev/null @@ -1,13 +0,0 @@ -12:pids:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -11:cpuset:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -10:freezer:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -9:cpu,cpuacct:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -8:perf_event:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -7:blkio:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -6:hugetlb:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -5:devices:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -4:memory:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -3:net_cls,net_prio:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -2:rdma:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -1:name=systemd:/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 -0::/docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 diff --git a/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo b/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo deleted file mode 100644 index 2b1a8702..00000000 --- a/cgroup/testdata/v1-two-cores-256m/proc/self/mountinfo +++ /dev/null @@ -1,38 +0,0 @@ -871 557 0:56 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/RXYCY5MVMMC6NWBBRBFODFMAS5:/var/lib/docker/overlay2/l/ASBERMFDXJ34LN76VHBH2V73S3,upperdir=/var/lib/docker/overlay2/c54155e4d9fedcbaa545064f3b5a78d68c760132cd9ea23152cf62ff485f9756/diff,workdir=/var/lib/docker/overlay2/c54155e4d9fedcbaa545064f3b5a78d68c760132cd9ea23152cf62ff485f9756/work,xino=off -873 871 0:61 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw -874 871 0:62 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -875 874 0:63 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 -876 871 0:64 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro -877 876 0:65 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755 -878 877 0:31 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,xattr,name=systemd -879 877 0:35 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,rdma -880 877 0:36 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/net_cls,net_prio ro,nosuid,nodev,noexec,relatime master:17 - cgroup cgroup rw,net_cls,net_prio -881 877 0:37 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:18 - cgroup cgroup rw,memory -882 877 0:38 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:19 - cgroup cgroup rw,devices -883 877 0:39 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:20 - cgroup cgroup rw,hugetlb -884 877 0:40 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:21 - cgroup cgroup rw,blkio -885 877 0:41 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:22 - cgroup cgroup rw,perf_event -886 877 0:42 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,cpu,cpuacct -887 877 0:43 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,freezer -888 877 0:44 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset -889 877 0:45 /docker/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45 /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,pids -890 874 0:60 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw -891 874 0:66 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k -892 871 0:50 /git/third_party/fscrypt-fork/bin/snapshot-cgroup /snapshot ro,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -893 871 0:50 /git/third_party/fscrypt-fork/cgroup/testdata/v1-two-cores-256m /out rw,relatime - 9p mount0 rw,dirsync,mmap,access=client,msize=131072,trans=virtio -894 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw -895 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw -896 871 252:1 /var/lib/docker/containers/efd53cc1cd155e9b1236a430f0fafb41b00e220b42ce8b6ee5d50bfe2a165d45/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw -558 873 0:61 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw -559 873 0:61 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw -560 873 0:61 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw -566 873 0:61 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw -567 873 0:61 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw -568 873 0:67 / /proc/acpi ro,relatime - tmpfs tmpfs ro -569 873 0:62 /null /proc/interrupts rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -570 873 0:62 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -571 873 0:62 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -572 873 0:62 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -573 873 0:62 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755 -574 873 0:68 / /proc/scsi ro,relatime - tmpfs tmpfs ro -575 876 0:69 / /sys/firmware ro,relatime - tmpfs tmpfs ro diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us deleted file mode 100644 index f7393e84..00000000 --- a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us +++ /dev/null @@ -1 +0,0 @@ -100000 diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us deleted file mode 100644 index 87766d88..00000000 --- a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us +++ /dev/null @@ -1 +0,0 @@ -200000 diff --git a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes b/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes deleted file mode 100644 index 853f47e7..00000000 --- a/cgroup/testdata/v1-two-cores-256m/sys/fs/cgroup/memory/memory.limit_in_bytes +++ /dev/null @@ -1 +0,0 @@ -268435456 From 1770e5688e88b2d0967b8b4833903d14b40fe69f Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:27:57 +0100 Subject: [PATCH 8/9] Move to cgroup struct --- actions/config.go | 12 +- cgroup/cgroup.go | 106 +++++++++-------- cgroup/cgroup_test.go | 260 ++++++------------------------------------ 3 files changed, 99 insertions(+), 279 deletions(-) diff --git a/actions/config.go b/actions/config.go index f2abf959..bd4ae284 100644 --- a/actions/config.go +++ b/actions/config.go @@ -255,7 +255,11 @@ func getHashingCosts(target time.Duration) (*metadata.HashingCosts, error) { // taking cgroup limits into account. Falls back to runtime.NumCPU() when // cgroup information is unavailable. func effectiveCPUCount() int { - quota, err := cgroup.CPUQuota() + cg, err := cgroup.New() + if err != nil { + return runtime.NumCPU() + } + quota, err := cg.CPUQuota() if err != nil || quota <= 0 { return runtime.NumCPU() } @@ -273,8 +277,10 @@ func memoryBytesLimit() int64 { util.NeverError(err) totalRAMBytes := int64(info.Totalram) - if cgroupMem, err := cgroup.MemoryLimit(); err == nil && cgroupMem > 0 { - totalRAMBytes = util.MinInt64(totalRAMBytes, cgroupMem) + if cg, err := cgroup.New(); err == nil { + if cgroupMem, err := cg.MemoryLimit(); err == nil && cgroupMem > 0 { + totalRAMBytes = util.MinInt64(totalRAMBytes, cgroupMem) + } } return util.MinInt64(totalRAMBytes/8, maxMemoryBytes) } diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go index 1c41d545..dff81386 100644 --- a/cgroup/cgroup.go +++ b/cgroup/cgroup.go @@ -35,48 +35,77 @@ import ( "strings" ) -// ErrNoLimit indicates that no cgroup limit is set. -var ErrNoLimit = errors.New("no cgroup limit set") +// Errors. +var ( + // ErrNoLimit indicates that no cgroup limit is set. + ErrNoLimit = errors.New("no cgroup limit set") -// CPUQuota returns the CPU quota as a fractional number of CPUs (e.g. 0.5 -// means half a core). Returns ErrNoLimit if no CPU limit is configured. -func CPUQuota() (float64, error) { - return CPUQuotaWithRoot("/") + // ErrV1Detected indicates that cgroup v1 controllers were found. Only v2 is + // supported. + ErrV1Detected = errors.New("cgroup v1 detected; only v2 is supported") +) + +// Cgroup provides access to cgroup v2 resource limits. Create one with +// New or NewFromRoot. +type Cgroup struct { + // cgroupDir is the resolved filesystem path to the cgroup directory + // (e.g. /sys/fs/cgroup/user.slice/...). + cgroupDir string } -// CPUQuotaWithRoot is like CPUQuota but resolves all filesystem paths -// relative to root instead of "/". This is useful for testing with a -// mock filesystem. -func CPUQuotaWithRoot(root string) (float64, error) { +// New returns a Cgroup by reading /proc/self/cgroup on the live system. +func New() (Cgroup, error) { + return NewFromRoot("/") +} + +// NewFromRoot is like New but resolves all filesystem paths relative to +// root instead of "/". This is useful for testing with a mock filesystem. +func NewFromRoot(root string) (Cgroup, error) { groupPath, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) + if err != nil { + return Cgroup{}, err + } + return Cgroup{ + cgroupDir: filepath.Join(root, "sys/fs/cgroup", groupPath), + }, nil +} + +// CPUQuota returns the CPU quota as a fractional number of CPUs (e.g. 0.5 +// means half a core). Returns ErrNoLimit if no CPU limit is configured. +func (c Cgroup) CPUQuota() (float64, error) { + data, err := c.readFile("cpu.max") if err != nil { return 0, err } - return cpuQuotaV2(filepath.Join(root, "sys/fs/cgroup", groupPath)) + return parseCPUMax(data) } // MemoryLimit returns the cgroup memory limit in bytes. Returns ErrNoLimit // if no memory limit is configured. -func MemoryLimit() (int64, error) { - return MemoryLimitWithRoot("/") +func (c Cgroup) MemoryLimit() (int64, error) { + data, err := c.readFile("memory.max") + if err != nil { + return 0, err + } + return parseMemoryMax(data) } -// MemoryLimitWithRoot is like MemoryLimit but resolves all filesystem paths -// relative to root instead of "/". This is useful for testing with a -// mock filesystem. -func MemoryLimitWithRoot(root string) (int64, error) { - groupPath, err := parseProcCgroup(filepath.Join(root, "proc/self/cgroup")) +func (c Cgroup) readFile(path string) (string, error) { + data, err := os.ReadFile(filepath.Join(c.cgroupDir, path)) if err != nil { - return 0, err + if os.IsNotExist(err) { + return "", ErrNoLimit + } + return "", err } - return memoryLimitV2(filepath.Join(root, "sys/fs/cgroup", groupPath)) + return strings.TrimSpace(string(data)), nil } // parseProcCgroup parses /proc/self/cgroup and returns the cgroup v2 group // path. The v2 entry is the line with hierarchy-ID "0" and an empty // controller list: "0::". // -// Returns an error if no v2 entry is found. +// Returns an error if v1 controllers are detected or no v2 entry is found. // // https://man7.org/linux/man-pages/man7/cgroups.7.html func parseProcCgroup(path string) (string, error) { @@ -86,6 +115,8 @@ func parseProcCgroup(path string) (string, error) { } defer f.Close() + var v2Path string + scanner := bufio.NewScanner(f) for scanner.Scan() { parts := strings.SplitN(scanner.Text(), ":", 3) @@ -93,27 +124,18 @@ func parseProcCgroup(path string) (string, error) { continue } if parts[0] == "0" && parts[1] == "" { - return parts[2], nil + v2Path = parts[2] + } else if parts[1] != "" { + return "", ErrV1Detected } } if err := scanner.Err(); err != nil { return "", err } - return "", fmt.Errorf("no cgroup v2 entry found in %s", path) -} - -// cpuQuotaV2 reads cpu.max from the given cgroup v2 directory. -// Format: "$MAX $PERIOD" or "max $PERIOD". -// https://docs.kernel.org/admin-guide/cgroup-v2.html -func cpuQuotaV2(cgroupDir string) (float64, error) { - data, err := os.ReadFile(filepath.Join(cgroupDir, "cpu.max")) - if err != nil { - if os.IsNotExist(err) { - return 0, ErrNoLimit - } - return 0, err + if v2Path == "" { + return "", fmt.Errorf("no cgroup v2 entry found in %s", path) } - return parseCPUMax(strings.TrimSpace(string(data))) + return v2Path, nil } func parseCPUMax(content string) (float64, error) { @@ -141,18 +163,6 @@ func parseCPUMax(content string) (float64, error) { return quota / period, nil } -// memoryLimitV2 reads memory.max from the given cgroup v2 directory. -func memoryLimitV2(cgroupDir string) (int64, error) { - data, err := os.ReadFile(filepath.Join(cgroupDir, "memory.max")) - if err != nil { - if os.IsNotExist(err) { - return 0, ErrNoLimit - } - return 0, err - } - return parseMemoryMax(strings.TrimSpace(string(data))) -} - func parseMemoryMax(content string) (int64, error) { if content == "max" { return 0, ErrNoLimit diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index 669df9c5..f1bc8f9b 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -25,7 +25,6 @@ import ( "os" "path/filepath" "strconv" - "strings" "testing" ) @@ -39,220 +38,15 @@ func writeFile(t *testing.T, path, content string) { } } -func TestParseCPUMax(t *testing.T) { - tests := []struct { - name string - content string - want float64 - wantErr string - }{ - {"half core", "50000 100000", 0.5, ""}, - {"two cores", "200000 100000", 2.0, ""}, - {"quota only with default period", "50000", 0.5, ""}, - {"unlimited", "max 100000", 0, "no cgroup limit set"}, - {"empty", "", 0, "unexpected cpu.max format"}, - {"zero period", "50000 0", 0, "period is zero"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseCPUMax(tt.content) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("parseCPUMax(%q) error = %v, want error containing %q", tt.content, err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("parseCPUMax(%q) unexpected error: %v", tt.content, err) - } - if math.Abs(got-tt.want) > 0.001 { - t.Errorf("parseCPUMax(%q) = %v, want %v", tt.content, got, tt.want) - } - }) - } -} - -func TestParseMemoryMax(t *testing.T) { - tests := []struct { - name string - content string - want int64 - wantErr string - }{ - {"128 MiB", "134217728", 134217728, ""}, - {"unlimited", "max", 0, "no cgroup limit set"}, - {"invalid", "abc", 0, "parsing memory.max"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseMemoryMax(tt.content) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("parseMemoryMax(%q) error = %v, want error containing %q", tt.content, err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("parseMemoryMax(%q) unexpected error: %v", tt.content, err) - } - if got != tt.want { - t.Errorf("parseMemoryMax(%q) = %v, want %v", tt.content, got, tt.want) - } - }) - } -} - -func TestParseProcCgroup(t *testing.T) { - tests := []struct { - name string - content string - wantGroup string - wantErr string - }{ - { - name: "cgroup v2", - content: "0::/user.slice/user-1000.slice/session-1.scope\n", - wantGroup: "/user.slice/user-1000.slice/session-1.scope", - }, - { - name: "v1 only (no v2 entry)", - content: "12:memory:/docker/abc123\n11:cpu,cpuacct:/docker/abc123\n", - wantErr: "no cgroup v2 entry", - }, - { - name: "hybrid v1 and v2", - content: "12:memory:/docker/abc123\n0::/docker/abc123\n", - wantGroup: "/docker/abc123", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := filepath.Join(t.TempDir(), "cgroup") - writeFile(t, f, tt.content) - - groupPath, err := parseProcCgroup(f) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("parseProcCgroup() error = %v, want error containing %q", err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if groupPath != tt.wantGroup { - t.Errorf("parseProcCgroup() = %q, want %q", groupPath, tt.wantGroup) - } - }) - } -} - -func TestCPUQuotaV2(t *testing.T) { - tests := []struct { - name string - cpuMax string - want float64 - wantErr string - }{ - {"half core", "50000 100000\n", 0.5, ""}, - {"four cores", "400000 100000\n", 4.0, ""}, - {"unlimited", "max 100000\n", 0, "no cgroup limit set"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "cpu.max"), tt.cpuMax) - - got, err := cpuQuotaV2(dir) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if math.Abs(got-tt.want) > 0.001 { - t.Errorf("cpuQuotaV2() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestCPUQuotaV2MissingFile(t *testing.T) { - _, err := cpuQuotaV2(t.TempDir()) - if !errors.Is(err, ErrNoLimit) { - t.Errorf("error = %v, want ErrNoLimit", err) - } -} - -func TestMemoryLimitV2(t *testing.T) { - tests := []struct { - name string - memoryMax string - want int64 - wantErr string - }{ - {"128 MiB", "134217728\n", 134217728, ""}, - {"unlimited", "max\n", 0, "no cgroup limit set"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dir := t.TempDir() - writeFile(t, filepath.Join(dir, "memory.max"), tt.memoryMax) - - got, err := memoryLimitV2(dir) - if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %v, want error containing %q", err, tt.wantErr) - } - return - } - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != tt.want { - t.Errorf("memoryLimitV2() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMemoryLimitV2MissingFile(t *testing.T) { - _, err := memoryLimitV2(t.TempDir()) - if !errors.Is(err, ErrNoLimit) { - t.Errorf("error = %v, want ErrNoLimit", err) - } -} - -func TestCPUQuotaWithRoot(t *testing.T) { +func TestCgroupV1Unsupported(t *testing.T) { + content := `12:memory:/docker/abc123 +11:cpu,cpuacct:/docker/abc123 +` root := t.TempDir() - - writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") - writeFile(t, filepath.Join(root, "sys/fs/cgroup/kubepods/pod123/cpu.max"), "50000 100000\n") - - got, err := CPUQuotaWithRoot(root) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if math.Abs(got-0.5) > 0.001 { - t.Errorf("CPUQuotaWithRoot() = %v, want 0.5", got) - } -} - -func TestMemoryLimitWithRoot(t *testing.T) { - root := t.TempDir() - - writeFile(t, filepath.Join(root, "proc/self/cgroup"), "0::/kubepods/pod123\n") - writeFile(t, filepath.Join(root, "sys/fs/cgroup/kubepods/pod123/memory.max"), "134217728\n") - - got, err := MemoryLimitWithRoot(root) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != 134217728 { - t.Errorf("MemoryLimitWithRoot() = %v, want 134217728", got) + writeFile(t, filepath.Join(root, "proc/self/cgroup"), content) + _, err := NewFromRoot(root) + if !errors.Is(err, ErrV1Detected) { + t.Fatalf("NewFromRoot() error = %v, want %v", err, ErrV1Detected) } } @@ -263,7 +57,7 @@ type testdataExpected struct { MemoryLimit *int64 `json:"memory_limit"` } -// TestWithRootFromTestdata runs CPUQuotaWithRoot and MemoryLimitWithRoot +// TestWithRootFromTestdata runs NewFromRoot, CPUQuota, and MemoryLimit // against filesystem snapshots captured from real Docker containers by // bin/snapshot-cgroup. Each subdirectory of testdata/ is a separate test // case containing a proc/ and sys/ tree plus an expected.json. @@ -292,34 +86,39 @@ func TestWithRootFromTestdata(t *testing.T) { t.Fatalf("parsing expected.json: %v", err) } - gotCPU, err := CPUQuotaWithRoot(root) + cg, err := NewFromRoot(root) + if err != nil { + t.Fatalf("NewFromRoot(%q): %v", root, err) + } + + gotCPU, err := cg.CPUQuota() if want.CPUQuota == nil { if !errors.Is(err, ErrNoLimit) { - t.Errorf("CPUQuotaWithRoot() error = %v, want ErrNoLimit", err) + t.Errorf("CPUQuota() error = %v, want ErrNoLimit", err) } } else if err != nil { - t.Fatalf("CPUQuotaWithRoot(%q): %v", root, err) + t.Fatalf("CPUQuota(): %v", err) } else if math.Abs(gotCPU-*want.CPUQuota) > 0.001 { - t.Errorf("CPUQuotaWithRoot() = %v, want %v", gotCPU, *want.CPUQuota) + t.Errorf("CPUQuota() = %v, want %v", gotCPU, *want.CPUQuota) } - gotMem, err := MemoryLimitWithRoot(root) + gotMem, err := cg.MemoryLimit() if want.MemoryLimit == nil { if !errors.Is(err, ErrNoLimit) { - t.Errorf("MemoryLimitWithRoot() error = %v, want ErrNoLimit", err) + t.Errorf("MemoryLimit() error = %v, want ErrNoLimit", err) } } else if err != nil { - t.Fatalf("MemoryLimitWithRoot(%q): %v", root, err) + t.Fatalf("MemoryLimit(): %v", err) } else if gotMem != *want.MemoryLimit { - t.Errorf("MemoryLimitWithRoot() = %v, want %v", gotMem, *want.MemoryLimit) + t.Errorf("MemoryLimit() = %v, want %v", gotMem, *want.MemoryLimit) } }) } } -// TestIntegrationCgroupLimits calls the real CPUQuota() and MemoryLimit() -// functions against the live kernel cgroup interface. It is intended to run -// inside a Docker container started with --cpus and --memory flags. +// TestIntegrationCgroupLimits calls the real New(), CPUQuota(), and +// MemoryLimit() against the live kernel cgroup interface. It is intended to +// run inside a Docker container started with --cpus and --memory flags. // // The test is skipped unless CGROUP_EXPECTED_CPU_QUOTA and // CGROUP_EXPECTED_MEMORY_LIMIT are set in the environment. @@ -330,12 +129,17 @@ func TestIntegrationCgroupLimits(t *testing.T) { t.Skip("set CGROUP_EXPECTED_CPU_QUOTA and CGROUP_EXPECTED_MEMORY_LIMIT to run") } + cg, err := New() + if err != nil { + t.Fatalf("New() error: %v", err) + } + if cpuStr != "" { wantCPU, err := strconv.ParseFloat(cpuStr, 64) if err != nil { t.Fatalf("bad CGROUP_EXPECTED_CPU_QUOTA %q: %v", cpuStr, err) } - gotCPU, err := CPUQuota() + gotCPU, err := cg.CPUQuota() if err != nil { t.Fatalf("CPUQuota() error: %v", err) } @@ -349,7 +153,7 @@ func TestIntegrationCgroupLimits(t *testing.T) { if err != nil { t.Fatalf("bad CGROUP_EXPECTED_MEMORY_LIMIT %q: %v", memStr, err) } - gotMem, err := MemoryLimit() + gotMem, err := cg.MemoryLimit() if err != nil { t.Fatalf("MemoryLimit() error: %v", err) } From 42319f49901a694a43232335bdf2defe65c0b092 Mon Sep 17 00:00:00 2001 From: mbrt <405934+mbrt@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:51:21 +0100 Subject: [PATCH 9/9] Remove half-core test as it's redundant --- bin/gen-cgroup-testdata | 1 - bin/snapshot-cgroup | 2 +- cgroup/testdata/v2-half-core-128m/expected.json | 1 - cgroup/testdata/v2-half-core-128m/proc/self/cgroup | 1 - cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max | 1 - cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max | 1 - 6 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 cgroup/testdata/v2-half-core-128m/expected.json delete mode 100644 cgroup/testdata/v2-half-core-128m/proc/self/cgroup delete mode 100644 cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max delete mode 100644 cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max diff --git a/bin/gen-cgroup-testdata b/bin/gen-cgroup-testdata index 6f0a15f7..3d9215bb 100755 --- a/bin/gen-cgroup-testdata +++ b/bin/gen-cgroup-testdata @@ -41,7 +41,6 @@ generate() { EOF } -generate "v2-half-core-128m" 0.5 134217728 --cpus=0.5 --memory=128m generate "v2-two-cores-256m" 2.0 268435456 --cpus=2 --memory=256m generate "v2-quarter-core-64m" 0.25 67108864 --cpus=0.25 --memory=64m generate "v2-no-limit" null null diff --git a/bin/snapshot-cgroup b/bin/snapshot-cgroup index ca1c818b..5fed85f9 100755 --- a/bin/snapshot-cgroup +++ b/bin/snapshot-cgroup @@ -1,7 +1,7 @@ #!/usr/bin/env bash # # snapshot-cgroup - Copy cgroup v2 files from the live system into a -# directory tree suitable for use with CPUQuotaWithRoot / MemoryLimitWithRoot. +# directory tree suitable for use with TestIntegrationCgroupLimits. # # Usage: snapshot-cgroup # diff --git a/cgroup/testdata/v2-half-core-128m/expected.json b/cgroup/testdata/v2-half-core-128m/expected.json deleted file mode 100644 index 8d190583..00000000 --- a/cgroup/testdata/v2-half-core-128m/expected.json +++ /dev/null @@ -1 +0,0 @@ -{"cpu_quota": 0.5, "memory_limit": 134217728} diff --git a/cgroup/testdata/v2-half-core-128m/proc/self/cgroup b/cgroup/testdata/v2-half-core-128m/proc/self/cgroup deleted file mode 100644 index 1e027b2a..00000000 --- a/cgroup/testdata/v2-half-core-128m/proc/self/cgroup +++ /dev/null @@ -1 +0,0 @@ -0::/ diff --git a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max deleted file mode 100644 index e4e48b99..00000000 --- a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/cpu.max +++ /dev/null @@ -1 +0,0 @@ -50000 100000 diff --git a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max b/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max deleted file mode 100644 index 7a758363..00000000 --- a/cgroup/testdata/v2-half-core-128m/sys/fs/cgroup/memory.max +++ /dev/null @@ -1 +0,0 @@ -134217728