github.com/rawahars/moby@v24.0.4+incompatible/integration-cli/docker_cli_exec_test.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "os" 8 "os/exec" 9 "reflect" 10 "runtime" 11 "sort" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/docker/docker/client" 18 "github.com/docker/docker/integration-cli/cli" 19 "github.com/docker/docker/integration-cli/cli/build" 20 "gotest.tools/v3/assert" 21 is "gotest.tools/v3/assert/cmp" 22 "gotest.tools/v3/icmd" 23 ) 24 25 type DockerCLIExecSuite struct { 26 ds *DockerSuite 27 } 28 29 func (s *DockerCLIExecSuite) TearDownTest(c *testing.T) { 30 s.ds.TearDownTest(c) 31 } 32 33 func (s *DockerCLIExecSuite) OnTimeout(c *testing.T) { 34 s.ds.OnTimeout(c) 35 } 36 37 func (s *DockerCLIExecSuite) TestExec(c *testing.T) { 38 testRequires(c, DaemonIsLinux) 39 out, _ := dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && top") 40 assert.NilError(c, waitRun(strings.TrimSpace(out))) 41 42 out, _ = dockerCmd(c, "exec", "testing", "cat", "/tmp/file") 43 assert.Equal(c, strings.Trim(out, "\r\n"), "test") 44 } 45 46 func (s *DockerCLIExecSuite) TestExecInteractive(c *testing.T) { 47 testRequires(c, DaemonIsLinux) 48 dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "sh", "-c", "echo test > /tmp/file && top") 49 50 execCmd := exec.Command(dockerBinary, "exec", "-i", "testing", "sh") 51 stdin, err := execCmd.StdinPipe() 52 assert.NilError(c, err) 53 stdout, err := execCmd.StdoutPipe() 54 assert.NilError(c, err) 55 56 err = execCmd.Start() 57 assert.NilError(c, err) 58 _, err = stdin.Write([]byte("cat /tmp/file\n")) 59 assert.NilError(c, err) 60 61 r := bufio.NewReader(stdout) 62 line, err := r.ReadString('\n') 63 assert.NilError(c, err) 64 line = strings.TrimSpace(line) 65 assert.Equal(c, line, "test") 66 err = stdin.Close() 67 assert.NilError(c, err) 68 errChan := make(chan error, 1) 69 go func() { 70 errChan <- execCmd.Wait() 71 close(errChan) 72 }() 73 select { 74 case err := <-errChan: 75 assert.NilError(c, err) 76 case <-time.After(1 * time.Second): 77 c.Fatal("docker exec failed to exit on stdin close") 78 } 79 } 80 81 func (s *DockerCLIExecSuite) TestExecAfterContainerRestart(c *testing.T) { 82 out := runSleepingContainer(c) 83 cleanedContainerID := strings.TrimSpace(out) 84 assert.NilError(c, waitRun(cleanedContainerID)) 85 dockerCmd(c, "restart", cleanedContainerID) 86 assert.NilError(c, waitRun(cleanedContainerID)) 87 88 out, _ = dockerCmd(c, "exec", cleanedContainerID, "echo", "hello") 89 assert.Equal(c, strings.TrimSpace(out), "hello") 90 } 91 92 func (s *DockerDaemonSuite) TestExecAfterDaemonRestart(c *testing.T) { 93 // TODO Windows CI: DockerDaemonSuite doesn't run on Windows, and requires a little work to get this ported. 94 s.d.StartWithBusybox(c) 95 96 out, err := s.d.Cmd("run", "-d", "--name", "top", "-p", "80", "busybox:latest", "top") 97 assert.NilError(c, err, "Could not run top: %s", out) 98 99 s.d.Restart(c) 100 101 out, err = s.d.Cmd("start", "top") 102 assert.NilError(c, err, "Could not start top after daemon restart: %s", out) 103 104 out, err = s.d.Cmd("exec", "top", "echo", "hello") 105 assert.NilError(c, err, "Could not exec on container top: %s", out) 106 assert.Equal(c, strings.TrimSpace(out), "hello") 107 } 108 109 // Regression test for #9155, #9044 110 func (s *DockerCLIExecSuite) TestExecEnv(c *testing.T) { 111 // TODO Windows CI: This one is interesting and may just end up being a feature 112 // difference between Windows and Linux. On Windows, the environment is passed 113 // into the process that is launched, not into the machine environment. Hence 114 // a subsequent exec will not have LALA set/ 115 testRequires(c, DaemonIsLinux) 116 runSleepingContainer(c, "-e", "LALA=value1", "-e", "LALA=value2", "-d", "--name", "testing") 117 assert.NilError(c, waitRun("testing")) 118 119 out, _ := dockerCmd(c, "exec", "testing", "env") 120 assert.Check(c, !strings.Contains(out, "LALA=value1")) 121 assert.Check(c, strings.Contains(out, "LALA=value2")) 122 assert.Check(c, strings.Contains(out, "HOME=/root")) 123 } 124 125 func (s *DockerCLIExecSuite) TestExecSetEnv(c *testing.T) { 126 testRequires(c, DaemonIsLinux) 127 runSleepingContainer(c, "-e", "HOME=/root", "-d", "--name", "testing") 128 assert.NilError(c, waitRun("testing")) 129 130 out, _ := dockerCmd(c, "exec", "-e", "HOME=/another", "-e", "ABC=xyz", "testing", "env") 131 assert.Check(c, !strings.Contains(out, "HOME=/root")) 132 assert.Check(c, strings.Contains(out, "HOME=/another")) 133 assert.Check(c, strings.Contains(out, "ABC=xyz")) 134 } 135 136 func (s *DockerCLIExecSuite) TestExecExitStatus(c *testing.T) { 137 runSleepingContainer(c, "-d", "--name", "top") 138 139 result := icmd.RunCommand(dockerBinary, "exec", "top", "sh", "-c", "exit 23") 140 result.Assert(c, icmd.Expected{ExitCode: 23, Error: "exit status 23"}) 141 } 142 143 func (s *DockerCLIExecSuite) TestExecPausedContainer(c *testing.T) { 144 testRequires(c, IsPausable) 145 146 out := runSleepingContainer(c, "-d", "--name", "testing") 147 ContainerID := strings.TrimSpace(out) 148 149 dockerCmd(c, "pause", "testing") 150 out, _, err := dockerCmdWithError("exec", ContainerID, "echo", "hello") 151 assert.ErrorContains(c, err, "", "container should fail to exec new command if it is paused") 152 153 expected := ContainerID + " is paused, unpause the container before exec" 154 assert.Assert(c, is.Contains(out, expected), "container should not exec new command if it is paused") 155 } 156 157 // regression test for #9476 158 func (s *DockerCLIExecSuite) TestExecTTYCloseStdin(c *testing.T) { 159 // TODO Windows CI: This requires some work to port to Windows. 160 testRequires(c, DaemonIsLinux) 161 dockerCmd(c, "run", "-d", "-it", "--name", "exec_tty_stdin", "busybox") 162 163 cmd := exec.Command(dockerBinary, "exec", "-i", "exec_tty_stdin", "cat") 164 stdinRw, err := cmd.StdinPipe() 165 assert.NilError(c, err) 166 167 stdinRw.Write([]byte("test")) 168 stdinRw.Close() 169 170 out, _, err := runCommandWithOutput(cmd) 171 assert.NilError(c, err, out) 172 173 out, _ = dockerCmd(c, "top", "exec_tty_stdin") 174 outArr := strings.Split(out, "\n") 175 assert.Assert(c, len(outArr) <= 3, "exec process left running") 176 assert.Assert(c, !strings.Contains(out, "nsenter-exec")) 177 } 178 179 func (s *DockerCLIExecSuite) TestExecTTYWithoutStdin(c *testing.T) { 180 out, _ := dockerCmd(c, "run", "-d", "-ti", "busybox") 181 id := strings.TrimSpace(out) 182 assert.NilError(c, waitRun(id)) 183 184 errChan := make(chan error, 1) 185 go func() { 186 defer close(errChan) 187 188 cmd := exec.Command(dockerBinary, "exec", "-ti", id, "true") 189 if _, err := cmd.StdinPipe(); err != nil { 190 errChan <- err 191 return 192 } 193 194 expected := "the input device is not a TTY" 195 if runtime.GOOS == "windows" { 196 expected += ". If you are using mintty, try prefixing the command with 'winpty'" 197 } 198 if out, _, err := runCommandWithOutput(cmd); err == nil { 199 errChan <- fmt.Errorf("exec should have failed") 200 return 201 } else if !strings.Contains(out, expected) { 202 errChan <- fmt.Errorf("exec failed with error %q: expected %q", out, expected) 203 return 204 } 205 }() 206 207 select { 208 case err := <-errChan: 209 assert.NilError(c, err) 210 case <-time.After(3 * time.Second): 211 c.Fatal("exec is running but should have failed") 212 } 213 } 214 215 // FIXME(vdemeester) this should be a unit tests on cli/command/container package 216 func (s *DockerCLIExecSuite) TestExecParseError(c *testing.T) { 217 // TODO Windows CI: Requires some extra work. Consider copying the 218 // runSleepingContainer helper to have an exec version. 219 testRequires(c, DaemonIsLinux) 220 dockerCmd(c, "run", "-d", "--name", "top", "busybox", "top") 221 222 // Test normal (non-detached) case first 223 icmd.RunCommand(dockerBinary, "exec", "top").Assert(c, icmd.Expected{ 224 ExitCode: 1, 225 Error: "exit status 1", 226 Err: "See 'docker exec --help'", 227 }) 228 } 229 230 func (s *DockerCLIExecSuite) TestExecStopNotHanging(c *testing.T) { 231 // TODO Windows CI: Requires some extra work. Consider copying the 232 // runSleepingContainer helper to have an exec version. 233 testRequires(c, DaemonIsLinux) 234 dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") 235 236 result := icmd.StartCmd(icmd.Command(dockerBinary, "exec", "testing", "top")) 237 result.Assert(c, icmd.Success) 238 go icmd.WaitOnCmd(0, result) 239 240 type dstop struct { 241 out string 242 err error 243 } 244 ch := make(chan dstop, 1) 245 go func() { 246 result := icmd.RunCommand(dockerBinary, "stop", "testing") 247 ch <- dstop{result.Combined(), result.Error} 248 close(ch) 249 }() 250 select { 251 case <-time.After(3 * time.Second): 252 c.Fatal("Container stop timed out") 253 case s := <-ch: 254 assert.NilError(c, s.err) 255 } 256 } 257 258 func (s *DockerCLIExecSuite) TestExecCgroup(c *testing.T) { 259 // Not applicable on Windows - using Linux specific functionality 260 testRequires(c, NotUserNamespace) 261 testRequires(c, DaemonIsLinux) 262 dockerCmd(c, "run", "-d", "--name", "testing", "busybox", "top") 263 264 out, _ := dockerCmd(c, "exec", "testing", "cat", "/proc/1/cgroup") 265 containerCgroups := sort.StringSlice(strings.Split(out, "\n")) 266 267 var wg sync.WaitGroup 268 var mu sync.Mutex 269 var execCgroups []sort.StringSlice 270 errChan := make(chan error, 5) 271 // exec a few times concurrently to get consistent failure 272 for i := 0; i < 5; i++ { 273 wg.Add(1) 274 go func() { 275 defer wg.Done() 276 out, _, err := dockerCmdWithError("exec", "testing", "cat", "/proc/self/cgroup") 277 if err != nil { 278 errChan <- err 279 return 280 } 281 cg := sort.StringSlice(strings.Split(out, "\n")) 282 283 mu.Lock() 284 execCgroups = append(execCgroups, cg) 285 mu.Unlock() 286 }() 287 } 288 wg.Wait() 289 close(errChan) 290 291 for err := range errChan { 292 assert.NilError(c, err) 293 } 294 295 for _, cg := range execCgroups { 296 if !reflect.DeepEqual(cg, containerCgroups) { 297 fmt.Println("exec cgroups:") 298 for _, name := range cg { 299 fmt.Printf(" %s\n", name) 300 } 301 302 fmt.Println("container cgroups:") 303 for _, name := range containerCgroups { 304 fmt.Printf(" %s\n", name) 305 } 306 c.Fatal("cgroups mismatched") 307 } 308 } 309 } 310 311 func (s *DockerCLIExecSuite) TestExecInspectID(c *testing.T) { 312 out := runSleepingContainer(c, "-d") 313 id := strings.TrimSuffix(out, "\n") 314 315 out = inspectField(c, id, "ExecIDs") 316 assert.Equal(c, out, "[]", "ExecIDs should be empty, got: %s", out) 317 318 // Start an exec, have it block waiting so we can do some checking 319 cmd := exec.Command(dockerBinary, "exec", id, "sh", "-c", 320 "while ! test -e /execid1; do sleep 1; done") 321 322 err := cmd.Start() 323 assert.NilError(c, err, "failed to start the exec cmd") 324 325 // Give the exec 10 chances/seconds to start then give up and stop the test 326 tries := 10 327 for i := 0; i < tries; i++ { 328 // Since its still running we should see exec as part of the container 329 out = strings.TrimSpace(inspectField(c, id, "ExecIDs")) 330 331 if out != "[]" && out != "<no value>" { 332 break 333 } 334 assert.Check(c, i+1 != tries, "ExecIDs still empty after 10 second") 335 time.Sleep(1 * time.Second) 336 } 337 338 // Save execID for later 339 execID, err := inspectFilter(id, "index .ExecIDs 0") 340 assert.NilError(c, err, "failed to get the exec id") 341 342 // End the exec by creating the missing file 343 err = exec.Command(dockerBinary, "exec", id, "sh", "-c", "touch /execid1").Run() 344 assert.NilError(c, err, "failed to run the 2nd exec cmd") 345 346 // Wait for 1st exec to complete 347 cmd.Wait() 348 349 // Give the exec 10 chances/seconds to stop then give up and stop the test 350 for i := 0; i < tries; i++ { 351 // Since its still running we should see exec as part of the container 352 out = strings.TrimSpace(inspectField(c, id, "ExecIDs")) 353 354 if out == "[]" { 355 break 356 } 357 assert.Check(c, i+1 != tries, "ExecIDs still empty after 10 second") 358 time.Sleep(1 * time.Second) 359 } 360 361 // But we should still be able to query the execID 362 apiClient, err := client.NewClientWithOpts(client.FromEnv) 363 assert.NilError(c, err) 364 defer apiClient.Close() 365 366 _, err = apiClient.ContainerExecInspect(context.Background(), execID) 367 assert.NilError(c, err) 368 369 // Now delete the container and then an 'inspect' on the exec should 370 // result in a 404 (not 'container not running') 371 out, ec := dockerCmd(c, "rm", "-f", id) 372 assert.Equal(c, ec, 0, "error removing container: %s", out) 373 374 _, err = apiClient.ContainerExecInspect(context.Background(), execID) 375 assert.ErrorContains(c, err, "No such exec instance") 376 } 377 378 func (s *DockerCLIExecSuite) TestLinksPingLinkedContainersOnRename(c *testing.T) { 379 // Problematic on Windows as Windows does not support links 380 testRequires(c, DaemonIsLinux) 381 var out string 382 out, _ = dockerCmd(c, "run", "-d", "--name", "container1", "busybox", "top") 383 idA := strings.TrimSpace(out) 384 assert.Assert(c, idA != "", "%s, id should not be nil", out) 385 out, _ = dockerCmd(c, "run", "-d", "--link", "container1:alias1", "--name", "container2", "busybox", "top") 386 idB := strings.TrimSpace(out) 387 assert.Assert(c, idB != "", "%s, id should not be nil", out) 388 389 dockerCmd(c, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") 390 dockerCmd(c, "rename", "container1", "container_new") 391 dockerCmd(c, "exec", "container2", "ping", "-c", "1", "alias1", "-W", "1") 392 } 393 394 func (s *DockerCLIExecSuite) TestRunMutableNetworkFiles(c *testing.T) { 395 // Not applicable on Windows to Windows CI. 396 testRequires(c, testEnv.IsLocalDaemon, DaemonIsLinux) 397 for _, fn := range []string{"resolv.conf", "hosts"} { 398 containers := cli.DockerCmd(c, "ps", "-q", "-a").Combined() 399 if containers != "" { 400 cli.DockerCmd(c, append([]string{"rm", "-fv"}, strings.Split(strings.TrimSpace(containers), "\n")...)...) 401 } 402 403 content := runCommandAndReadContainerFile(c, fn, dockerBinary, "run", "-d", "--name", "c1", "busybox", "sh", "-c", fmt.Sprintf("echo success >/etc/%s && top", fn)) 404 405 assert.Equal(c, strings.TrimSpace(string(content)), "success", "Content was not what was modified in the container", string(content)) 406 407 out, _ := dockerCmd(c, "run", "-d", "--name", "c2", "busybox", "top") 408 contID := strings.TrimSpace(out) 409 netFilePath := containerStorageFile(contID, fn) 410 411 f, err := os.OpenFile(netFilePath, os.O_WRONLY|os.O_SYNC|os.O_APPEND, 0644) 412 assert.NilError(c, err) 413 414 if _, err := f.Seek(0, 0); err != nil { 415 f.Close() 416 c.Fatal(err) 417 } 418 419 if err := f.Truncate(0); err != nil { 420 f.Close() 421 c.Fatal(err) 422 } 423 424 if _, err := f.Write([]byte("success2\n")); err != nil { 425 f.Close() 426 c.Fatal(err) 427 } 428 f.Close() 429 430 res, _ := dockerCmd(c, "exec", contID, "cat", "/etc/"+fn) 431 assert.Equal(c, res, "success2\n") 432 } 433 } 434 435 func (s *DockerCLIExecSuite) TestExecWithUser(c *testing.T) { 436 // TODO Windows CI: This may be fixable in the future once Windows 437 // supports users 438 testRequires(c, DaemonIsLinux) 439 dockerCmd(c, "run", "-d", "--name", "parent", "busybox", "top") 440 441 out, _ := dockerCmd(c, "exec", "-u", "1", "parent", "id") 442 assert.Assert(c, strings.Contains(out, "uid=1(daemon) gid=1(daemon)")) 443 444 out, _ = dockerCmd(c, "exec", "-u", "root", "parent", "id") 445 assert.Assert(c, strings.Contains(out, "uid=0(root) gid=0(root)"), "exec with user by id expected daemon user got %s", out) 446 } 447 448 func (s *DockerCLIExecSuite) TestExecWithPrivileged(c *testing.T) { 449 // Not applicable on Windows 450 testRequires(c, DaemonIsLinux, NotUserNamespace) 451 // Start main loop which attempts mknod repeatedly 452 dockerCmd(c, "run", "-d", "--name", "parent", "--cap-drop=ALL", "busybox", "sh", "-c", `while (true); do if [ -e /exec_priv ]; then cat /exec_priv && mknod /tmp/sda b 8 0 && echo "Success"; else echo "Privileged exec has not run yet"; fi; usleep 10000; done`) 453 454 // Check exec mknod doesn't work 455 icmd.RunCommand(dockerBinary, "exec", "parent", "sh", "-c", "mknod /tmp/sdb b 8 16").Assert(c, icmd.Expected{ 456 ExitCode: 1, 457 Err: "Operation not permitted", 458 }) 459 460 // Check exec mknod does work with --privileged 461 result := icmd.RunCommand(dockerBinary, "exec", "--privileged", "parent", "sh", "-c", `echo "Running exec --privileged" > /exec_priv && mknod /tmp/sdb b 8 16 && usleep 50000 && echo "Finished exec --privileged" > /exec_priv && echo ok`) 462 result.Assert(c, icmd.Success) 463 464 actual := strings.TrimSpace(result.Combined()) 465 assert.Equal(c, actual, "ok", "exec mknod in --cap-drop=ALL container with --privileged failed, output: %q", result.Combined()) 466 467 // Check subsequent unprivileged exec cannot mknod 468 icmd.RunCommand(dockerBinary, "exec", "parent", "sh", "-c", "mknod /tmp/sdc b 8 32").Assert(c, icmd.Expected{ 469 ExitCode: 1, 470 Err: "Operation not permitted", 471 }) 472 // Confirm at no point was mknod allowed 473 result = icmd.RunCommand(dockerBinary, "logs", "parent") 474 result.Assert(c, icmd.Success) 475 assert.Assert(c, !strings.Contains(result.Combined(), "Success")) 476 } 477 478 func (s *DockerCLIExecSuite) TestExecWithImageUser(c *testing.T) { 479 // Not applicable on Windows 480 testRequires(c, DaemonIsLinux) 481 name := "testbuilduser" 482 buildImageSuccessfully(c, name, build.WithDockerfile(`FROM busybox 483 RUN echo 'dockerio:x:1001:1001::/bin:/bin/false' >> /etc/passwd 484 USER dockerio`)) 485 dockerCmd(c, "run", "-d", "--name", "dockerioexec", name, "top") 486 487 out, _ := dockerCmd(c, "exec", "dockerioexec", "whoami") 488 assert.Assert(c, strings.Contains(out, "dockerio"), "exec with user by id expected dockerio user got %s", out) 489 } 490 491 func (s *DockerCLIExecSuite) TestExecOnReadonlyContainer(c *testing.T) { 492 // Windows does not support read-only 493 // --read-only + userns has remount issues 494 testRequires(c, DaemonIsLinux, NotUserNamespace) 495 dockerCmd(c, "run", "-d", "--read-only", "--name", "parent", "busybox", "top") 496 dockerCmd(c, "exec", "parent", "true") 497 } 498 499 func (s *DockerCLIExecSuite) TestExecUlimits(c *testing.T) { 500 testRequires(c, DaemonIsLinux) 501 name := "testexeculimits" 502 runSleepingContainer(c, "-d", "--ulimit", "nofile=511:511", "--name", name) 503 assert.NilError(c, waitRun(name)) 504 505 out, _, err := dockerCmdWithError("exec", name, "sh", "-c", "ulimit -n") 506 assert.NilError(c, err) 507 assert.Equal(c, strings.TrimSpace(out), "511") 508 } 509 510 // #15750 511 func (s *DockerCLIExecSuite) TestExecStartFails(c *testing.T) { 512 name := "exec-15750" 513 runSleepingContainer(c, "-d", "--name", name) 514 assert.NilError(c, waitRun(name)) 515 516 out, _, err := dockerCmdWithError("exec", name, "no-such-cmd") 517 assert.ErrorContains(c, err, "", out) 518 519 expectedMsg := "executable file not found" 520 if DaemonIsWindows() { 521 expectedMsg = "The system cannot find the file specified" 522 } 523 assert.Assert(c, is.Contains(out, expectedMsg)) 524 } 525 526 // Fix regression in https://github.com/docker/docker/pull/26461#issuecomment-250287297 527 func (s *DockerCLIExecSuite) TestExecWindowsPathNotWiped(c *testing.T) { 528 testRequires(c, DaemonIsWindows) 529 out, _ := dockerCmd(c, "run", "-d", "--name", "testing", minimalBaseImage(), "powershell", "start-sleep", "60") 530 assert.NilError(c, waitRun(strings.TrimSpace(out))) 531 532 out, _ = dockerCmd(c, "exec", "testing", "powershell", "write-host", "$env:PATH") 533 out = strings.ToLower(strings.Trim(out, "\r\n")) 534 assert.Assert(c, strings.Contains(out, `windowspowershell\v1.0`)) 535 } 536 537 func (s *DockerCLIExecSuite) TestExecEnvLinksHost(c *testing.T) { 538 testRequires(c, DaemonIsLinux) 539 runSleepingContainer(c, "-d", "--name", "foo") 540 runSleepingContainer(c, "-d", "--link", "foo:db", "--hostname", "myhost", "--name", "bar") 541 out, _ := dockerCmd(c, "exec", "bar", "env") 542 assert.Check(c, is.Contains(out, "HOSTNAME=myhost")) 543 assert.Check(c, is.Contains(out, "DB_NAME=/bar/db")) 544 }