github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_run_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  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"os"
    28  	"path/filepath"
    29  	"strconv"
    30  	"strings"
    31  	"syscall"
    32  	"testing"
    33  	"time"
    34  
    35  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    36  	"github.com/containerd/nerdctl/pkg/strutil"
    37  	"github.com/containerd/nerdctl/pkg/testutil"
    38  	"gotest.tools/v3/assert"
    39  	"gotest.tools/v3/icmd"
    40  )
    41  
    42  func TestRunCustomRootfs(t *testing.T) {
    43  	testutil.DockerIncompatible(t)
    44  	base := testutil.NewBase(t)
    45  	rootfs := prepareCustomRootfs(base, testutil.AlpineImage)
    46  	defer os.RemoveAll(rootfs)
    47  	base.Cmd("run", "--rm", "--rootfs", rootfs, "/bin/cat", "/proc/self/environ").AssertOutContains("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
    48  	base.Cmd("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", rootfs, "echo", "foo").AssertOutExactly("echo foo\n")
    49  }
    50  
    51  func prepareCustomRootfs(base *testutil.Base, imageName string) string {
    52  	base.Cmd("pull", imageName).AssertOK()
    53  	tmpDir, err := os.MkdirTemp(base.T.TempDir(), "test-save")
    54  	assert.NilError(base.T, err)
    55  	defer os.RemoveAll(tmpDir)
    56  	archiveTarPath := filepath.Join(tmpDir, "a.tar")
    57  	base.Cmd("save", "-o", archiveTarPath, imageName).AssertOK()
    58  	rootfs, err := os.MkdirTemp(base.T.TempDir(), "rootfs")
    59  	assert.NilError(base.T, err)
    60  	err = extractDockerArchive(archiveTarPath, rootfs)
    61  	assert.NilError(base.T, err)
    62  	return rootfs
    63  }
    64  
    65  func TestRunShmSize(t *testing.T) {
    66  	t.Parallel()
    67  	base := testutil.NewBase(t)
    68  	const shmSize = "32m"
    69  
    70  	base.Cmd("run", "--rm", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k")
    71  }
    72  
    73  func TestRunPidHost(t *testing.T) {
    74  	t.Parallel()
    75  	base := testutil.NewBase(t)
    76  	pid := os.Getpid()
    77  
    78  	base.Cmd("run", "--rm", "--pid=host", testutil.AlpineImage, "ps", "auxw").AssertOutContains(strconv.Itoa(pid))
    79  }
    80  
    81  func TestRunUtsHost(t *testing.T) {
    82  	t.Parallel()
    83  	base := testutil.NewBase(t)
    84  
    85  	// Was thinking of os.ReadLink("/proc/1/ns/uts")
    86  	// but you'd get EPERM for rootless. Just validate the
    87  	// hostname is the same.
    88  	hostName, err := os.Hostname()
    89  	assert.NilError(base.T, err)
    90  
    91  	base.Cmd("run", "--rm", "--uts=host", testutil.AlpineImage, "hostname").AssertOutContains(hostName)
    92  	// Validate we can't provide a hostname with uts=host
    93  	base.Cmd("run", "--rm", "--uts=host", "--hostname=foobar", testutil.AlpineImage, "hostname").AssertFail()
    94  }
    95  
    96  func TestRunPidContainer(t *testing.T) {
    97  	t.Parallel()
    98  	base := testutil.NewBase(t)
    99  
   100  	sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", "infinity").Run()
   101  	baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout())
   102  	defer base.Cmd("rm", "-f", baseContainerID).Run()
   103  
   104  	base.Cmd("run", "--rm", fmt.Sprintf("--pid=container:%s", baseContainerID),
   105  		testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep infinity")
   106  }
   107  
   108  func TestRunIpcHost(t *testing.T) {
   109  	t.Parallel()
   110  	base := testutil.NewBase(t)
   111  	testFilePath := filepath.Join("/dev/shm",
   112  		fmt.Sprintf("%s-%d-%s", testutil.Identifier(t), os.Geteuid(), base.Target))
   113  	err := os.WriteFile(testFilePath, []byte(""), 0644)
   114  	assert.NilError(base.T, err)
   115  	defer os.Remove(testFilePath)
   116  
   117  	base.Cmd("run", "--rm", "--ipc=host", testutil.AlpineImage, "ls", testFilePath).AssertOK()
   118  }
   119  
   120  func TestRunAddHost(t *testing.T) {
   121  	// Not parallelizable (https://github.com/containerd/nerdctl/issues/1127)
   122  	base := testutil.NewBase(t)
   123  	base.Cmd("run", "--rm", "--add-host", "testing.example.com:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error {
   124  		var found bool
   125  		sc := bufio.NewScanner(bytes.NewBufferString(stdout))
   126  		for sc.Scan() {
   127  			//removing spaces and tabs separating items
   128  			line := strings.ReplaceAll(sc.Text(), " ", "")
   129  			line = strings.ReplaceAll(line, "\t", "")
   130  			if strings.Contains(line, "10.0.0.1testing.example.com") {
   131  				found = true
   132  			}
   133  		}
   134  		if !found {
   135  			return errors.New("host was not added")
   136  		}
   137  		return nil
   138  	})
   139  	base.Cmd("run", "--rm", "--add-host", "test:10.0.0.1", "--add-host", "test1:10.0.0.1", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error {
   140  		var found int
   141  		sc := bufio.NewScanner(bytes.NewBufferString(stdout))
   142  		for sc.Scan() {
   143  			//removing spaces and tabs separating items
   144  			line := strings.ReplaceAll(sc.Text(), " ", "")
   145  			line = strings.ReplaceAll(line, "\t", "")
   146  			if strutil.InStringSlice([]string{"10.0.0.1test", "10.0.0.1test1"}, line) {
   147  				found++
   148  			}
   149  		}
   150  		if found != 2 {
   151  			return fmt.Errorf("host was not added, found %d", found)
   152  		}
   153  		return nil
   154  	})
   155  	base.Cmd("run", "--rm", "--add-host", "10.0.0.1:testing.example.com", testutil.AlpineImage, "cat", "/etc/hosts").AssertFail()
   156  
   157  	response := "This is the expected response for --add-host special IP test."
   158  	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   159  		io.WriteString(w, response)
   160  	})
   161  	const hostPort = 8081
   162  	s := http.Server{Addr: fmt.Sprintf(":%d", hostPort), Handler: nil, ReadTimeout: 30 * time.Second}
   163  	go s.ListenAndServe()
   164  	defer s.Shutdown(context.Background())
   165  	base.Cmd("run", "--rm", "--add-host", "test:host-gateway", testutil.NginxAlpineImage, "curl", fmt.Sprintf("test:%d", hostPort)).AssertOutExactly(response)
   166  }
   167  
   168  func TestRunAddHostWithCustomHostGatewayIP(t *testing.T) {
   169  	// Not parallelizable (https://github.com/containerd/nerdctl/issues/1127)
   170  	base := testutil.NewBase(t)
   171  	testutil.DockerIncompatible(t)
   172  	base.Cmd("run", "--rm", "--host-gateway-ip", "192.168.5.2", "--add-host", "test:host-gateway", testutil.AlpineImage, "cat", "/etc/hosts").AssertOutWithFunc(func(stdout string) error {
   173  		var found bool
   174  		sc := bufio.NewScanner(bytes.NewBufferString(stdout))
   175  		for sc.Scan() {
   176  			//removing spaces and tabs separating items
   177  			line := strings.ReplaceAll(sc.Text(), " ", "")
   178  			line = strings.ReplaceAll(line, "\t", "")
   179  			if strings.Contains(line, "192.168.5.2test") {
   180  				found = true
   181  			}
   182  		}
   183  		if !found {
   184  			return errors.New("host was not added")
   185  		}
   186  		return nil
   187  	})
   188  }
   189  
   190  func TestRunUlimit(t *testing.T) {
   191  	t.Parallel()
   192  	base := testutil.NewBase(t)
   193  	ulimit := "nofile=622:622"
   194  	ulimit2 := "nofile=622:722"
   195  
   196  	base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n")
   197  	base.Cmd("run", "--rm", "--ulimit", ulimit, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("622\n")
   198  
   199  	base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Sn").AssertOutExactly("622\n")
   200  	base.Cmd("run", "--rm", "--ulimit", ulimit2, testutil.AlpineImage, "sh", "-c", "ulimit -Hn").AssertOutExactly("722\n")
   201  }
   202  
   203  func TestRunWithInit(t *testing.T) {
   204  	t.Parallel()
   205  	testutil.DockerIncompatible(t)
   206  	base := testutil.NewBase(t)
   207  
   208  	container := testutil.Identifier(t)
   209  	base.Cmd("run", "-d", "--name", container, testutil.AlpineImage, "sleep", "infinity").AssertOK()
   210  	defer base.Cmd("rm", "-f", container).Run()
   211  
   212  	base.Cmd("stop", "--time=3", container).AssertOK()
   213  	// Unable to handle TERM signal, be killed when timeout
   214  	assert.Equal(t, base.InspectContainer(container).State.ExitCode, 137)
   215  
   216  	// Test with --init-path
   217  	container1 := container + "-1"
   218  	base.Cmd("run", "-d", "--name", container1, "--init-binary", "tini-custom",
   219  		testutil.AlpineImage, "sleep", "infinity").AssertOK()
   220  	defer base.Cmd("rm", "-f", container1).Run()
   221  
   222  	base.Cmd("stop", "--time=3", container1).AssertOK()
   223  	assert.Equal(t, base.InspectContainer(container1).State.ExitCode, 143)
   224  
   225  	// Test with --init
   226  	container2 := container + "-2"
   227  	base.Cmd("run", "-d", "--name", container2, "--init",
   228  		testutil.AlpineImage, "sleep", "infinity").AssertOK()
   229  	defer base.Cmd("rm", "-f", container2).Run()
   230  
   231  	base.Cmd("stop", "--time=3", container2).AssertOK()
   232  	assert.Equal(t, base.InspectContainer(container2).State.ExitCode, 143)
   233  }
   234  
   235  func TestRunTTY(t *testing.T) {
   236  	t.Parallel()
   237  	base := testutil.NewBase(t)
   238  	if testutil.GetTarget() == testutil.Nerdctl {
   239  		testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
   240  	}
   241  
   242  	const sttyPartialOutput = "speed 38400 baud"
   243  	// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
   244  	// unbuffer(1) can be installed with `apt-get install expect`.
   245  	unbuffer := []string{"unbuffer"}
   246  	base.CmdWithHelper(unbuffer, "run", "--rm", "-it", testutil.CommonImage, "stty").AssertOutContains(sttyPartialOutput)
   247  	base.CmdWithHelper(unbuffer, "run", "--rm", "-t", testutil.CommonImage, "stty").AssertOutContains(sttyPartialOutput)
   248  	base.Cmd("run", "--rm", "-i", testutil.CommonImage, "stty").AssertFail()
   249  	base.Cmd("run", "--rm", testutil.CommonImage, "stty").AssertFail()
   250  
   251  	// tests pipe works
   252  	res := icmd.RunCmd(icmd.Command("unbuffer", "/bin/sh", "-c", fmt.Sprintf("%q run --rm -it %q echo hi | grep hi", base.Binary, testutil.CommonImage)))
   253  	assert.Equal(t, 0, res.ExitCode, res.Combined())
   254  }
   255  
   256  func runSigProxy(t *testing.T, args ...string) (string, bool, bool) {
   257  	t.Parallel()
   258  	base := testutil.NewBase(t)
   259  	testContainerName := testutil.Identifier(t)
   260  	defer base.Cmd("rm", "-f", testContainerName).Run()
   261  
   262  	fullArgs := []string{"run"}
   263  	fullArgs = append(fullArgs, args...)
   264  	fullArgs = append(fullArgs,
   265  		"--name",
   266  		testContainerName,
   267  		testutil.CommonImage,
   268  		"sh",
   269  		"-c",
   270  		testutil.SigProxyTestScript,
   271  	)
   272  
   273  	result := base.Cmd(fullArgs...).Start()
   274  	process := result.Cmd.Process
   275  
   276  	// Waits until we reach the trap command in the shell script, then sends SIGINT.
   277  	time.Sleep(3 * time.Second)
   278  	syscall.Kill(process.Pid, syscall.SIGINT)
   279  
   280  	// Waits until SIGINT is sent and responded to, then kills process to avoid timeout
   281  	time.Sleep(3 * time.Second)
   282  	process.Kill()
   283  
   284  	sigIntRecieved := strings.Contains(result.Stdout(), testutil.SigProxyTrueOut)
   285  	timedOut := strings.Contains(result.Stdout(), testutil.SigProxyTimeoutMsg)
   286  
   287  	return result.Stdout(), sigIntRecieved, timedOut
   288  }
   289  
   290  func TestRunSigProxy(t *testing.T) {
   291  
   292  	type testCase struct {
   293  		name        string
   294  		args        []string
   295  		want        bool
   296  		expectedOut string
   297  	}
   298  	testCases := []testCase{
   299  		{
   300  			name:        "SigProxyDefault",
   301  			args:        []string{},
   302  			want:        true,
   303  			expectedOut: testutil.SigProxyTrueOut,
   304  		},
   305  		{
   306  			name:        "SigProxyTrue",
   307  			args:        []string{"--sig-proxy=true"},
   308  			want:        true,
   309  			expectedOut: testutil.SigProxyTrueOut,
   310  		},
   311  		{
   312  			name:        "SigProxyFalse",
   313  			args:        []string{"--sig-proxy=false"},
   314  			want:        false,
   315  			expectedOut: "",
   316  		},
   317  	}
   318  
   319  	for _, tc := range testCases {
   320  		tc := tc
   321  		t.Run(tc.name, func(t *testing.T) {
   322  			stdout, sigIntRecieved, timedOut := runSigProxy(t, tc.args...)
   323  			errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, stdout)
   324  			assert.Equal(t, false, timedOut, errorMsg)
   325  			assert.Equal(t, tc.want, sigIntRecieved, errorMsg)
   326  		})
   327  	}
   328  }
   329  
   330  func TestRunWithFluentdLogDriver(t *testing.T) {
   331  	base := testutil.NewBase(t)
   332  	tempDirectory := t.TempDir()
   333  	err := os.Chmod(tempDirectory, 0777)
   334  	assert.NilError(t, err)
   335  
   336  	containerName := testutil.Identifier(t)
   337  	base.Cmd("run", "-d", "--name", containerName, "-p", "24224:24224",
   338  		"-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK()
   339  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   340  	time.Sleep(3 * time.Second)
   341  
   342  	testContainerName := containerName + "test"
   343  	base.Cmd("run", "-d", "--log-driver", "fluentd", "--name", testContainerName, testutil.CommonImage,
   344  		"sh", "-c", "echo test").AssertOK()
   345  	defer base.Cmd("rm", "-f", testContainerName).AssertOK()
   346  
   347  	inspectedContainer := base.InspectContainer(testContainerName)
   348  	matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log")
   349  	assert.NilError(t, err)
   350  	assert.Equal(t, 1, len(matches))
   351  
   352  	data, err := os.ReadFile(matches[0])
   353  	assert.NilError(t, err)
   354  	logData := string(data)
   355  	assert.Equal(t, true, strings.Contains(logData, "test"))
   356  	assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID))
   357  }
   358  
   359  func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) {
   360  	base := testutil.NewBase(t)
   361  	tempDirectory := t.TempDir()
   362  	err := os.Chmod(tempDirectory, 0777)
   363  	assert.NilError(t, err)
   364  
   365  	containerName := testutil.Identifier(t)
   366  	base.Cmd("run", "-d", "--name", containerName, "-p", "24225:24224",
   367  		"-v", fmt.Sprintf("%s:/fluentd/log", tempDirectory), testutil.FluentdImage).AssertOK()
   368  	defer base.Cmd("rm", "-f", containerName).AssertOK()
   369  	time.Sleep(3 * time.Second)
   370  
   371  	testContainerName := containerName + "test"
   372  	base.Cmd("run", "-d", "--log-driver", "fluentd", "--log-opt", "fluentd-address=127.0.0.1:24225",
   373  		"--name", testContainerName, testutil.CommonImage, "sh", "-c", "echo test2").AssertOK()
   374  	defer base.Cmd("rm", "-f", testContainerName).AssertOK()
   375  
   376  	inspectedContainer := base.InspectContainer(testContainerName)
   377  	matches, err := filepath.Glob(tempDirectory + "/" + "data.*.log")
   378  	assert.NilError(t, err)
   379  	assert.Equal(t, 1, len(matches))
   380  
   381  	data, err := os.ReadFile(matches[0])
   382  	assert.NilError(t, err)
   383  	logData := string(data)
   384  	assert.Equal(t, true, strings.Contains(logData, "test2"))
   385  	assert.Equal(t, true, strings.Contains(logData, inspectedContainer.ID))
   386  }
   387  
   388  func TestRunWithOOMScoreAdj(t *testing.T) {
   389  	if rootlessutil.IsRootless() {
   390  		t.Skip("test skipped for rootless containers.")
   391  	}
   392  	t.Parallel()
   393  	base := testutil.NewBase(t)
   394  	var score = "-42"
   395  
   396  	base.Cmd("run", "--rm", "--oom-score-adj", score, testutil.AlpineImage, "cat", "/proc/self/oom_score_adj").AssertOutContains(score)
   397  }
   398  
   399  func TestRunWithDetachKeys(t *testing.T) {
   400  	t.Parallel()
   401  
   402  	if testutil.GetTarget() == testutil.Docker {
   403  		t.Skip("When detaching from a container, for a session started with 'docker attach'" +
   404  			", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
   405  			" However, the flag is called '--detach-keys' in all cases" +
   406  			", so nerdctl prints 'read detach keys' for all cases" +
   407  			", and that's why this test is skipped for Docker.")
   408  	}
   409  
   410  	base := testutil.NewBase(t)
   411  	containerName := testutil.Identifier(t)
   412  	opts := []func(*testutil.Cmd){
   413  		testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))), // https://www.physics.udel.edu/~watson/scen103/ascii.html
   414  	}
   415  	defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
   416  	// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
   417  	// unbuffer(1) can be installed with `apt-get install expect`.
   418  	//
   419  	// "-p" is needed because we need unbuffer to read from stdin, and from [1]:
   420  	// "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
   421  	//  To use unbuffer in a pipeline, use the -p flag."
   422  	//
   423  	// [1] https://linux.die.net/man/1/unbuffer
   424  	base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", containerName, testutil.CommonImage).
   425  		CmdOption(opts...).AssertOutContains("read detach keys")
   426  	container := base.InspectContainer(containerName)
   427  	assert.Equal(base.T, container.State.Running, true)
   428  }