github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_run_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  	"bufio"
    21  	"bytes"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"regexp"
    28  	"runtime"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/containerd/nerdctl/pkg/testutil"
    34  	"gotest.tools/v3/assert"
    35  	"gotest.tools/v3/icmd"
    36  	"gotest.tools/v3/poll"
    37  )
    38  
    39  func TestRunEntrypointWithBuild(t *testing.T) {
    40  	t.Parallel()
    41  	testutil.RequiresBuild(t)
    42  	base := testutil.NewBase(t)
    43  	defer base.Cmd("builder", "prune").Run()
    44  	imageName := testutil.Identifier(t)
    45  	defer base.Cmd("rmi", imageName).Run()
    46  
    47  	dockerfile := fmt.Sprintf(`FROM %s
    48  ENTRYPOINT ["echo", "foo"]
    49  CMD ["echo", "bar"]
    50  	`, testutil.CommonImage)
    51  
    52  	buildCtx, err := createBuildContext(dockerfile)
    53  	assert.NilError(t, err)
    54  	defer os.RemoveAll(buildCtx)
    55  
    56  	base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
    57  	base.Cmd("run", "--rm", imageName).AssertOutExactly("foo echo bar\n")
    58  	base.Cmd("run", "--rm", "--entrypoint", "", imageName).AssertFail()
    59  	base.Cmd("run", "--rm", "--entrypoint", "", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error {
    60  		if !strings.Contains(stdout, "blah") {
    61  			return errors.New("echo blah was not executed?")
    62  		}
    63  		if strings.Contains(stdout, "bar") {
    64  			return errors.New("echo bar should not be executed")
    65  		}
    66  		if strings.Contains(stdout, "foo") {
    67  			return errors.New("echo foo should not be executed")
    68  		}
    69  		return nil
    70  	})
    71  	base.Cmd("run", "--rm", "--entrypoint", "time", imageName).AssertFail()
    72  	base.Cmd("run", "--rm", "--entrypoint", "time", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error {
    73  		if !strings.Contains(stdout, "blah") {
    74  			return errors.New("echo blah was not executed?")
    75  		}
    76  		if strings.Contains(stdout, "bar") {
    77  			return errors.New("echo bar should not be executed")
    78  		}
    79  		if strings.Contains(stdout, "foo") {
    80  			return errors.New("echo foo should not be executed")
    81  		}
    82  		return nil
    83  	})
    84  }
    85  
    86  func TestRunWorkdir(t *testing.T) {
    87  	t.Parallel()
    88  	base := testutil.NewBase(t)
    89  	dir := "/foo"
    90  	if runtime.GOOS == "windows" {
    91  		dir = "c:" + dir
    92  	}
    93  	cmd := base.Cmd("run", "--rm", "--workdir="+dir, testutil.CommonImage, "pwd")
    94  	cmd.AssertOutContains("/foo")
    95  }
    96  
    97  func TestRunWithDoubleDash(t *testing.T) {
    98  	t.Parallel()
    99  	testutil.DockerIncompatible(t)
   100  	base := testutil.NewBase(t)
   101  	base.Cmd("run", "--rm", testutil.CommonImage, "--", "sh", "-euxc", "exit 0").AssertOK()
   102  }
   103  
   104  func TestRunExitCode(t *testing.T) {
   105  	t.Parallel()
   106  	base := testutil.NewBase(t)
   107  	tID := testutil.Identifier(t)
   108  	testContainer0 := tID + "-0"
   109  	testContainer123 := tID + "-123"
   110  	defer base.Cmd("rm", "-f", testContainer0, testContainer123).Run()
   111  
   112  	base.Cmd("run", "--name", testContainer0, testutil.CommonImage, "sh", "-euxc", "exit 0").AssertOK()
   113  	base.Cmd("run", "--name", testContainer123, testutil.CommonImage, "sh", "-euxc", "exit 123").AssertExitCode(123)
   114  	base.Cmd("ps", "-a").AssertOutWithFunc(func(stdout string) error {
   115  		if !strings.Contains(stdout, "Exited (0)") {
   116  			return fmt.Errorf("no entry for %q", testContainer0)
   117  		}
   118  		if !strings.Contains(stdout, "Exited (123)") {
   119  			return fmt.Errorf("no entry for %q", testContainer123)
   120  		}
   121  		return nil
   122  	})
   123  
   124  	inspect0 := base.InspectContainer(testContainer0)
   125  	assert.Equal(base.T, "exited", inspect0.State.Status)
   126  	assert.Equal(base.T, 0, inspect0.State.ExitCode)
   127  
   128  	inspect123 := base.InspectContainer(testContainer123)
   129  	assert.Equal(base.T, "exited", inspect123.State.Status)
   130  	assert.Equal(base.T, 123, inspect123.State.ExitCode)
   131  }
   132  
   133  func TestRunCIDFile(t *testing.T) {
   134  	t.Parallel()
   135  	base := testutil.NewBase(t)
   136  	fileName := filepath.Join(t.TempDir(), "cid.file")
   137  
   138  	base.Cmd("run", "--rm", "--cidfile", fileName, testutil.CommonImage).AssertOK()
   139  	defer os.Remove(fileName)
   140  
   141  	_, err := os.Stat(fileName)
   142  	assert.NilError(base.T, err)
   143  
   144  	base.Cmd("run", "--rm", "--cidfile", fileName, testutil.CommonImage).AssertFail()
   145  }
   146  
   147  func TestRunEnvFile(t *testing.T) {
   148  	t.Parallel()
   149  	base := testutil.NewBase(t)
   150  	base.Env = append(os.Environ(), "HOST_ENV=ENV-IN-HOST")
   151  
   152  	tID := testutil.Identifier(t)
   153  	file1, err := os.CreateTemp("", tID)
   154  	assert.NilError(base.T, err)
   155  	path1 := file1.Name()
   156  	defer file1.Close()
   157  	defer os.Remove(path1)
   158  	err = os.WriteFile(path1, []byte("# this is a comment line\nTESTKEY1=TESTVAL1"), 0666)
   159  	assert.NilError(base.T, err)
   160  
   161  	file2, err := os.CreateTemp("", tID)
   162  	assert.NilError(base.T, err)
   163  	path2 := file2.Name()
   164  	defer file2.Close()
   165  	defer os.Remove(path2)
   166  	err = os.WriteFile(path2, []byte("# this is a comment line\nTESTKEY2=TESTVAL2\nHOST_ENV"), 0666)
   167  	assert.NilError(base.T, err)
   168  
   169  	base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $TESTKEY1").AssertOutExactly("TESTVAL1")
   170  	base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $TESTKEY2").AssertOutExactly("TESTVAL2")
   171  	base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $HOST_ENV").AssertOutExactly("ENV-IN-HOST")
   172  }
   173  
   174  func TestRunEnv(t *testing.T) {
   175  	t.Parallel()
   176  	base := testutil.NewBase(t)
   177  	base.Env = append(os.Environ(), "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
   178  	base.Cmd("run", "--rm",
   179  		"--env", "FOO=foo1,foo2",
   180  		"--env", "BAR=bar1 bar2",
   181  		"--env", "BAZ=",
   182  		"--env", "QUX", // not exported in OS
   183  		"--env", "QUUX=quux1",
   184  		"--env", "QUUX=quux2",
   185  		"--env", "CORGE", // OS exported
   186  		"--env", "GRAULT=grault_key=grault_value", // value contains `=` char
   187  		"--env", "GARPLY=", // OS exported
   188  		"--env", "WALDO=", // not exported in OS
   189  
   190  		testutil.CommonImage, "env").AssertOutWithFunc(func(stdout string) error {
   191  		if !strings.Contains(stdout, "\nFOO=foo1,foo2\n") {
   192  			return errors.New("got bad FOO")
   193  		}
   194  		if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") {
   195  			return errors.New("got bad BAR")
   196  		}
   197  		if !strings.Contains(stdout, "\nBAZ=\n") && runtime.GOOS != "windows" {
   198  			return errors.New("got bad BAZ")
   199  		}
   200  		if strings.Contains(stdout, "QUX") {
   201  			return errors.New("got bad QUX (should not be set)")
   202  		}
   203  		if !strings.Contains(stdout, "\nQUUX=quux2\n") {
   204  			return errors.New("got bad QUUX")
   205  		}
   206  		if !strings.Contains(stdout, "\nCORGE=corge-value-in-host\n") {
   207  			return errors.New("got bad CORGE")
   208  		}
   209  		if !strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n") {
   210  			return errors.New("got bad GRAULT")
   211  		}
   212  		if !strings.Contains(stdout, "\nGARPLY=\n") && runtime.GOOS != "windows" {
   213  			return errors.New("got bad GARPLY")
   214  		}
   215  		if !strings.Contains(stdout, "\nWALDO=\n") && runtime.GOOS != "windows" {
   216  			return errors.New("got bad WALDO")
   217  		}
   218  
   219  		return nil
   220  	})
   221  }
   222  
   223  func TestRunStdin(t *testing.T) {
   224  	t.Parallel()
   225  	base := testutil.NewBase(t)
   226  	if testutil.GetTarget() == testutil.Nerdctl {
   227  		testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
   228  	}
   229  
   230  	const testStr = "test-run-stdin"
   231  	opts := []func(*testutil.Cmd){
   232  		testutil.WithStdin(strings.NewReader(testStr)),
   233  	}
   234  	base.Cmd("run", "--rm", "-i", testutil.CommonImage, "cat").CmdOption(opts...).AssertOutExactly(testStr)
   235  }
   236  
   237  func TestRunWithJsonFileLogDriver(t *testing.T) {
   238  	if runtime.GOOS == "windows" {
   239  		t.Skip("json-file log driver is not yet implemented on Windows")
   240  	}
   241  	base := testutil.NewBase(t)
   242  	containerName := testutil.Identifier(t)
   243  
   244  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   245  	base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage,
   246  		"sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK()
   247  
   248  	time.Sleep(3 * time.Second)
   249  	inspectedContainer := base.InspectContainer(containerName)
   250  	logJSONPath := filepath.Dir(inspectedContainer.LogPath)
   251  	// matches = current log file + old log files to retain
   252  	matches, err := filepath.Glob(filepath.Join(logJSONPath, inspectedContainer.ID+"*"))
   253  	assert.NilError(t, err)
   254  	if len(matches) != 2 {
   255  		t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches)
   256  	}
   257  	for _, file := range matches {
   258  		fInfo, err := os.Stat(file)
   259  		assert.NilError(t, err)
   260  		// The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility.
   261  		// Docker log rotation lacks precision because the size check is done at the log entry level
   262  		// and not at the byte level (io.Writer), so docker log files can exceed 5k
   263  		if fInfo.Size() > 5200 {
   264  			t.Fatal("file size exceeded 5k")
   265  		}
   266  	}
   267  }
   268  
   269  func TestRunWithJsonFileLogDriverAndLogPathOpt(t *testing.T) {
   270  	if runtime.GOOS == "windows" {
   271  		t.Skip("json-file log driver is not yet implemented on Windows")
   272  	}
   273  	testutil.DockerIncompatible(t)
   274  	base := testutil.NewBase(t)
   275  	containerName := testutil.Identifier(t)
   276  
   277  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   278  	customLogJSONPath := filepath.Join(t.TempDir(), containerName, containerName+"-json.log")
   279  	base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage,
   280  		"sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK()
   281  
   282  	time.Sleep(3 * time.Second)
   283  	rawBytes, err := os.ReadFile(customLogJSONPath)
   284  	assert.NilError(t, err)
   285  	if len(rawBytes) == 0 {
   286  		t.Fatalf("logs are not written correctly to log-path: %s", customLogJSONPath)
   287  	}
   288  
   289  	// matches = current log file + old log files to retain
   290  	matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), containerName+"*"))
   291  	assert.NilError(t, err)
   292  	if len(matches) != 2 {
   293  		t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches)
   294  	}
   295  	for _, file := range matches {
   296  		fInfo, err := os.Stat(file)
   297  		assert.NilError(t, err)
   298  		if fInfo.Size() > 5200 {
   299  			t.Fatal("file size exceeded 5k")
   300  		}
   301  	}
   302  }
   303  
   304  func TestRunWithJournaldLogDriver(t *testing.T) {
   305  	if runtime.GOOS == "windows" {
   306  		t.Skip("journald log driver is not yet implemented on Windows")
   307  	}
   308  	base := testutil.NewBase(t)
   309  	containerName := testutil.Identifier(t)
   310  
   311  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   312  	base.Cmd("run", "-d", "--log-driver", "journald", "--name", containerName, testutil.CommonImage,
   313  		"sh", "-euxc", "echo foo; echo bar").AssertOK()
   314  
   315  	time.Sleep(3 * time.Second)
   316  	journalctl, err := exec.LookPath("journalctl")
   317  	assert.NilError(t, err)
   318  	inspectedContainer := base.InspectContainer(containerName)
   319  	found := 0
   320  	check := func(log poll.LogT) poll.Result {
   321  		res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12])))
   322  		assert.Equal(t, 0, res.ExitCode, res.Combined())
   323  		if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
   324  			found = 1
   325  			return poll.Success()
   326  		}
   327  		return poll.Continue("reading from journald is not yet finished")
   328  	}
   329  	poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
   330  	assert.Equal(t, 1, found)
   331  }
   332  
   333  func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) {
   334  	if runtime.GOOS == "windows" {
   335  		t.Skip("journald log driver is not yet implemented on Windows")
   336  	}
   337  	base := testutil.NewBase(t)
   338  	containerName := testutil.Identifier(t)
   339  
   340  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   341  	base.Cmd("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", containerName, testutil.CommonImage,
   342  		"sh", "-euxc", "echo foo; echo bar").AssertOK()
   343  
   344  	time.Sleep(3 * time.Second)
   345  	journalctl, err := exec.LookPath("journalctl")
   346  	assert.NilError(t, err)
   347  	inspectedContainer := base.InspectContainer(containerName)
   348  	found := 0
   349  	check := func(log poll.LogT) poll.Result {
   350  		res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID)))
   351  		assert.Equal(t, 0, res.ExitCode, res.Combined())
   352  		if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
   353  			found = 1
   354  			return poll.Success()
   355  		}
   356  		return poll.Continue("reading from journald is not yet finished")
   357  	}
   358  	poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
   359  	assert.Equal(t, 1, found)
   360  }
   361  
   362  func TestRunWithLogBinary(t *testing.T) {
   363  	testutil.RequiresBuild(t)
   364  	if runtime.GOOS == "windows" {
   365  		t.Skip("buildkit is not enabled on windows, this feature may work on windows.")
   366  	}
   367  	testutil.DockerIncompatible(t)
   368  	t.Parallel()
   369  	base := testutil.NewBase(t)
   370  	imageName := testutil.Identifier(t) + "-image"
   371  	containerName := testutil.Identifier(t)
   372  
   373  	const dockerfile = `
   374  FROM golang:latest as builder
   375  WORKDIR /go/src/
   376  RUN mkdir -p logger
   377  WORKDIR /go/src/logger
   378  RUN echo '\
   379  	package main \n\
   380  	\n\
   381  	import ( \n\
   382  	"bufio" \n\
   383  	"context" \n\
   384  	"fmt" \n\
   385  	"io" \n\
   386  	"os" \n\
   387  	"path/filepath" \n\
   388  	"sync" \n\
   389  	\n\
   390  	"github.com/containerd/containerd/runtime/v2/logging"\n\
   391  	)\n\
   392  
   393  	func main() {\n\
   394  		logging.Run(log)\n\
   395  	}\n\
   396  
   397  	func log(ctx context.Context, config *logging.Config, ready func() error) error {\n\
   398  		var wg sync.WaitGroup \n\
   399  		wg.Add(2) \n\
   400  		// forward both stdout and stderr to temp files \n\
   401  		go copy(&wg, config.Stdout, config.ID, "stdout") \n\
   402  		go copy(&wg, config.Stderr, config.ID, "stderr") \n\
   403  
   404  		// signal that we are ready and setup for the container to be started \n\
   405  		if err := ready(); err != nil { \n\
   406  		return err \n\
   407  		} \n\
   408  		wg.Wait() \n\
   409  		return nil \n\
   410  	}\n\
   411  	\n\
   412  	func copy(wg *sync.WaitGroup, r io.Reader, id string, kind string) { \n\
   413  		f, _ := os.Create(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", id, kind))) \n\
   414  		defer f.Close() \n\
   415  		defer wg.Done() \n\
   416  		s := bufio.NewScanner(r) \n\
   417  		for s.Scan() { \n\
   418  			f.WriteString(s.Text()) \n\
   419  		} \n\
   420  	}\n' >> main.go
   421  
   422  
   423  RUN go mod init
   424  RUN go mod tidy
   425  RUN go build .
   426  
   427  FROM scratch
   428  COPY --from=builder /go/src/logger/logger /
   429  	`
   430  
   431  	buildCtx, err := createBuildContext(dockerfile)
   432  	assert.NilError(t, err)
   433  	defer os.RemoveAll(buildCtx)
   434  	tmpDir := t.TempDir()
   435  	base.Cmd("build", buildCtx, "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", tmpDir)).AssertOK()
   436  	defer base.Cmd("image", "rm", "-f", imageName).AssertOK()
   437  
   438  	base.Cmd("container", "rm", "-f", containerName).AssertOK()
   439  	base.Cmd("run", "-d", "--log-driver", fmt.Sprintf("binary://%s/logger", tmpDir), "--name", containerName, testutil.CommonImage,
   440  		"sh", "-euxc", "echo foo; echo bar").AssertOK()
   441  	defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
   442  
   443  	inspectedContainer := base.InspectContainer(containerName)
   444  	bytes, err := os.ReadFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", inspectedContainer.ID, "stdout")))
   445  	assert.NilError(t, err)
   446  	log := string(bytes)
   447  	assert.Check(t, strings.Contains(log, "foo"))
   448  	assert.Check(t, strings.Contains(log, "bar"))
   449  }
   450  
   451  func TestRunWithTtyAndDetached(t *testing.T) {
   452  	if runtime.GOOS == "windows" {
   453  		t.Skip("json-file log driver is not yet implemented on Windows")
   454  	}
   455  	base := testutil.NewBase(t)
   456  	imageName := testutil.CommonImage
   457  	withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t)
   458  	withTtyContainerName := "with-terminal-" + testutil.Identifier(t)
   459  
   460  	// without -t, fail
   461  	base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK()
   462  	defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK()
   463  	base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty")
   464  	withoutTtyContainer := base.InspectContainer(withoutTtyContainerName)
   465  	assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode)
   466  
   467  	// with -t, success
   468  	base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK()
   469  	defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK()
   470  	base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;")
   471  	withTtyContainer := base.InspectContainer(withTtyContainerName)
   472  	assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
   473  }
   474  
   475  // history: There was a bug that the --add-host items disappear when the another container created.
   476  // This case ensures that it's doesn't happen.
   477  // (https://github.com/containerd/nerdctl/issues/2560)
   478  func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) {
   479  	if runtime.GOOS == "windows" {
   480  		t.Skip("ocihook is not yet supported on Windows")
   481  	}
   482  	base := testutil.NewBase(t)
   483  
   484  	containerName := testutil.Identifier(t)
   485  	hostMapping := "test-add-host:10.0.0.1"
   486  	base.Cmd("run", "-d", "--add-host", hostMapping, "--name", containerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
   487  	defer base.Cmd("container", "rm", "-f", containerName).Run()
   488  
   489  	checkEtcHosts := func(stdout string) error {
   490  		matcher, err := regexp.Compile(`^10.0.0.1\s+test-add-host$`)
   491  		if err != nil {
   492  			return err
   493  		}
   494  		var found bool
   495  		sc := bufio.NewScanner(bytes.NewBufferString(stdout))
   496  		for sc.Scan() {
   497  			if matcher.Match(sc.Bytes()) {
   498  				found = true
   499  			}
   500  		}
   501  		if !found {
   502  			return fmt.Errorf("host not found")
   503  		}
   504  		return nil
   505  	}
   506  	base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts)
   507  
   508  	// run another container
   509  	base.Cmd("run", "--rm", testutil.CommonImage).AssertOK()
   510  
   511  	base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts)
   512  }
   513  
   514  // https://github.com/containerd/nerdctl/issues/2726
   515  func TestRunRmTime(t *testing.T) {
   516  	base := testutil.NewBase(t)
   517  	base.Cmd("pull", testutil.CommonImage)
   518  	t0 := time.Now()
   519  	base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK()
   520  	t1 := time.Now()
   521  	took := t1.Sub(t0)
   522  	const deadline = 3 * time.Second
   523  	if took > deadline {
   524  		t.Fatalf("expected to have completed in %v, took %v", deadline, took)
   525  	}
   526  }
   527  
   528  func runAttachStdin(t *testing.T, testStr string, args []string) string {
   529  	if runtime.GOOS == "windows" {
   530  		t.Skip("run attach test is not yet implemented on Windows")
   531  	}
   532  
   533  	t.Parallel()
   534  	base := testutil.NewBase(t)
   535  	containerName := testutil.Identifier(t)
   536  
   537  	opts := []func(*testutil.Cmd){
   538  		testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")),
   539  	}
   540  
   541  	fullArgs := []string{"run", "--rm", "-i"}
   542  	fullArgs = append(fullArgs, args...)
   543  	fullArgs = append(fullArgs,
   544  		"--name",
   545  		containerName,
   546  		testutil.CommonImage,
   547  	)
   548  
   549  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   550  	result := base.Cmd(fullArgs...).CmdOption(opts...).Run()
   551  
   552  	return result.Combined()
   553  }
   554  
   555  func runAttach(t *testing.T, testStr string, args []string) string {
   556  	if runtime.GOOS == "windows" {
   557  		t.Skip("run attach test is not yet implemented on Windows")
   558  	}
   559  
   560  	t.Parallel()
   561  	base := testutil.NewBase(t)
   562  	containerName := testutil.Identifier(t)
   563  
   564  	fullArgs := []string{"run"}
   565  	fullArgs = append(fullArgs, args...)
   566  	fullArgs = append(fullArgs,
   567  		"--name",
   568  		containerName,
   569  		testutil.CommonImage,
   570  		"sh",
   571  		"-euxc",
   572  		"echo "+testStr,
   573  	)
   574  
   575  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   576  	result := base.Cmd(fullArgs...).Run()
   577  
   578  	return result.Combined()
   579  }
   580  
   581  func TestRunAttachFlag(t *testing.T) {
   582  
   583  	type testCase struct {
   584  		name        string
   585  		args        []string
   586  		testFunc    func(t *testing.T, testStr string, args []string) string
   587  		testStr     string
   588  		expectedOut string
   589  		dockerOut   string
   590  	}
   591  	testCases := []testCase{
   592  		{
   593  			name:        "AttachFlagStdin",
   594  			args:        []string{"-a", "STDIN", "-a", "STDOUT"},
   595  			testFunc:    runAttachStdin,
   596  			testStr:     "test-run-stdio",
   597  			expectedOut: "test-run-stdio",
   598  			dockerOut:   "test-run-stdio",
   599  		},
   600  		{
   601  			name:        "AttachFlagStdOut",
   602  			args:        []string{"-a", "STDOUT"},
   603  			testFunc:    runAttach,
   604  			testStr:     "foo",
   605  			expectedOut: "foo",
   606  			dockerOut:   "foo",
   607  		},
   608  		{
   609  			name:        "AttachFlagMixedValue",
   610  			args:        []string{"-a", "STDIN", "-a", "invalid-value"},
   611  			testFunc:    runAttach,
   612  			testStr:     "foo",
   613  			expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
   614  			dockerOut:   "valid streams are STDIN, STDOUT and STDERR",
   615  		},
   616  		{
   617  			name:        "AttachFlagInvalidValue",
   618  			args:        []string{"-a", "invalid-stream"},
   619  			testFunc:    runAttach,
   620  			testStr:     "foo",
   621  			expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
   622  			dockerOut:   "valid streams are STDIN, STDOUT and STDERR",
   623  		},
   624  		{
   625  			name:        "AttachFlagCaseInsensitive",
   626  			args:        []string{"-a", "stdin", "-a", "stdout"},
   627  			testFunc:    runAttachStdin,
   628  			testStr:     "test-run-stdio",
   629  			expectedOut: "test-run-stdio",
   630  			dockerOut:   "test-run-stdio",
   631  		},
   632  	}
   633  
   634  	for _, tc := range testCases {
   635  		tc := tc
   636  		t.Run(tc.name, func(t *testing.T) {
   637  			actualOut := tc.testFunc(t, tc.testStr, tc.args)
   638  			errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut)
   639  			if testutil.GetTarget() == testutil.Docker {
   640  				assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
   641  			} else {
   642  				assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg)
   643  			}
   644  		})
   645  	}
   646  }