From 099ae9b4b02ea43d2c09a20a4da73d3d52962823 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 8 Dec 2017 15:13:43 -0800 Subject: [PATCH 1/6] Disallow using legacy (V1) registries Interacting with v1 registries was deprecated in Docker 1.8.3, disabled by default in Docker 17.06, and scheduled for removal in Docker 17.12. This patch disallows enabling V1 registry through the `--disable-legacy-registry` option, and the `"disable-legacy-registry": false` option in the daemon configuration file. The actual V1 registry code is still in place, and will be removed separately. With this patch applied: $ dockerd --disable-legacy-registry=false ERROR: The '--disable-legacy-registry' flag has been removed. Interacting with legacy (v1) registries is no longer supported Or, when setting through the `daemon.json` configuration file $ mkdir -p /etc/docker/ $ echo '{"disable-legacy-registry":false}' > /etc/docker/daemon.json $ dockerd ERROR: The 'disable-legacy-registry' configuration option has been removed. Interacting with legacy (v1) registries is no longer supported Signed-off-by: Sebastiaan van Stijn Upstream-commit: 8d6df8a0addc9a37b48c5a1827dd3f65f2ed57cf Component: engine --- components/engine/cmd/dockerd/config.go | 2 + components/engine/cmd/dockerd/daemon.go | 12 +++- .../engine/cmd/dockerd/daemon_unix_test.go | 12 ---- .../integration-cli/docker_cli_logout_test.go | 6 +- .../integration-cli/docker_cli_pull_test.go | 12 ---- .../docker_cli_v2_only_test.go | 64 +------------------ 6 files changed, 15 insertions(+), 93 deletions(-) diff --git a/components/engine/cmd/dockerd/config.go b/components/engine/cmd/dockerd/config.go index c4ae197335..c7da6ee4f0 100644 --- a/components/engine/cmd/dockerd/config.go +++ b/components/engine/cmd/dockerd/config.go @@ -92,6 +92,8 @@ func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag. flags.Var(insecureRegistries, "insecure-registry", "Enable insecure registry communication") if runtime.GOOS != "windows" { + // TODO: Remove this flag after 3 release cycles (18.03) flags.BoolVar(&options.V2Only, "disable-legacy-registry", true, "Disable contacting legacy registries") + flags.MarkHidden("disable-legacy-registry") } } diff --git a/components/engine/cmd/dockerd/daemon.go b/components/engine/cmd/dockerd/daemon.go index 02a03141df..d73b63a0f0 100644 --- a/components/engine/cmd/dockerd/daemon.go +++ b/components/engine/cmd/dockerd/daemon.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "time" @@ -472,8 +473,15 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { return nil, err } - if !conf.V2Only { - logrus.Warnf(`The "disable-legacy-registry" option is deprecated and wil be removed in Docker v17.12. Interacting with legacy (v1) registries will no longer be supported in Docker v17.12"`) + if runtime.GOOS != "windows" { + if flags.Changed("disable-legacy-registry") { + // TODO: Remove this error after 3 release cycles (18.03) + return nil, errors.New("ERROR: The '--disable-legacy-registry' flag has been removed. Interacting with legacy (v1) registries is no longer supported") + } + if !conf.V2Only { + // TODO: Remove this error after 3 release cycles (18.03) + return nil, errors.New("ERROR: The 'disable-legacy-registry' configuration option has been removed. Interacting with legacy (v1) registries is no longer supported") + } } if flags.Changed("graph") { diff --git a/components/engine/cmd/dockerd/daemon_unix_test.go b/components/engine/cmd/dockerd/daemon_unix_test.go index 475ff9efa7..41c392e1b1 100644 --- a/components/engine/cmd/dockerd/daemon_unix_test.go +++ b/components/engine/cmd/dockerd/daemon_unix_test.go @@ -97,15 +97,3 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) { assert.True(t, loadedConfig.EnableUserlandProxy) } - -func TestLoadDaemonConfigWithLegacyRegistryOptions(t *testing.T) { - content := `{"disable-legacy-registry": false}` - tempFile := fs.NewFile(t, "config", fs.WithContent(content)) - defer tempFile.Remove() - - opts := defaultOptions(tempFile.Path()) - loadedConfig, err := loadDaemonCliConfig(opts) - require.NoError(t, err) - require.NotNil(t, loadedConfig) - assert.False(t, loadedConfig.V2Only) -} diff --git a/components/engine/integration-cli/docker_cli_logout_test.go b/components/engine/integration-cli/docker_cli_logout_test.go index 5076ceba09..e0752f489c 100644 --- a/components/engine/integration-cli/docker_cli_logout_test.go +++ b/components/engine/integration-cli/docker_cli_logout_test.go @@ -13,9 +13,7 @@ import ( ) func (s *DockerRegistryAuthHtpasswdSuite) TestLogoutWithExternalAuth(c *check.C) { - - // @TODO TestLogoutWithExternalAuth expects docker to fall back to a v1 registry, so has to be updated for v17.12, when v1 registries are no longer supported - s.d.StartWithBusybox(c, "--disable-legacy-registry=false") + s.d.StartWithBusybox(c) osPath := os.Getenv("PATH") defer os.Setenv("PATH", osPath) @@ -62,7 +60,7 @@ func (s *DockerRegistryAuthHtpasswdSuite) TestLogoutWithExternalAuth(c *check.C) // check I cannot pull anymore out, err := s.d.Cmd("--config", tmp, "pull", repoName) c.Assert(err, check.NotNil, check.Commentf(out)) - c.Assert(out, checker.Contains, "Error: image dockercli/busybox:authtest not found") + c.Assert(out, checker.Contains, "no basic auth credentials") } // #23100 diff --git a/components/engine/integration-cli/docker_cli_pull_test.go b/components/engine/integration-cli/docker_cli_pull_test.go index 613cdb311f..0e88b1e56f 100644 --- a/components/engine/integration-cli/docker_cli_pull_test.go +++ b/components/engine/integration-cli/docker_cli_pull_test.go @@ -259,18 +259,6 @@ func (s *DockerHubPullSuite) TestPullClientDisconnect(c *check.C) { c.Assert(err, checker.NotNil, check.Commentf("image was pulled after client disconnected")) } -func (s *DockerRegistryAuthHtpasswdSuite) TestPullNoCredentialsNotFound(c *check.C) { - // @TODO TestPullNoCredentialsNotFound expects docker to fall back to a v1 registry, so has to be updated for v17.12, when v1 registries are no longer supported - s.d.StartWithBusybox(c, "--disable-legacy-registry=false") - - // we don't care about the actual image, we just want to see image not found - // because that means v2 call returned 401 and we fell back to v1 which usually - // gives a 404 (in this case the test registry doesn't handle v1 at all) - out, err := s.d.Cmd("pull", privateRegistryURL+"/busybox") - c.Assert(err, check.NotNil, check.Commentf(out)) - c.Assert(out, checker.Contains, "Error: image busybox:latest not found") -} - // Regression test for https://github.com/docker/docker/issues/26429 func (s *DockerSuite) TestPullLinuxImageFailsOnWindows(c *check.C) { testRequires(c, DaemonIsWindows, Network) diff --git a/components/engine/integration-cli/docker_cli_v2_only_test.go b/components/engine/integration-cli/docker_cli_v2_only_test.go index b82cdbde1f..3757341025 100644 --- a/components/engine/integration-cli/docker_cli_v2_only_test.go +++ b/components/engine/integration-cli/docker_cli_v2_only_test.go @@ -22,7 +22,7 @@ func makefile(path string, contents string) (string, error) { return f.Name(), nil } -// TestV2Only ensures that a daemon by default does not +// TestV2Only ensures that a daemon does not // attempt to contact any v1 registry endpoints. func (s *DockerRegistrySuite) TestV2Only(c *check.C) { reg, err := registry.NewMock(c) @@ -56,65 +56,3 @@ func (s *DockerRegistrySuite) TestV2Only(c *check.C) { s.d.Cmd("push", repoName) s.d.Cmd("pull", repoName) } - -// TestV1 starts a daemon with legacy registries enabled -// and ensure v1 endpoints are hit for the following operations: -// login, push, pull, build & run -func (s *DockerRegistrySuite) TestV1(c *check.C) { - reg, err := registry.NewMock(c) - defer reg.Close() - c.Assert(err, check.IsNil) - - v2Pings := 0 - reg.RegisterHandler("/v2/", func(w http.ResponseWriter, r *http.Request) { - v2Pings++ - // V2 ping 404 causes fallback to v1 - w.WriteHeader(404) - }) - - v1Pings := 0 - reg.RegisterHandler("/v1/_ping", func(w http.ResponseWriter, r *http.Request) { - v1Pings++ - }) - - v1Logins := 0 - reg.RegisterHandler("/v1/users/", func(w http.ResponseWriter, r *http.Request) { - v1Logins++ - }) - - v1Repo := 0 - reg.RegisterHandler("/v1/repositories/busybox/", func(w http.ResponseWriter, r *http.Request) { - v1Repo++ - }) - - reg.RegisterHandler("/v1/repositories/busybox/images", func(w http.ResponseWriter, r *http.Request) { - v1Repo++ - }) - - s.d.Start(c, "--insecure-registry", reg.URL(), "--disable-legacy-registry=false") - - tmp, err := ioutil.TempDir("", "integration-cli-") - c.Assert(err, check.IsNil) - defer os.RemoveAll(tmp) - - dockerfileName, err := makefile(tmp, fmt.Sprintf("FROM %s/busybox", reg.URL())) - c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile")) - - s.d.Cmd("build", "--file", dockerfileName, tmp) - c.Assert(v1Repo, check.Equals, 1, check.Commentf("Expected v1 repository access after build")) - - repoName := fmt.Sprintf("%s/busybox", reg.URL()) - s.d.Cmd("run", repoName) - c.Assert(v1Repo, check.Equals, 2, check.Commentf("Expected v1 repository access after run")) - - s.d.Cmd("login", "-u", "richard", "-p", "testtest", reg.URL()) - c.Assert(v1Logins, check.Equals, 1, check.Commentf("Expected v1 login attempt")) - - s.d.Cmd("tag", "busybox", repoName) - s.d.Cmd("push", repoName) - - c.Assert(v1Repo, check.Equals, 2) - - s.d.Cmd("pull", repoName) - c.Assert(v1Repo, check.Equals, 3, check.Commentf("Expected v1 repository access after pull")) -} From ef0d4a2becb22764c5b72ee1fd4fa260c48f63b9 Mon Sep 17 00:00:00 2001 From: Vivek Goyal Date: Mon, 11 Dec 2017 13:27:14 -0500 Subject: [PATCH 2/6] devmapper: Log fstype and mount options during mount error Right now we only log source and destination (and demsg) if mount operation fails. fstype and mount options are available easily. It probably is a good idea to log these as well. Especially sometimes failures can happen due to mount options. Signed-off-by: Vivek Goyal Upstream-commit: f728d74ac5d185adaa5f1a88eadc71217806859f Component: engine --- components/engine/daemon/graphdriver/devmapper/deviceset.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/engine/daemon/graphdriver/devmapper/deviceset.go b/components/engine/daemon/graphdriver/devmapper/deviceset.go index db41f05097..6659878bc6 100644 --- a/components/engine/daemon/graphdriver/devmapper/deviceset.go +++ b/components/engine/daemon/graphdriver/devmapper/deviceset.go @@ -1201,7 +1201,7 @@ func (devices *DeviceSet) growFS(info *devInfo) error { options = joinMountOptions(options, devices.mountOptions) if err := mount.Mount(info.DevName(), fsMountPoint, devices.BaseDeviceFilesystem, options); err != nil { - return fmt.Errorf("Error mounting '%s' on '%s': %s\n%v", info.DevName(), fsMountPoint, err, string(dmesg.Dmesg(256))) + return fmt.Errorf("Error mounting '%s' on '%s' (fstype='%s' options='%s'): %s\n%v", info.DevName(), fsMountPoint, devices.BaseDeviceFilesystem, options, err, string(dmesg.Dmesg(256))) } defer unix.Unmount(fsMountPoint, unix.MNT_DETACH) @@ -2392,7 +2392,7 @@ func (devices *DeviceSet) MountDevice(hash, path, mountLabel string) error { options = joinMountOptions(options, label.FormatMountLabel("", mountLabel)) if err := mount.Mount(info.DevName(), path, fstype, options); err != nil { - return fmt.Errorf("devmapper: Error mounting '%s' on '%s': %s\n%v", info.DevName(), path, err, string(dmesg.Dmesg(256))) + return fmt.Errorf("devmapper: Error mounting '%s' on '%s' (fstype='%s' options='%s'): %s\n%v", info.DevName(), path, fstype, options, err, string(dmesg.Dmesg(256))) } if fstype == "xfs" && devices.xfsNospaceRetries != "" { From eae25b27ea92fed9785b2c184b5783b0e10eab3b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 11 Dec 2017 10:59:51 -0800 Subject: [PATCH 3/6] Fix typo in log-message Signed-off-by: Sebastiaan van Stijn Upstream-commit: 5c3418e38b9603e8ff582d53c2face57f0f01cce Component: engine --- components/engine/libcontainerd/remote_daemon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/engine/libcontainerd/remote_daemon.go b/components/engine/libcontainerd/remote_daemon.go index e6fd05f08a..609bcfba7a 100644 --- a/components/engine/libcontainerd/remote_daemon.go +++ b/components/engine/libcontainerd/remote_daemon.go @@ -278,7 +278,7 @@ func (r *remote) monitorConnection(client *containerd.Client) { select { case <-r.shutdownContext.Done(): - r.logger.Info("stopping healtcheck following graceful shutdown") + r.logger.Info("stopping healthcheck following graceful shutdown") client.Close() return default: From 6c564b67851940a7669e851a329cb2a96c867a88 Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Mon, 11 Dec 2017 15:52:50 -0500 Subject: [PATCH 4/6] Fix vendoring of go-gelf to point to specific commit ID Signed-off-by: Ghislain Bourgeois Upstream-commit: 5bc11021250e39e71ca9edf62ce5c33f15c3756f Component: engine --- components/engine/vendor.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/engine/vendor.conf b/components/engine/vendor.conf index 9ff52908b5..22e6f4e1d7 100644 --- a/components/engine/vendor.conf +++ b/components/engine/vendor.conf @@ -77,7 +77,7 @@ github.com/syndtr/gocapability 2c00daeb6c3b45114c80ac44119e7b8801fdd852 github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 # gelf logging driver deps -github.com/Graylog2/go-gelf v2 +github.com/Graylog2/go-gelf ba920adcee0319adcc15f52851f1458f56547975 github.com/fluent/fluent-logger-golang v1.3.0 # fluent-logger-golang deps From d586c2cf196b3a63cb1a8c6041c4ab90cb9e9acf Mon Sep 17 00:00:00 2001 From: Ghislain Bourgeois Date: Mon, 11 Dec 2017 09:22:51 -0500 Subject: [PATCH 5/6] Update Graylog2/go-gelf vendoring. Fixes #35613 Signed-off-by: Ghislain Bourgeois Upstream-commit: f9f3c49302fc80e586e3cb10af43114929556b47 Component: engine --- components/engine/vendor.conf | 2 +- .../Graylog2/go-gelf/gelf/tcpreader.go | 97 +++++++++++++++---- .../Graylog2/go-gelf/gelf/tcpwriter.go | 12 ++- .../Graylog2/go-gelf/gelf/writer.go | 3 + 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/components/engine/vendor.conf b/components/engine/vendor.conf index 22e6f4e1d7..87f7930262 100644 --- a/components/engine/vendor.conf +++ b/components/engine/vendor.conf @@ -77,7 +77,7 @@ github.com/syndtr/gocapability 2c00daeb6c3b45114c80ac44119e7b8801fdd852 github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 # gelf logging driver deps -github.com/Graylog2/go-gelf ba920adcee0319adcc15f52851f1458f56547975 +github.com/Graylog2/go-gelf 4143646226541087117ff2f83334ea48b3201841 github.com/fluent/fluent-logger-golang v1.3.0 # fluent-logger-golang deps diff --git a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpreader.go b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpreader.go index 8f22c9aea4..74255ec3be 100644 --- a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpreader.go +++ b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpreader.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net" + "time" ) type TCPReader struct { @@ -13,16 +14,21 @@ type TCPReader struct { messages chan []byte } -func newTCPReader(addr string) (*TCPReader, chan string, error) { +type connChannels struct { + drop chan string + confirm chan string +} + +func newTCPReader(addr string) (*TCPReader, chan string, chan string, error) { var err error tcpAddr, err := net.ResolveTCPAddr("tcp", addr) if err != nil { - return nil, nil, fmt.Errorf("ResolveTCPAddr('%s'): %s", addr, err) + return nil, nil, nil, fmt.Errorf("ResolveTCPAddr('%s'): %s", addr, err) } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { - return nil, nil, fmt.Errorf("ListenTCP: %s", err) + return nil, nil, nil, fmt.Errorf("ListenTCP: %s", err) } r := &TCPReader{ @@ -30,26 +36,61 @@ func newTCPReader(addr string) (*TCPReader, chan string, error) { messages: make(chan []byte, 100), // Make a buffered channel with at most 100 messages } - signal := make(chan string, 1) + closeSignal := make(chan string, 1) + doneSignal := make(chan string, 1) - go r.listenUntilCloseSignal(signal) + go r.listenUntilCloseSignal(closeSignal, doneSignal) - return r, signal, nil + return r, closeSignal, doneSignal, nil } -func (r *TCPReader) listenUntilCloseSignal(signal chan string) { - defer func() { signal <- "done" }() - defer r.listener.Close() +func (r *TCPReader) accepter(connections chan net.Conn) { for { conn, err := r.listener.Accept() if err != nil { break } - go handleConnection(conn, r.messages) + connections <- conn + } +} + +func (r *TCPReader) listenUntilCloseSignal(closeSignal chan string, doneSignal chan string) { + defer func() { doneSignal <- "done" }() + defer r.listener.Close() + var conns []connChannels + connectionsChannel := make(chan net.Conn, 1) + go r.accepter(connectionsChannel) + for { select { - case sig := <-signal: - if sig == "stop" { - break + case conn := <-connectionsChannel: + dropSignal := make(chan string, 1) + dropConfirm := make(chan string, 1) + channels := connChannels{drop: dropSignal, confirm: dropConfirm} + go handleConnection(conn, r.messages, dropSignal, dropConfirm) + conns = append(conns, channels) + default: + } + + select { + case sig := <-closeSignal: + if sig == "stop" || sig == "drop" { + if len(conns) >= 1 { + for _, s := range conns { + if s.drop != nil { + s.drop <- "drop" + <-s.confirm + conns = append(conns[:0], conns[1:]...) + } + } + if sig == "stop" { + return + } + } else if sig == "stop" { + closeSignal <- "stop" + } + if sig == "drop" { + doneSignal <- "done" + } } default: } @@ -60,19 +101,41 @@ func (r *TCPReader) addr() string { return r.listener.Addr().String() } -func handleConnection(conn net.Conn, messages chan<- []byte) { +func handleConnection(conn net.Conn, messages chan<- []byte, dropSignal chan string, dropConfirm chan string) { + defer func() { dropConfirm <- "done" }() defer conn.Close() reader := bufio.NewReader(conn) var b []byte var err error + drop := false + canDrop := false for { + conn.SetDeadline(time.Now().Add(2 * time.Second)) if b, err = reader.ReadBytes(0); err != nil { - continue - } - if len(b) > 0 { + if drop { + return + } + } else if len(b) > 0 { messages <- b + canDrop = true + if drop { + return + } + } else if drop { + return + } + select { + case sig := <-dropSignal: + if sig == "drop" { + drop = true + time.Sleep(1 * time.Second) + if canDrop { + return + } + } + default: } } } diff --git a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpwriter.go b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpwriter.go index ab95cbcd02..da1390d1d6 100644 --- a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpwriter.go +++ b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/tcpwriter.go @@ -75,12 +75,17 @@ func (w *TCPWriter) Write(p []byte) (n int, err error) { func (w *TCPWriter) writeToSocketWithReconnectAttempts(zBytes []byte) (n int, err error) { var errConn error + var i int w.mu.Lock() - for i := 0; n <= w.MaxReconnect; i++ { + for i = 0; i <= w.MaxReconnect; i++ { errConn = nil - n, err = w.conn.Write(zBytes) + if w.conn != nil { + n, err = w.conn.Write(zBytes) + } else { + err = fmt.Errorf("Connection was nil, will attempt reconnect") + } if err != nil { time.Sleep(w.ReconnectDelay * time.Second) w.conn, errConn = net.Dial("tcp", w.addr) @@ -90,6 +95,9 @@ func (w *TCPWriter) writeToSocketWithReconnectAttempts(zBytes []byte) (n int, er } w.mu.Unlock() + if i > w.MaxReconnect { + return 0, fmt.Errorf("Maximum reconnection attempts was reached; giving up") + } if errConn != nil { return 0, fmt.Errorf("Write Failed: %s\nReconnection failed: %s", err, errConn) } diff --git a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/writer.go b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/writer.go index 93c36929b4..153be2c340 100644 --- a/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/writer.go +++ b/components/engine/vendor/github.com/Graylog2/go-gelf/gelf/writer.go @@ -27,5 +27,8 @@ type GelfWriter struct { // Close connection and interrupt blocked Read or Write operations func (w *GelfWriter) Close() error { + if w.conn == nil { + return nil + } return w.conn.Close() } From c8a642f1285f7a842f4528e10dd7c1131a6c3919 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 11 Dec 2017 18:28:29 -0800 Subject: [PATCH 6/6] Make TestLogsAPIUntil less flaky Commit ee594dcb7d42f95048c9047d86c61447243db3cd removed the `sleep 0.5` from this test, because sleep has a full-second precision. However, in some cases, all three log-entries are output at the same time, causing the `--until` filter to fail. This patch adds back a `sleep`, but uses 1 second instead. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 1360f0dc9a5460bccf7974f7718d031435b883f3 Component: engine --- components/engine/integration-cli/docker_api_logs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/engine/integration-cli/docker_api_logs_test.go b/components/engine/integration-cli/docker_api_logs_test.go index 41164df314..ef1e77d88b 100644 --- a/components/engine/integration-cli/docker_api_logs_test.go +++ b/components/engine/integration-cli/docker_api_logs_test.go @@ -151,7 +151,7 @@ func (s *DockerSuite) TestLogsAPIUntilFutureFollow(c *check.C) { func (s *DockerSuite) TestLogsAPIUntil(c *check.C) { name := "logsuntil" - dockerCmd(c, "run", "--name", name, "busybox", "/bin/sh", "-c", "for i in $(seq 1 3); do echo log$i; done") + dockerCmd(c, "run", "--name", name, "busybox", "/bin/sh", "-c", "for i in $(seq 1 3); do echo log$i; sleep 1; done") client, err := request.NewClient() if err != nil {