github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_run_cgroup_linux_test.go (about)

     1  /*
     2     Copyright The containerd 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         http://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  */
    16  
    17  package main
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"testing"
    25  
    26  	"github.com/containerd/cgroups/v3"
    27  	"github.com/containerd/continuity/testutil/loopback"
    28  	"github.com/containerd/nerdctl/pkg/cmd/container"
    29  	"github.com/containerd/nerdctl/pkg/testutil"
    30  	"github.com/moby/sys/userns"
    31  	"gotest.tools/v3/assert"
    32  )
    33  
    34  func TestRunCgroupV2(t *testing.T) {
    35  	t.Parallel()
    36  	if cgroups.Mode() != cgroups.Unified {
    37  		t.Skip("test requires cgroup v2")
    38  	}
    39  	base := testutil.NewBase(t)
    40  	info := base.Info()
    41  	switch info.CgroupDriver {
    42  	case "none", "":
    43  		t.Skip("test requires cgroup driver")
    44  	}
    45  
    46  	if !info.MemoryLimit {
    47  		t.Skip("test requires MemoryLimit")
    48  	}
    49  	if !info.SwapLimit {
    50  		t.Skip("test requires SwapLimit")
    51  	}
    52  	if !info.CPUShares {
    53  		t.Skip("test requires CPUShares")
    54  	}
    55  	if !info.CPUSet {
    56  		t.Skip("test requires CPUSet")
    57  	}
    58  	if !info.PidsLimit {
    59  		t.Skip("test requires PidsLimit")
    60  	}
    61  	const expected1 = `42000 100000
    62  44040192
    63  44040192
    64  42
    65  77
    66  0-1
    67  0
    68  `
    69  	const expected2 = `42000 100000
    70  44040192
    71  60817408
    72  6291456
    73  42
    74  77
    75  0-1
    76  0
    77  `
    78  
    79  	// In CgroupV2 CPUWeight replace CPUShares => weight := 1 + ((shares-2)*9999)/262142
    80  	base.Cmd("run", "--rm",
    81  		"--cpus", "0.42", "--cpuset-mems", "0",
    82  		"--memory", "42m",
    83  		"--pids-limit", "42",
    84  		"--cpu-shares", "2000", "--cpuset-cpus", "0-1",
    85  		"-w", "/sys/fs/cgroup", testutil.AlpineImage,
    86  		"cat", "cpu.max", "memory.max", "memory.swap.max",
    87  		"pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1)
    88  	base.Cmd("run", "--rm",
    89  		"--cpu-quota", "42000", "--cpuset-mems", "0",
    90  		"--cpu-period", "100000", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m",
    91  		"--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1",
    92  		"-w", "/sys/fs/cgroup", testutil.AlpineImage,
    93  		"cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max",
    94  		"cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2)
    95  
    96  	base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate1", "-w", "/sys/fs/cgroup", "-d",
    97  		testutil.AlpineImage, "sleep", "infinity").AssertOK()
    98  	defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate1").Run()
    99  	update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000",
   100  		"--memory", "42m",
   101  		"--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1"}
   102  	if base.Target == testutil.Docker && info.CgroupVersion == "2" && info.SwapLimit {
   103  		// Workaround for Docker with cgroup v2:
   104  		// > Error response from daemon: Cannot update container 67c13276a13dd6a091cdfdebb355aa4e1ecb15fbf39c2b5c9abee89053e88fce:
   105  		// > Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time
   106  		update = append(update, "--memory-swap=84m")
   107  	}
   108  	update = append(update, testutil.Identifier(t)+"-testUpdate1")
   109  	base.Cmd(update...).AssertOK()
   110  	base.Cmd("exec", testutil.Identifier(t)+"-testUpdate1",
   111  		"cat", "cpu.max", "memory.max", "memory.swap.max",
   112  		"pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected1)
   113  
   114  	defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate2").Run()
   115  	base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate2", "-w", "/sys/fs/cgroup", "-d",
   116  		testutil.AlpineImage, "sleep", "infinity").AssertOK()
   117  	base.EnsureContainerStarted(testutil.Identifier(t) + "-testUpdate2")
   118  
   119  	base.Cmd("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000",
   120  		"--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m",
   121  		"--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1",
   122  		testutil.Identifier(t)+"-testUpdate2").AssertOK()
   123  	base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2",
   124  		"cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low",
   125  		"pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2)
   126  
   127  }
   128  
   129  func TestRunCgroupV1(t *testing.T) {
   130  	t.Parallel()
   131  	switch cgroups.Mode() {
   132  	case cgroups.Legacy, cgroups.Hybrid:
   133  	default:
   134  		t.Skip("test requires cgroup v1")
   135  	}
   136  	base := testutil.NewBase(t)
   137  	info := base.Info()
   138  	switch info.CgroupDriver {
   139  	case "none", "":
   140  		t.Skip("test requires cgroup driver")
   141  	}
   142  	if !info.MemoryLimit {
   143  		t.Skip("test requires MemoryLimit")
   144  	}
   145  	if !info.CPUShares {
   146  		t.Skip("test requires CPUShares")
   147  	}
   148  	if !info.CPUSet {
   149  		t.Skip("test requires CPUSet")
   150  	}
   151  	if !info.PidsLimit {
   152  		t.Skip("test requires PidsLimit")
   153  	}
   154  	quota := "/sys/fs/cgroup/cpu/cpu.cfs_quota_us"
   155  	period := "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
   156  	cpusetMems := "/sys/fs/cgroup/cpuset/cpuset.mems"
   157  	memoryLimit := "/sys/fs/cgroup/memory/memory.limit_in_bytes"
   158  	memoryReservation := "/sys/fs/cgroup/memory/memory.soft_limit_in_bytes"
   159  	memorySwap := "/sys/fs/cgroup/memory/memory.memsw.limit_in_bytes"
   160  	memorySwappiness := "/sys/fs/cgroup/memory/memory.swappiness"
   161  	pidsLimit := "/sys/fs/cgroup/pids/pids.max"
   162  	cpuShare := "/sys/fs/cgroup/cpu/cpu.shares"
   163  	cpusetCpus := "/sys/fs/cgroup/cpuset/cpuset.cpus"
   164  
   165  	const expected = "42000\n100000\n0\n44040192\n6291456\n104857600\n0\n42\n2000\n0-1\n"
   166  	base.Cmd("run", "--rm", "--cpus", "0.42", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected)
   167  	base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected)
   168  }
   169  
   170  func TestRunDevice(t *testing.T) {
   171  	if os.Geteuid() != 0 || userns.RunningInUserNS() {
   172  		t.Skip("test requires the root in the initial user namespace")
   173  	}
   174  
   175  	const n = 3
   176  	lo := make([]*loopback.Loopback, n)
   177  	loContent := make([]string, n)
   178  
   179  	for i := 0; i < n; i++ {
   180  		var err error
   181  		lo[i], err = loopback.New(4096)
   182  		assert.NilError(t, err)
   183  		t.Logf("lo[%d] = %+v", i, lo[i])
   184  		defer lo[i].Close()
   185  		loContent[i] = fmt.Sprintf("lo%d-content", i)
   186  		assert.NilError(t, os.WriteFile(lo[i].Device, []byte(loContent[i]), 0700))
   187  	}
   188  
   189  	base := testutil.NewBase(t)
   190  	containerName := testutil.Identifier(t)
   191  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   192  	// lo0 is readable but not writable.
   193  	// lo1 is readable and writable
   194  	// lo2 is not accessible.
   195  	base.Cmd("run",
   196  		"-d",
   197  		"--name", containerName,
   198  		"--device", lo[0].Device+":r",
   199  		"--device", lo[1].Device,
   200  		testutil.AlpineImage, "sleep", "infinity").Run()
   201  
   202  	base.Cmd("exec", containerName, "cat", lo[0].Device).AssertOutContains(loContent[0])
   203  	base.Cmd("exec", containerName, "cat", lo[1].Device).AssertOutContains(loContent[1])
   204  	base.Cmd("exec", containerName, "cat", lo[2].Device).AssertFail()
   205  	base.Cmd("exec", containerName, "sh", "-ec", "echo -n \"overwritten-lo0-content\">"+lo[0].Device).AssertFail()
   206  	base.Cmd("exec", containerName, "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device).AssertOK()
   207  	lo1Read, err := os.ReadFile(lo[1].Device)
   208  	assert.NilError(t, err)
   209  	assert.Equal(t, string(bytes.Trim(lo1Read, "\x00")), "overwritten-lo1-content")
   210  }
   211  
   212  func TestParseDevice(t *testing.T) {
   213  	t.Parallel()
   214  	type testCase struct {
   215  		s               string
   216  		expectedDevPath string
   217  		expectedMode    string
   218  		err             string
   219  	}
   220  	testCases := []testCase{
   221  		{
   222  			s:               "/dev/sda1",
   223  			expectedDevPath: "/dev/sda1",
   224  			expectedMode:    "rwm",
   225  		},
   226  		{
   227  			s:               "/dev/sda2:r",
   228  			expectedDevPath: "/dev/sda2",
   229  			expectedMode:    "r",
   230  		},
   231  		{
   232  			s:               "/dev/sda3:rw",
   233  			expectedDevPath: "/dev/sda3",
   234  			expectedMode:    "rw",
   235  		},
   236  		{
   237  			s:   "sda4",
   238  			err: "not an absolute path",
   239  		},
   240  		{
   241  			s:               "/dev/sda5:/dev/sda5",
   242  			expectedDevPath: "/dev/sda5",
   243  			expectedMode:    "rwm",
   244  		},
   245  		{
   246  			s:   "/dev/sda6:/dev/foo6",
   247  			err: "not supported yet",
   248  		},
   249  		{
   250  			s:   "/dev/sda7:/dev/sda7:rwmx",
   251  			err: "unexpected rune",
   252  		},
   253  	}
   254  
   255  	for _, tc := range testCases {
   256  		t.Log(tc.s)
   257  		devPath, mode, err := container.ParseDevice(tc.s)
   258  		if tc.err == "" {
   259  			assert.NilError(t, err)
   260  			assert.Equal(t, tc.expectedDevPath, devPath)
   261  			assert.Equal(t, tc.expectedMode, mode)
   262  		} else {
   263  			assert.ErrorContains(t, err, tc.err)
   264  		}
   265  	}
   266  }
   267  
   268  func TestRunCgroupConf(t *testing.T) {
   269  	t.Parallel()
   270  	if cgroups.Mode() != cgroups.Unified {
   271  		t.Skip("test requires cgroup v2")
   272  	}
   273  	testutil.DockerIncompatible(t) // Docker lacks --cgroup-conf
   274  	base := testutil.NewBase(t)
   275  	info := base.Info()
   276  	switch info.CgroupDriver {
   277  	case "none", "":
   278  		t.Skip("test requires cgroup driver")
   279  	}
   280  	if !info.MemoryLimit {
   281  		t.Skip("test requires MemoryLimit")
   282  	}
   283  	base.Cmd("run", "--rm", "--cgroup-conf", "memory.high=33554432", "-w", "/sys/fs/cgroup", testutil.AlpineImage,
   284  		"cat", "memory.high").AssertOutExactly("33554432\n")
   285  }
   286  
   287  func TestRunCgroupParent(t *testing.T) {
   288  	t.Parallel()
   289  	base := testutil.NewBase(t)
   290  	info := base.Info()
   291  	containerName := testutil.Identifier(t)
   292  	defer base.Cmd("rm", "-f", containerName).Run()
   293  
   294  	switch info.CgroupDriver {
   295  	case "none", "":
   296  		t.Skip("test requires cgroup driver")
   297  	}
   298  
   299  	t.Logf("Using %q cgroup driver", info.CgroupDriver)
   300  
   301  	parent := "/foobarbaz"
   302  	if info.CgroupDriver == "systemd" {
   303  		// Path separators aren't allowed in systemd path. runc
   304  		// explicitly checks for this.
   305  		// https://github.com/opencontainers/runc/blob/016a0d29d1750180b2a619fc70d6fe0d80111be0/libcontainer/cgroups/systemd/common.go#L65-L68
   306  		parent = "foobarbaz.slice"
   307  	}
   308  
   309  	// cgroup2 without host cgroup ns will just output 0::/ which doesn't help much to verify
   310  	// we got our expected path. This approach should work for both cgroup1 and 2, there will
   311  	// just be many more entries for cgroup1 as there'll be an entry per controller.
   312  	base.Cmd(
   313  		"run",
   314  		"-d",
   315  		"--name",
   316  		containerName,
   317  		"--cgroupns=host",
   318  		"--cgroup-parent", parent,
   319  		testutil.AlpineImage,
   320  		"sleep",
   321  		"infinity",
   322  	).AssertOK()
   323  
   324  	id := base.InspectContainer(containerName).ID
   325  	expected := filepath.Join(parent, id)
   326  	if info.CgroupDriver == "systemd" {
   327  		expected = filepath.Join(parent, fmt.Sprintf("nerdctl-%s", id))
   328  	}
   329  	base.Cmd("exec", containerName, "cat", "/proc/self/cgroup").AssertOutContains(expected)
   330  }
   331  
   332  func TestRunBlkioWeightCgroupV2(t *testing.T) {
   333  	t.Parallel()
   334  	if cgroups.Mode() != cgroups.Unified {
   335  		t.Skip("test requires cgroup v2")
   336  	}
   337  	if _, err := os.Stat("/sys/module/bfq"); err != nil {
   338  		t.Skipf("test requires \"bfq\" module to be loaded: %v", err)
   339  	}
   340  	base := testutil.NewBase(t)
   341  	info := base.Info()
   342  	switch info.CgroupDriver {
   343  	case "none", "":
   344  		t.Skip("test requires cgroup driver")
   345  	}
   346  	containerName := testutil.Identifier(t)
   347  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   348  	// when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight
   349  	base.Cmd("run", "--name", containerName, "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", "infinity").AssertOK()
   350  	base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 300\n")
   351  	base.Cmd("update", containerName, "--blkio-weight", "400").AssertOK()
   352  	base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 400\n")
   353  }