gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/cgroup/cgroup_v2_test.go (about)

     1  // Copyright The runc Authors.
     2  // Copyright 2021 The gVisor Authors.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //	https://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  package cgroup
    16  
    17  import (
    18  	"io/ioutil"
    19  	"os"
    20  	"path/filepath"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  
    25  	specs "github.com/opencontainers/runtime-spec/specs-go"
    26  	"gvisor.dev/gvisor/pkg/test/testutil"
    27  )
    28  
    29  var (
    30  	cgroupv2MountInfo    = `29 22 0:26 / /sys/fs/cgroup rw shared:4 - cgroup2 cgroup2 rw,seclabel,nsdelegate`
    31  	multipleCg2MountInfo = `34 28 0:29 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime shared:8 - cgroup2 cgroup2 rw
    32  1479 28 0:29 / /run/some/module/cgroupv2 rw,relatime shared:650 - cgroup2 none rw
    33  `
    34  )
    35  
    36  func TestIO(t *testing.T) {
    37  	for _, tc := range []struct {
    38  		name  string
    39  		spec  *specs.LinuxBlockIO
    40  		path  string
    41  		wants string
    42  	}{
    43  		{
    44  			name: "simple",
    45  			spec: &specs.LinuxBlockIO{
    46  				Weight: uint16Ptr(1),
    47  			},
    48  			path:  "io.weight",
    49  			wants: strconv.FormatUint(convertBlkIOToIOWeightValue(1), 10),
    50  		},
    51  		{
    52  			name: "throttlereadbps",
    53  			spec: &specs.LinuxBlockIO{
    54  				ThrottleReadBpsDevice: []specs.LinuxThrottleDevice{
    55  					makeLinuxThrottleDevice(1, 2, 3),
    56  				},
    57  			},
    58  			path:  "io.max",
    59  			wants: "1:2 rbps=3",
    60  		},
    61  		{
    62  			name: "throttlewritebps",
    63  			spec: &specs.LinuxBlockIO{
    64  				ThrottleWriteBpsDevice: []specs.LinuxThrottleDevice{
    65  					makeLinuxThrottleDevice(4, 5, 6),
    66  				},
    67  			},
    68  			path:  "io.max",
    69  			wants: "4:5 wbps=6",
    70  		},
    71  		{
    72  			name: "throttlereadiops",
    73  			spec: &specs.LinuxBlockIO{
    74  				ThrottleReadIOPSDevice: []specs.LinuxThrottleDevice{
    75  					makeLinuxThrottleDevice(7, 8, 9),
    76  				},
    77  			},
    78  			path:  "io.max",
    79  			wants: "7:8 riops=9",
    80  		},
    81  		{
    82  			name: "throttlewriteiops",
    83  			spec: &specs.LinuxBlockIO{
    84  				ThrottleWriteIOPSDevice: []specs.LinuxThrottleDevice{
    85  					makeLinuxThrottleDevice(10, 11, 12),
    86  				},
    87  			},
    88  			path:  "io.max",
    89  			wants: "10:11 wiops=12",
    90  		},
    91  		{
    92  			name:  "nil_values",
    93  			spec:  &specs.LinuxBlockIO{},
    94  			path:  "not_used",
    95  			wants: "",
    96  		},
    97  	} {
    98  		t.Run(tc.name, func(t *testing.T) {
    99  			testutil.TmpDir()
   100  			dir, err := ioutil.TempDir(testutil.TmpDir(), "cgroup")
   101  			if err != nil {
   102  				t.Fatalf("error creating temporary directory: %v", err)
   103  			}
   104  			defer os.RemoveAll(dir)
   105  
   106  			fd, err := os.Create(filepath.Join(dir, tc.path))
   107  			if err != nil {
   108  				t.Fatalf("os.CreatTemp(): %v", err)
   109  			}
   110  			fd.Close()
   111  
   112  			spec := &specs.LinuxResources{
   113  				BlockIO: tc.spec,
   114  			}
   115  			ctrlr := io2{}
   116  			if err := ctrlr.set(spec, dir); err != nil {
   117  				t.Fatalf("ctrlr.set(): %v", err)
   118  			}
   119  
   120  			gotBytes, err := ioutil.ReadFile(filepath.Join(dir, tc.path))
   121  			if err != nil {
   122  				t.Fatal(err.Error())
   123  			}
   124  			got := strings.TrimSuffix(string(gotBytes), "\n")
   125  			if got != tc.wants {
   126  				t.Errorf("wrong file content, file: %q, want: %q, got: %q", tc.path, tc.wants, got)
   127  			}
   128  		})
   129  	}
   130  }
   131  
   132  func TestLoadPathsCgroupv2(t *testing.T) {
   133  	for _, tc := range []struct {
   134  		name      string
   135  		cgroups   string
   136  		mountinfo string
   137  		want      map[string]string
   138  		err       string
   139  	}{
   140  		{
   141  			name:      "cgroupv2",
   142  			cgroups:   "0::/docker/123",
   143  			mountinfo: cgroupv2MountInfo,
   144  			want: map[string]string{
   145  				"cgroup2": "docker/123",
   146  			},
   147  		},
   148  
   149  		{
   150  			name:      "cgroupv2-nested",
   151  			cgroups:   "0::/",
   152  			mountinfo: cgroupv2MountInfo,
   153  			want: map[string]string{
   154  				"cgroup2": ".",
   155  			},
   156  		},
   157  		{
   158  			name:      "multiple-cgv2",
   159  			cgroups:   "0::/system.slice/containerd.service\n",
   160  			mountinfo: multipleCg2MountInfo,
   161  			want: map[string]string{
   162  				"cgroup2": "system.slice/containerd.service",
   163  			},
   164  		},
   165  	} {
   166  		t.Run(tc.name, func(t *testing.T) {
   167  			r := strings.NewReader(tc.cgroups)
   168  			mountinfo := strings.NewReader(tc.mountinfo)
   169  			got, err := loadPathsHelper(r, mountinfo, true)
   170  			if len(tc.err) == 0 {
   171  				if err != nil {
   172  					t.Fatalf("Unexpected error: %v", err)
   173  				}
   174  			} else if !strings.Contains(err.Error(), tc.err) {
   175  				t.Fatalf("Wrong error message, want: *%s*, got: %v", tc.err, err)
   176  			}
   177  			for key, vWant := range tc.want {
   178  				vGot, ok := got[key]
   179  				if !ok {
   180  					t.Errorf("Missing controller %q", key)
   181  				}
   182  				if vWant != vGot {
   183  					t.Errorf("Wrong controller %q value, want: %q, got: %q", key, vWant, vGot)
   184  				}
   185  				delete(got, key)
   186  			}
   187  			for k, v := range got {
   188  				t.Errorf("Unexpected controller %q: %q", k, v)
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  func TestGetLimits(t *testing.T) {
   195  	for _, tc := range []struct {
   196  		name      string
   197  		mem       string
   198  		cpu       string
   199  		expMem    uint64
   200  		expCPU    int
   201  		limitPath string
   202  		path      string
   203  	}{
   204  		{
   205  			name:      "get limit from parent cgroup",
   206  			mem:       "150",
   207  			cpu:       "100 50",
   208  			limitPath: "user.slice",
   209  			path:      "user.slice/container.scope",
   210  			expMem:    150,
   211  			expCPU:    2,
   212  		},
   213  		{
   214  			name:      "get limit from leaf cgroup",
   215  			mem:       "150",
   216  			cpu:       "100 50",
   217  			limitPath: "user.slice/container.scope",
   218  			path:      "user.slice/container.scope",
   219  			expMem:    150,
   220  			expCPU:    2,
   221  		},
   222  	} {
   223  		t.Run(tc.name, func(t *testing.T) {
   224  			testutil.TmpDir()
   225  			dir, err := ioutil.TempDir(testutil.TmpDir(), "cgroup")
   226  			if err != nil {
   227  				t.Fatalf("error creating temporary directory: %v", err)
   228  			}
   229  			defer os.RemoveAll(dir)
   230  
   231  			fullPath := filepath.Join(dir, tc.path)
   232  			if err := os.MkdirAll(fullPath, 0o777); err != nil {
   233  				t.Fatalf("os.MkdirAll(): %v", err)
   234  			}
   235  			cg := cgroupV2{
   236  				Mountpoint: dir,
   237  				Path:       tc.path,
   238  			}
   239  
   240  			if err := os.WriteFile(filepath.Join(dir, tc.path, "memory.max"), []byte("max"), 0o777); err != nil {
   241  				t.Fatalf("os.WriteFile(): %v", err)
   242  			}
   243  			if err := os.WriteFile(filepath.Join(dir, tc.path, "cpu.max"), []byte("max max"), 0o777); err != nil {
   244  				t.Fatalf("os.WriteFile(): %v", err)
   245  			}
   246  			if err := os.WriteFile(filepath.Join(dir, tc.limitPath, "memory.max"), []byte(tc.mem), 0o655); err != nil {
   247  				t.Fatalf("os.WriteFile(): %v", err)
   248  			}
   249  			if err := os.WriteFile(filepath.Join(dir, tc.limitPath, "cpu.max"), []byte(tc.cpu), 0o655); err != nil {
   250  				t.Fatalf("os.WriteFile(): %v", err)
   251  			}
   252  
   253  			quota, err := cg.CPUQuota()
   254  			if err != nil {
   255  				t.Fatalf("cg.CPUQuota(): %v", err)
   256  			}
   257  			if int(quota) != tc.expCPU {
   258  				t.Errorf("cg.CPUQuota() = %v, want %v", quota, tc.expCPU)
   259  			}
   260  			mem, err := cg.MemoryLimit()
   261  			if err != nil {
   262  				t.Fatalf("cg.MemoryLimit(): %v", err)
   263  			}
   264  			if mem != tc.expMem {
   265  				t.Errorf("cg.MemoryLimit() = %v, want %v", mem, tc.expMem)
   266  			}
   267  		})
   268  	}
   269  }
   270  
   271  func TestNumToStr(t *testing.T) {
   272  	cases := map[int64]string{
   273  		0:  "",
   274  		-1: "max",
   275  		10: "10",
   276  	}
   277  	for i, expected := range cases {
   278  		got := numToStr(i)
   279  		if got != expected {
   280  			t.Errorf("expected numToStr(%d) to be %q, got %q", i, expected, got)
   281  		}
   282  	}
   283  }
   284  
   285  func TestConvertBlkIOToIOWeightValue(t *testing.T) {
   286  	cases := map[uint16]uint64{
   287  		0:    0,
   288  		10:   1,
   289  		1000: 10000,
   290  	}
   291  	for i, expected := range cases {
   292  		got := convertBlkIOToIOWeightValue(i)
   293  		if got != expected {
   294  			t.Errorf("expected ConvertBlkIOToIOWeightValue(%d) to be %d, got %d", i, expected, got)
   295  		}
   296  	}
   297  }
   298  
   299  func TestConvertCPUSharesToCgroupV2Value(t *testing.T) {
   300  	cases := map[uint64]uint64{
   301  		0:      0,
   302  		2:      1,
   303  		262144: 10000,
   304  	}
   305  	for i, expected := range cases {
   306  		got := convertCPUSharesToCgroupV2Value(i)
   307  		if got != expected {
   308  			t.Errorf("expected ConvertCPUSharesToCgroupV2Value(%d) to be %d, got %d", i, expected, got)
   309  		}
   310  	}
   311  }
   312  
   313  func TestConvertMemorySwapToCgroupV2Value(t *testing.T) {
   314  	cases := []struct {
   315  		memswap, memory int64
   316  		expected        int64
   317  		expErr          bool
   318  	}{
   319  		{
   320  			memswap:  0,
   321  			memory:   0,
   322  			expected: 0,
   323  		},
   324  		{
   325  			memswap:  -1,
   326  			memory:   0,
   327  			expected: -1,
   328  		},
   329  		{
   330  			memswap:  -1,
   331  			memory:   -1,
   332  			expected: -1,
   333  		},
   334  		{
   335  			memswap: -2,
   336  			memory:  0,
   337  			expErr:  true,
   338  		},
   339  		{
   340  			memswap:  -1,
   341  			memory:   1000,
   342  			expected: -1,
   343  		},
   344  		{
   345  			memswap:  1000,
   346  			memory:   1000,
   347  			expected: 0,
   348  		},
   349  		{
   350  			memswap:  500,
   351  			memory:   200,
   352  			expected: 300,
   353  		},
   354  		{
   355  			memswap: 300,
   356  			memory:  400,
   357  			expErr:  true,
   358  		},
   359  		{
   360  			memswap: 300,
   361  			memory:  0,
   362  			expErr:  true,
   363  		},
   364  		{
   365  			memswap: 300,
   366  			memory:  -300,
   367  			expErr:  true,
   368  		},
   369  		{
   370  			memswap: 300,
   371  			memory:  -1,
   372  			expErr:  true,
   373  		},
   374  	}
   375  
   376  	for _, c := range cases {
   377  		swap, err := convertMemorySwapToCgroupV2Value(c.memswap, c.memory)
   378  		if c.expErr {
   379  			if err == nil {
   380  				t.Errorf("memswap: %d, memory %d, expected error, got %d, nil", c.memswap, c.memory, swap)
   381  			}
   382  			// no more checks
   383  			continue
   384  		}
   385  		if err != nil {
   386  			t.Errorf("memswap: %d, memory %d, expected success, got error %s", c.memswap, c.memory, err)
   387  		}
   388  		if swap != c.expected {
   389  			t.Errorf("memswap: %d, memory %d, expected %d, got %d", c.memswap, c.memory, c.expected, swap)
   390  		}
   391  	}
   392  }
   393  
   394  func TestParseCPUQuota(t *testing.T) {
   395  	cases := []struct {
   396  		quota    string
   397  		expected float64
   398  		expErr   bool
   399  	}{
   400  		{
   401  			quota:    "max 100000\n",
   402  			expected: -1,
   403  		},
   404  		{
   405  			quota:    "10000 100000",
   406  			expected: 0.1,
   407  		},
   408  		{
   409  			quota:    "20000 100000\n",
   410  			expected: 0.2,
   411  		},
   412  
   413  		{
   414  			quota:    "-1",
   415  			expected: -1,
   416  			expErr:   true,
   417  		},
   418  	}
   419  
   420  	for _, c := range cases {
   421  		res, err := parseCPUQuota(c.quota)
   422  		if c.expErr {
   423  			if err == nil {
   424  				t.Errorf("quota: %q, expected error, got %.2f, nil", c.quota, res)
   425  			}
   426  			continue
   427  		}
   428  		if err != nil {
   429  			t.Errorf("quota: %q, expected success, got error %s", c.quota, err)
   430  		}
   431  		if res != c.expected {
   432  			t.Errorf("quota: %q, expected %.2f, got error %.2f", c.quota, c.expected, res)
   433  		}
   434  	}
   435  }