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 }