| // Copyright 2024 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 | |
| // | |
| // https://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. | |
| //go:build linux | |
| package resource | |
| import ( | |
| "context" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "syscall" | |
| "testing" | |
| ) | |
| type constraintTestOpts struct { | |
| // overrideAll creates all the mock files. | |
| overrideAll bool | |
| // cgroupVersion dictates the cgroup version fs to mock. | |
| cgroupVersion int | |
| // hasCPUController indicates if cgroupv1 has the cpu controller. | |
| hasCPUController bool | |
| // hasMemoryController indicates if cgroupv1 has the memory controller. | |
| hasMemoryController bool | |
| } | |
| func constraintTestSetup(t *testing.T, opts constraintTestOpts) (ConstraintClient, string) { | |
| t.Helper() | |
| tempDir := t.TempDir() | |
| testCgroupDir := filepath.Join(tempDir, "cgroup") | |
| if err := os.MkdirAll(testCgroupDir, 0755); err != nil { | |
| t.Fatalf("failed to create test cgroup directory: %v", err) | |
| } | |
| // Since the temp directory is in an actual file system, we need to use | |
| // os.RemoveAll to remove the cgroup directory. | |
| remove = os.RemoveAll | |
| t.Cleanup(func() { | |
| remove = syscall.Rmdir | |
| }) | |
| if opts.cgroupVersion == 1 { | |
| // Create all the fake controllers -- cpu and memory. | |
| var cpuController, memoryController string | |
| if opts.hasCPUController || opts.overrideAll { | |
| cpuController = "cpu" | |
| if err := os.MkdirAll(filepath.Join(testCgroupDir, "cpu"), 0755); err != nil { | |
| t.Fatalf("failed to create cpu directory: %v", err) | |
| } | |
| } | |
| if opts.hasMemoryController || opts.overrideAll { | |
| memoryController = "memory" | |
| if err := os.MkdirAll(filepath.Join(testCgroupDir, "memory"), 0755); err != nil { | |
| t.Fatalf("failed to create memory directory: %v", err) | |
| } | |
| } | |
| return &cgroupv1Client{ | |
| cgroupsDir: testCgroupDir, | |
| memoryLimitFile: "memory.limit_in_bytes", | |
| cpuController: cpuController, | |
| memoryController: memoryController, | |
| }, testCgroupDir | |
| } | |
| // Make a fake cgroup.controllers file. | |
| controllers := make([]string, 0) | |
| if opts.hasCPUController || opts.overrideAll { | |
| controllers = append(controllers, "cpu") | |
| } | |
| if opts.hasMemoryController || opts.overrideAll { | |
| controllers = append(controllers, "memory") | |
| } | |
| if err := os.WriteFile(filepath.Join(testCgroupDir, "cgroup.controllers"), []byte("cpu memory"), 0755); err != nil { | |
| t.Fatalf("failed to write cgroup.controllers file: %v", err) | |
| } | |
| // Write a cgroup.subtree_control file. | |
| if err := os.WriteFile(filepath.Join(testCgroupDir, "cgroup.subtree_control"), []byte(strings.Join(controllers, " ")), 0755); err != nil { | |
| t.Fatalf("failed to write cgroup.subtree_control file: %v", err) | |
| } | |
| return &cgroupv2Client{ | |
| cgroupsDir: testCgroupDir, | |
| memoryLimitFile: "memory.max", | |
| }, testCgroupDir | |
| } | |
| // TestInitCgroupv1 tests initializing the cgroupv1 client. | |
| func TestInitCgroupv1(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| opts constraintTestOpts | |
| cpuController bool | |
| memoryController bool | |
| }{ | |
| { | |
| name: "no-memory-or-cpu-controller", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 1, | |
| }, | |
| }, | |
| { | |
| name: "no-memory-controller", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 1, | |
| hasCPUController: true, | |
| }, | |
| cpuController: true, | |
| }, | |
| { | |
| name: "no-cpu-controller", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 1, | |
| hasMemoryController: true, | |
| }, | |
| memoryController: true, | |
| }, | |
| { | |
| name: "both-controllers", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 1, | |
| hasCPUController: true, | |
| hasMemoryController: true, | |
| }, | |
| cpuController: true, | |
| memoryController: true, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| _, cgroupsDir := constraintTestSetup(t, test.opts) | |
| cv1Client, err := initCgroupv1(cgroupsDir) | |
| if err != nil { | |
| t.Fatalf("initCgroupv1() returned unexpected err %v, expected nil", err) | |
| } | |
| if test.cpuController != (cv1Client.cpuController != "") { | |
| t.Errorf("initCgroupv1() cpuController got %q, expected %v", cv1Client.cpuController, test.cpuController) | |
| } | |
| if test.memoryController != (cv1Client.memoryController != "") { | |
| t.Errorf("initCgroupv1() memoryController got %q, expected %v", cv1Client.memoryController, test.memoryController) | |
| } | |
| }) | |
| } | |
| } | |
| // TestApplyResourceConstraintsV1 tests applying cgroup resource constraints when the kernel uses | |
| // cgroupv1. | |
| func TestApplyResourceConstraintsV1(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| opts constraintTestOpts | |
| expectErr bool | |
| }{ | |
| { | |
| name: "success", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 1, | |
| hasCPUController: true, | |
| hasMemoryController: true, | |
| }, | |
| }, | |
| { | |
| name: "miss-cpu-controller", | |
| opts: constraintTestOpts{cgroupVersion: 1, hasMemoryController: true}, | |
| expectErr: true, | |
| }, | |
| { | |
| name: "miss-memory-controller", | |
| opts: constraintTestOpts{cgroupVersion: 1, hasCPUController: true}, | |
| expectErr: true, | |
| }, | |
| { | |
| name: "miss-cpu-and-memory-controllers", | |
| opts: constraintTestOpts{cgroupVersion: 1}, | |
| expectErr: true, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| c, cgroupsDir := constraintTestSetup(t, test.opts) | |
| testName := "testPlugin_A" | |
| testConstraint := Constraint{1, testName, 50000, 10} | |
| // Apply constraints and check errors. | |
| err := c.Apply(testConstraint) | |
| if err != nil { | |
| if !test.expectErr { | |
| t.Fatalf("Apply(ctx, %+v) returned unexpected err %v, expected nil", testConstraint, err) | |
| } | |
| return | |
| } | |
| if test.expectErr { | |
| t.Fatalf("Apply(ctx, %+v) returned nil, expected err", testConstraint) | |
| } | |
| // Check that the mock test cgroup fs has the correct files written. | |
| // First check the fake CPU controller. | |
| files, err := os.ReadDir(filepath.Join(cgroupsDir, "cpu")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu directory: %v", err) | |
| } | |
| // See if a group was created for it. | |
| for _, file := range files { | |
| if file.IsDir() { | |
| continue | |
| } | |
| if file.Name() != testName { | |
| t.Fatalf("plugin cgroup %q for cpu was not created", file.Name()) | |
| } | |
| } | |
| // Check that a cgroup.procs file was written with the PID. | |
| procsContents, err := os.ReadFile(filepath.Join(cgroupsDir, "cpu", testName, "cgroup.procs")) | |
| if err != nil { | |
| t.Fatalf("failed to read cgroup.procs file: %v", err) | |
| } | |
| if string(procsContents) != "1" { | |
| t.Fatalf("cgroup.procs file for test plugin should contain 1.") | |
| } | |
| // Now check the fake memory controller. | |
| files, err = os.ReadDir(filepath.Join(cgroupsDir, "memory")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu directory: %v", err) | |
| } | |
| // See if a group was created for it. | |
| for _, file := range files { | |
| if file.IsDir() { | |
| continue | |
| } | |
| if file.Name() != testName { | |
| t.Fatalf("plugin cgroup %q for memory was not created", file.Name()) | |
| } | |
| } | |
| // Check that a cgroup.procs file was written with the PID. | |
| procsContents, err = os.ReadFile(filepath.Join(cgroupsDir, "memory", testName, "cgroup.procs")) | |
| if err != nil { | |
| t.Fatalf("failed to read cgroup.procs file: %v", err) | |
| } | |
| if string(procsContents) != "1" { | |
| t.Fatalf("cgroup.procs file for test plugin should contain 1.") | |
| } | |
| // Verify the memory file contains the correct contents. | |
| memoryContents, err := os.ReadFile(filepath.Join(cgroupsDir, "memory", testName, "memory.limit_in_bytes")) | |
| if err != nil { | |
| t.Fatalf("failed to read memory.max file: %v", err) | |
| } | |
| if string(memoryContents) != "50000" { | |
| t.Fatalf("memory.max file for test plugin should contain 50000.") | |
| } | |
| // Verify both CPU files. | |
| cpuPeriodContents, err := os.ReadFile(filepath.Join(cgroupsDir, "cpu", testName, "cpu.cfs_period_us")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu.cfs_period_us file: %v", err) | |
| } | |
| if string(cpuPeriodContents) != "100000" { | |
| t.Fatalf("cpu.cfs_period_us file for test plugin should contain 100000.") | |
| } | |
| cpuQuotaContents, err := os.ReadFile(filepath.Join(cgroupsDir, "cpu", testName, "cpu.cfs_quota_us")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu.cfs_quota_us file: %v", err) | |
| } | |
| if string(cpuQuotaContents) != "10000" { | |
| t.Fatalf("cpu.cfs_quota_us file for test plugin should contain 10000.") | |
| } | |
| }) | |
| } | |
| } | |
| func TestApplyResourceConstraintsV2(t *testing.T) { | |
| tests := []struct { | |
| name string | |
| opts constraintTestOpts | |
| expectErr bool | |
| }{ | |
| { | |
| name: "success", | |
| opts: constraintTestOpts{ | |
| cgroupVersion: 2, | |
| hasCPUController: true, | |
| hasMemoryController: true, | |
| }, | |
| }, | |
| { | |
| name: "miss-cpu-controller", | |
| opts: constraintTestOpts{cgroupVersion: 2, hasMemoryController: true}, | |
| expectErr: true, | |
| }, | |
| { | |
| name: "miss-memory-controller", | |
| opts: constraintTestOpts{cgroupVersion: 2, hasCPUController: true}, | |
| expectErr: true, | |
| }, | |
| { | |
| name: "miss-cpu-and-memory-controllers", | |
| opts: constraintTestOpts{cgroupVersion: 2}, | |
| expectErr: true, | |
| }, | |
| } | |
| for _, test := range tests { | |
| t.Run(test.name, func(t *testing.T) { | |
| c, cgroupsDir := constraintTestSetup(t, test.opts) | |
| testName := "testPlugin_B" | |
| testConstraint := Constraint{1, testName, 50000, 10} | |
| err := c.Apply(testConstraint) | |
| if err != nil { | |
| if !test.expectErr { | |
| t.Fatalf("applyResourceConstraints(ctx, %+v) returned unexpected err %v, expected nil", testConstraint, err) | |
| } | |
| return | |
| } | |
| if test.expectErr { | |
| t.Fatalf("applyResourceConstraints(ctx, %+v) returned nil, expected err", testConstraint) | |
| } | |
| // Check that the mock test cgroup fs has the correct files written. | |
| // First check the group was created. | |
| files, err := os.ReadDir(filepath.Join(cgroupsDir, guestAgentCgroupDir)) | |
| if err != nil { | |
| t.Fatalf("failed to read cgroup directory for guest agent: %v", err) | |
| } | |
| // Verify the cgroup was created for the test plugin. | |
| var foundFile bool | |
| for _, file := range files { | |
| if !file.IsDir() { | |
| continue | |
| } | |
| if file.Name() == testName { | |
| foundFile = true | |
| break | |
| } | |
| } | |
| if !foundFile { | |
| t.Fatalf("cgroup directory for test plugin %q was not created", testName) | |
| } | |
| // Verify the procs file. | |
| procsContents, err := os.ReadFile(filepath.Join(cgroupsDir, guestAgentCgroupDir, testName, "cgroup.procs")) | |
| if err != nil { | |
| t.Fatalf("failed to read cgroup.procs file for test plugin: %v", err) | |
| } | |
| if string(procsContents) != "1" { | |
| t.Fatalf("cgroup.procs file for test plugin should contain 1.") | |
| } | |
| // Verify the memory.max file. | |
| memoryContents, err := os.ReadFile(filepath.Join(cgroupsDir, guestAgentCgroupDir, testName, "memory.max")) | |
| if err != nil { | |
| t.Fatalf("failed to read memory.max file for test plugin: %v", err) | |
| } | |
| if string(memoryContents) != "50000" { | |
| t.Fatalf("memory.max file got %s, expected 50000", string(memoryContents)) | |
| } | |
| // Verify the cpu.max file. | |
| cpuMaxContents, err := os.ReadFile(filepath.Join(cgroupsDir, guestAgentCgroupDir, testName, "cpu.max")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu.max file for test plugin: %v", err) | |
| } | |
| if string(cpuMaxContents) != "10000 100000" { | |
| t.Fatalf("cpu.max file got %s, expected \"10000 100000\".", string(cpuMaxContents)) | |
| } | |
| }) | |
| } | |
| } | |
| func TestRemoveConstraintV1(t *testing.T) { | |
| opts := constraintTestOpts{cgroupVersion: 1, hasCPUController: true, hasMemoryController: true} | |
| c, cgroupsDir := constraintTestSetup(t, opts) | |
| testName := "testPlugin_A" | |
| testConstraint := Constraint{1, testName, 50000, 10} | |
| // No error should occur. | |
| if err := c.Apply(testConstraint); err != nil { | |
| t.Fatalf("Apply(ctx, %+v) returned unexpected err %v, expected nil", testConstraint, err) | |
| } | |
| if err := c.RemoveConstraint(context.Background(), testName); err != nil { | |
| t.Fatalf("RemoveConstraint(ctx, %s) returned unexpected err %v, expected nil", testName, err) | |
| } | |
| // Check the group doesn't exist in cpu controller. | |
| files, err := os.ReadDir(filepath.Join(cgroupsDir, "cpu")) | |
| if err != nil { | |
| t.Fatalf("failed to read cpu directory: %v", err) | |
| } | |
| for _, file := range files { | |
| if !file.IsDir() { | |
| continue | |
| } | |
| if file.Name() == testName { | |
| t.Fatalf("plugin cgroup %q for cpu still exists", file.Name()) | |
| } | |
| } | |
| // Check the group doesn't exist in memory controller. | |
| files, err = os.ReadDir(filepath.Join(cgroupsDir, "memory")) | |
| if err != nil { | |
| t.Fatalf("failed to read memory directory: %v", err) | |
| } | |
| for _, file := range files { | |
| if !file.IsDir() { | |
| continue | |
| } | |
| if file.Name() == testName { | |
| t.Fatalf("plugin cgroup %q for memory still exists", file.Name()) | |
| } | |
| } | |
| } | |
| func TestRemoveConstraintV2(t *testing.T) { | |
| opts := constraintTestOpts{cgroupVersion: 2, hasCPUController: true, hasMemoryController: true} | |
| c, cgroupsDir := constraintTestSetup(t, opts) | |
| testName := "testPlugin_A" | |
| testConstraint := Constraint{1, testName, 50000, 10} | |
| // No error should occur. | |
| if err := c.Apply(testConstraint); err != nil { | |
| t.Fatalf("Apply(ctx, %+v) returned unexpected err %v, expected nil", testConstraint, err) | |
| } | |
| if err := c.RemoveConstraint(context.Background(), testName); err != nil { | |
| t.Fatalf("RemoveConstraint(ctx, %s) returned unexpected err %v, expected nil", testName, err) | |
| } | |
| // Check the group doesn't exist in guest agent cgroup. | |
| files, err := os.ReadDir(filepath.Join(cgroupsDir, guestAgentCgroupDir)) | |
| if err != nil { | |
| t.Fatalf("failed to read cgroup directory for guest agent: %v", err) | |
| } | |
| for _, file := range files { | |
| if !file.IsDir() { | |
| continue | |
| } | |
| if file.Name() == testName { | |
| t.Fatalf("plugin cgroup %q for guest agent still exists", file.Name()) | |
| } | |
| } | |
| } |