blob: 95fccaff31dafbd614bd444c508d01f256561d91 [file] [edit]
// 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"
"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)
}
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 := []string{}
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())
}
}
}