gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/test/root/crictl_test.go (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package root 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/http" 24 "os" 25 "os/exec" 26 "path" 27 "regexp" 28 "strconv" 29 "strings" 30 "sync" 31 "testing" 32 "time" 33 34 "gvisor.dev/gvisor/pkg/cleanup" 35 "gvisor.dev/gvisor/pkg/test/criutil" 36 "gvisor.dev/gvisor/pkg/test/dockerutil" 37 "gvisor.dev/gvisor/pkg/test/testutil" 38 ) 39 40 // Tests for crictl have to be run as root (rather than in a user namespace) 41 // because crictl creates named network namespaces in /var/run/netns/. 42 43 // Sandbox returns a JSON config for a simple sandbox. Sandbox names must be 44 // unique so different names should be used when running tests on the same 45 // containerd instance. 46 func Sandbox(name string) string { 47 // Sandbox is a default JSON config for a sandbox. 48 s := map[string]any{ 49 "metadata": map[string]string{ 50 "name": name, 51 "namespace": "default", 52 "uid": testutil.RandomID(""), 53 }, 54 "linux": map[string]string{}, 55 "log_directory": "/tmp", 56 } 57 58 v, err := json.Marshal(s) 59 if err != nil { 60 // This shouldn't happen. 61 panic(err) 62 } 63 return string(v) 64 } 65 66 // SimpleSpec returns a JSON config for a simple container that runs the 67 // specified command in the specified image. 68 func SimpleSpec(name, image string, cmd []string, extra map[string]any) string { 69 s := map[string]any{ 70 "metadata": map[string]string{ 71 "name": name, 72 }, 73 "image": map[string]string{ 74 "image": testutil.ImageByName(image), 75 }, 76 // Log files are not deleted after root tests are run. Log to random 77 // paths to ensure logs are fresh. 78 "log_path": fmt.Sprintf("%s.log", testutil.RandomID(name)), 79 "stdin": false, 80 "tty": false, 81 } 82 if len(cmd) > 0 { // Omit if empty. 83 s["command"] = cmd 84 } 85 for k, v := range extra { 86 s[k] = v // Extra settings. 87 } 88 v, err := json.Marshal(s) 89 if err != nil { 90 // This shouldn't happen. 91 panic(err) 92 } 93 return string(v) 94 } 95 96 // Httpd is a JSON config for an httpd container. 97 var Httpd = SimpleSpec("httpd", "basic/httpd", nil, nil) 98 99 // TestCrictlSanity refers to b/112433158. 100 func TestCrictlSanity(t *testing.T) { 101 // Setup containerd and crictl. 102 crictl, cleanup, err := setup(t) 103 if err != nil { 104 t.Fatalf("failed to setup crictl: %v", err) 105 } 106 defer cleanup() 107 podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), Httpd) 108 if err != nil { 109 t.Fatalf("start failed: %v", err) 110 } 111 112 // Look for the httpd page. 113 if err = httpGet(crictl, podID, "index.html"); err != nil { 114 t.Fatalf("failed to get page: %v", err) 115 } 116 117 // Stop everything. 118 if err := crictl.StopPodAndContainer(podID, contID); err != nil { 119 t.Fatalf("stop failed: %v", err) 120 } 121 } 122 123 // HttpdMountPaths is a JSON config for an httpd container with additional 124 // mounts. 125 var HttpdMountPaths = SimpleSpec("httpd", "basic/httpd", nil, map[string]any{ 126 "mounts": []map[string]any{ 127 { 128 "container_path": "/var/run/secrets/kubernetes.io/serviceaccount", 129 "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/volumes/kubernetes.io~secret/default-token-2rpfx", 130 "readonly": true, 131 }, 132 { 133 "container_path": "/etc/hosts", 134 "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/etc-hosts", 135 "readonly": false, 136 }, 137 { 138 "container_path": "/dev/termination-log", 139 "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/containers/httpd/d1709580", 140 "readonly": false, 141 }, 142 { 143 "container_path": "/usr/local/apache2/htdocs/test", 144 "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064", 145 "readonly": true, 146 }, 147 }, 148 "linux": map[string]any{}, 149 }) 150 151 // TestMountPaths refers to b/117635704. 152 func TestMountPaths(t *testing.T) { 153 // Setup containerd and crictl. 154 crictl, cleanup, err := setup(t) 155 if err != nil { 156 t.Fatalf("failed to setup crictl: %v", err) 157 } 158 defer cleanup() 159 podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/httpd", Sandbox("default"), HttpdMountPaths) 160 if err != nil { 161 t.Fatalf("start failed: %v", err) 162 } 163 164 // Look for the directory available at /test. 165 if err = httpGet(crictl, podID, "test"); err != nil { 166 t.Fatalf("failed to get page: %v", err) 167 } 168 169 // Stop everything. 170 if err := crictl.StopPodAndContainer(podID, contID); err != nil { 171 t.Fatalf("stop failed: %v", err) 172 } 173 } 174 175 // TestMountPaths refers to b/118728671. 176 func TestMountOverSymlinks(t *testing.T) { 177 // Setup containerd and crictl. 178 crictl, cleanup, err := setup(t) 179 if err != nil { 180 t.Fatalf("failed to setup crictl: %v", err) 181 } 182 defer cleanup() 183 184 spec := SimpleSpec("busybox", "basic/symlink-resolv", []string{"sleep", "1000"}, nil) 185 podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/symlink-resolv", Sandbox("default"), spec) 186 if err != nil { 187 t.Fatalf("start failed: %v", err) 188 } 189 190 out, err := crictl.Exec(contID, "readlink", "/etc/resolv.conf") 191 if err != nil { 192 t.Fatalf("readlink failed: %v, out: %s", err, out) 193 } 194 if want := "/tmp/resolv.conf"; !strings.Contains(string(out), want) { 195 t.Fatalf("/etc/resolv.conf is not pointing to %q: %q", want, string(out)) 196 } 197 198 etc, err := crictl.Exec(contID, "cat", "/etc/resolv.conf") 199 if err != nil { 200 t.Fatalf("cat failed: %v, out: %s", err, etc) 201 } 202 tmp, err := crictl.Exec(contID, "cat", "/tmp/resolv.conf") 203 if err != nil { 204 t.Fatalf("cat failed: %v, out: %s", err, out) 205 } 206 if tmp != etc { 207 t.Fatalf("file content doesn't match:\n\t/etc/resolv.conf: %s\n\t/tmp/resolv.conf: %s", string(etc), string(tmp)) 208 } 209 210 // Stop everything. 211 if err := crictl.StopPodAndContainer(podID, contID); err != nil { 212 t.Fatalf("stop failed: %v", err) 213 } 214 } 215 216 // TestHomeDir tests that the HOME environment variable is set for 217 // Pod containers. 218 func TestHomeDir(t *testing.T) { 219 // Setup containerd and crictl. 220 crictl, cleanup, err := setup(t) 221 if err != nil { 222 t.Fatalf("failed to setup crictl: %v", err) 223 } 224 defer cleanup() 225 226 // Note that container ID returned here is a sub-container. All Pod 227 // containers are sub-containers. The root container of the sandbox is the 228 // pause container. 229 t.Run("sub-container", func(t *testing.T) { 230 contSpec := SimpleSpec("subcontainer", "basic/busybox", []string{"sh", "-c", "echo $HOME"}, nil) 231 podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("subcont-sandbox"), contSpec) 232 if err != nil { 233 t.Fatalf("start failed: %v", err) 234 } 235 236 out, err := crictl.Logs(contID) 237 if err != nil { 238 t.Fatalf("failed retrieving container logs: %v, out: %s", err, out) 239 } 240 if got, want := strings.TrimSpace(string(out)), "/root"; got != want { 241 t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want) 242 } 243 244 // Stop everything; note that the pod may have already stopped. 245 crictl.StopPodAndContainer(podID, contID) 246 }) 247 248 // Tests that HOME is set for the exec process. 249 t.Run("exec", func(t *testing.T) { 250 contSpec := SimpleSpec("exec", "basic/busybox", []string{"sleep", "1000"}, nil) 251 podID, contID, err := crictl.StartPodAndContainer(containerdRuntime, "basic/busybox", Sandbox("exec-sandbox"), contSpec) 252 if err != nil { 253 t.Fatalf("start failed: %v", err) 254 } 255 256 out, err := crictl.Exec(contID, "sh", "-c", "echo $HOME") 257 if err != nil { 258 t.Fatalf("failed retrieving container logs: %v, out: %s", err, out) 259 } 260 if got, want := strings.TrimSpace(string(out)), "/root"; got != want { 261 t.Fatalf("Home directory invalid. Got %q, Want : %q", got, want) 262 } 263 264 // Stop everything. 265 if err := crictl.StopPodAndContainer(podID, contID); err != nil { 266 t.Fatalf("stop failed: %v", err) 267 } 268 }) 269 } 270 271 const containerdRuntime = "runsc" 272 273 // containerdConfigv14 is the containerd (1.4-) configuration file that 274 // configures the gVisor shim. 275 // 276 // Note that the v2 shim binary name must be containerd-shim-<runtime>-v1. 277 const containerdConfigv14 = ` 278 disabled_plugins = ["restart"] 279 [plugins.cri] 280 disable_tcp_service = true 281 [plugins.linux] 282 shim_debug = true 283 [plugins.cri.containerd.runtimes.` + containerdRuntime + `] 284 runtime_type = "io.containerd.` + containerdRuntime + `.v1" 285 [plugins.cri.containerd.runtimes.` + containerdRuntime + `.options] 286 TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options" 287 ` 288 289 // containerdConfig is the containerd (1.5+) configuration file that 290 // configures the gVisor shim. 291 // 292 // Note that the v2 shim binary name must be containerd-shim-<runtime>-v1. 293 const containerdConfig = ` 294 version=2 295 disabled_plugins = ["io.containerd.internal.v1.restart"] 296 [plugins."io.containerd.grpc.v1.cri"] 297 disable_tcp_service = true 298 [plugins."io.containerd.runtime.v1.linux"] 299 shim_debug = true 300 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] 301 runtime_type = "io.containerd.runc.v2" 302 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.` + containerdRuntime + `] 303 runtime_type = "io.containerd.` + containerdRuntime + `.v1" 304 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.` + containerdRuntime + `.options] 305 TypeUrl = "io.containerd.` + containerdRuntime + `.v1.options" 306 ` 307 308 // setup sets up before a test. Specifically it: 309 // - Creates directories and a socket for containerd to utilize. 310 // - Runs containerd and waits for it to reach a "ready" state for testing. 311 // - Returns a cleanup function that should be called at the end of the test. 312 func setup(t *testing.T) (*criutil.Crictl, func(), error) { 313 // Create temporary containerd root and state directories, and a socket 314 // via which crictl and containerd communicate. 315 containerdRoot, err := ioutil.TempDir(testutil.TmpDir(), "containerd-root") 316 if err != nil { 317 t.Fatalf("failed to create containerd root: %v", err) 318 } 319 cu := cleanup.Make(func() { os.RemoveAll(containerdRoot) }) 320 defer cu.Clean() 321 t.Logf("Using containerd root: %s", containerdRoot) 322 323 containerdState, err := ioutil.TempDir(testutil.TmpDir(), "containerd-state") 324 if err != nil { 325 t.Fatalf("failed to create containerd state: %v", err) 326 } 327 cu.Add(func() { os.RemoveAll(containerdState) }) 328 t.Logf("Using containerd state: %s", containerdState) 329 330 sockDir, err := ioutil.TempDir(testutil.TmpDir(), "containerd-sock") 331 if err != nil { 332 t.Fatalf("failed to create containerd socket directory: %v", err) 333 } 334 cu.Add(func() { os.RemoveAll(sockDir) }) 335 sockAddr := path.Join(sockDir, "test.sock") 336 t.Logf("Using containerd socket: %s", sockAddr) 337 338 // Extract the containerd version. 339 versionCmd := exec.Command(getContainerd(), "-v") 340 out, err := versionCmd.CombinedOutput() 341 if err != nil { 342 t.Fatalf("error extracting containerd version: %v (%s)", err, string(out)) 343 } 344 r := regexp.MustCompile(" v([0-9]+)\\.([0-9]+)\\.([0-9+])") 345 vs := r.FindStringSubmatch(string(out)) 346 if len(vs) != 4 { 347 t.Fatalf("error unexpected version string: %s", string(out)) 348 } 349 major, err := strconv.ParseUint(vs[1], 10, 64) 350 if err != nil { 351 t.Fatalf("error parsing containerd major version: %v (%s)", err, string(out)) 352 } 353 minor, err := strconv.ParseUint(vs[2], 10, 64) 354 if err != nil { 355 t.Fatalf("error parsing containerd minor version: %v (%s)", err, string(out)) 356 } 357 t.Logf("Using containerd version: %d.%d", major, minor) 358 359 // Check if containerd supports shim v2. 360 if major < 1 || (major == 1 && minor <= 1) { 361 t.Skipf("skipping incompatible containerd (want at least 1.2, got %d.%d)", major, minor) 362 } 363 364 // We rewrite a configuration. This is based on the current docker 365 // configuration for the runtime under test. 366 runtime, err := dockerutil.RuntimePath() 367 if err != nil { 368 t.Fatalf("error discovering runtime path: %v", err) 369 } 370 t.Logf("Using runtime: %v", runtime) 371 372 // Construct a PATH that includes the runtime directory. This is 373 // because the shims will be installed there, and containerd may infer 374 // the binary name and search the PATH. 375 runtimeDir := path.Dir(runtime) 376 modifiedPath, ok := os.LookupEnv("PATH") 377 if ok { 378 modifiedPath = ":" + modifiedPath // We prepend below. 379 } 380 modifiedPath = path.Dir(getContainerd()) + modifiedPath 381 modifiedPath = runtimeDir + ":" + modifiedPath 382 t.Logf("Using PATH: %v", modifiedPath) 383 384 // Generate the configuration for the test. 385 config := getContainerdConfig(major, minor) 386 t.Logf("Using config: %s", config) 387 configFile, configCleanup, err := testutil.WriteTmpFile("containerd-config", config) 388 if err != nil { 389 t.Fatalf("failed to write containerd config") 390 } 391 cu.Add(configCleanup) 392 393 // Start containerd. 394 args := []string{ 395 getContainerd(), 396 "--config", configFile, 397 "--log-level", "debug", 398 "--root", containerdRoot, 399 "--state", containerdState, 400 "--address", sockAddr, 401 } 402 t.Logf("Using args: %s", strings.Join(args, " ")) 403 cmd := exec.Command(args[0], args[1:]...) 404 cmd.Env = append(os.Environ(), "PATH="+modifiedPath) 405 406 // Include output in logs. 407 stderrPipe, err := cmd.StderrPipe() 408 if err != nil { 409 t.Fatalf("failed to create stderr pipe: %v", err) 410 } 411 cu.Add(func() { stderrPipe.Close() }) 412 stdoutPipe, err := cmd.StdoutPipe() 413 if err != nil { 414 t.Fatalf("failed to create stdout pipe: %v", err) 415 } 416 cu.Add(func() { stdoutPipe.Close() }) 417 var ( 418 wg sync.WaitGroup 419 stderr bytes.Buffer 420 stdout bytes.Buffer 421 ) 422 startupR, startupW := io.Pipe() 423 wg.Add(2) 424 go func() { 425 defer wg.Done() 426 io.Copy(io.MultiWriter(startupW, &stderr), stderrPipe) 427 }() 428 go func() { 429 defer wg.Done() 430 io.Copy(io.MultiWriter(startupW, &stdout), stdoutPipe) 431 }() 432 cu.Add(func() { 433 wg.Wait() 434 t.Logf("containerd stdout: %s", stdout.String()) 435 t.Logf("containerd stderr: %s", stderr.String()) 436 }) 437 438 // Start the process. 439 if err := cmd.Start(); err != nil { 440 t.Fatalf("failed running containerd: %v", err) 441 } 442 443 // Wait for containerd to boot. 444 if err := testutil.WaitUntilRead(startupR, "Start streaming server", 10*time.Second); err != nil { 445 t.Fatalf("failed to start containerd: %v", err) 446 } 447 448 // Discard all subsequent data. 449 go io.Copy(ioutil.Discard, startupR) 450 451 // Create the crictl interface. 452 cc := criutil.NewCrictl(t, sockAddr) 453 cu.Add(cc.CleanUp) 454 455 // Kill must be the last cleanup (as it will be executed first). 456 cu.Add(func() { 457 // Best effort: ignore errors. 458 testutil.KillCommand(cmd) 459 }) 460 461 return cc, cu.Release(), nil 462 } 463 464 // httpGet GETs the contents of a file served from a pod on port 80. 465 func httpGet(crictl *criutil.Crictl, podID, filePath string) error { 466 // Get the IP of the httpd server. 467 ip, err := crictl.PodIP(podID) 468 if err != nil { 469 return fmt.Errorf("failed to get IP from pod %q: %v", podID, err) 470 } 471 472 // GET the page. We may be waiting for the server to start, so retry 473 // with a timeout. 474 var resp *http.Response 475 cb := func() error { 476 r, err := http.Get(fmt.Sprintf("http://%s", path.Join(ip, filePath))) 477 resp = r 478 return err 479 } 480 if err := testutil.Poll(cb, 20*time.Second); err != nil { 481 return err 482 } 483 defer resp.Body.Close() 484 485 if resp.StatusCode != 200 { 486 return fmt.Errorf("bad status returned: %d", resp.StatusCode) 487 } 488 return nil 489 } 490 491 func getContainerd() string { 492 // Use the local path if it exists, otherwise, use the system one. 493 if _, err := os.Stat("/usr/local/bin/containerd"); err == nil { 494 return "/usr/local/bin/containerd" 495 } 496 return "/usr/bin/containerd" 497 } 498 499 func getContainerdConfig(major, minor uint64) string { 500 if major == 1 && minor <= 4 { 501 return containerdConfigv14 502 } 503 return containerdConfig 504 }