github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/compose_exec_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  	"errors"
    21  	"fmt"
    22  	"net"
    23  	"os"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/containerd/nerdctl/pkg/testutil"
    28  	"gotest.tools/v3/assert"
    29  )
    30  
    31  func TestComposeExec(t *testing.T) {
    32  	base := testutil.NewBase(t)
    33  	var dockerComposeYAML = fmt.Sprintf(`
    34  version: '3.1'
    35  
    36  services:
    37    svc0:
    38      image: %s
    39      command: "sleep infinity"
    40    svc1:
    41      image: %s
    42      command: "sleep infinity"
    43  `, testutil.CommonImage, testutil.CommonImage)
    44  
    45  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
    46  	defer comp.CleanUp()
    47  	projectName := comp.ProjectName()
    48  	t.Logf("projectName=%q", projectName)
    49  
    50  	base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "svc0").AssertOK()
    51  	defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
    52  
    53  	// test basic functionality and `--workdir` flag
    54  	base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "svc0", "echo", "success").AssertOutExactly("success\n")
    55  	base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "--workdir", "/tmp", "svc0", "pwd").AssertOutExactly("/tmp\n")
    56  	// cannot `exec` on non-running service
    57  	base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "svc1", "echo", "success").AssertFail()
    58  }
    59  
    60  func TestComposeExecWithEnv(t *testing.T) {
    61  	base := testutil.NewBase(t)
    62  	var dockerComposeYAML = fmt.Sprintf(`
    63  version: '3.1'
    64  
    65  services:
    66    svc0:
    67      image: %s
    68      command: "sleep infinity"
    69  `, testutil.CommonImage)
    70  
    71  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
    72  	defer comp.CleanUp()
    73  	projectName := comp.ProjectName()
    74  	t.Logf("projectName=%q", projectName)
    75  
    76  	base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
    77  	defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
    78  
    79  	// FYI: https://github.com/containerd/nerdctl/blob/e4b2b6da56555dc29ed66d0fd8e7094ff2bc002d/cmd/nerdctl/run_test.go#L177
    80  	base.Env = append(os.Environ(), "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
    81  	base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY",
    82  		"--env", "FOO=foo1,foo2",
    83  		"--env", "BAR=bar1 bar2",
    84  		"--env", "BAZ=",
    85  		"--env", "QUX", // not exported in OS
    86  		"--env", "QUUX=quux1",
    87  		"--env", "QUUX=quux2",
    88  		"--env", "CORGE", // OS exported
    89  		"--env", "GRAULT=grault_key=grault_value", // value contains `=` char
    90  		"--env", "GARPLY=", // OS exported
    91  		"--env", "WALDO=", // not exported in OS
    92  
    93  		"svc0", "env").AssertOutWithFunc(func(stdout string) error {
    94  		if !strings.Contains(stdout, "\nFOO=foo1,foo2\n") {
    95  			return errors.New("got bad FOO")
    96  		}
    97  		if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") {
    98  			return errors.New("got bad BAR")
    99  		}
   100  		if !strings.Contains(stdout, "\nBAZ=\n") {
   101  			return errors.New("got bad BAZ")
   102  		}
   103  		if strings.Contains(stdout, "QUX") {
   104  			return errors.New("got bad QUX (should not be set)")
   105  		}
   106  		if !strings.Contains(stdout, "\nQUUX=quux2\n") {
   107  			return errors.New("got bad QUUX")
   108  		}
   109  		if !strings.Contains(stdout, "\nCORGE=corge-value-in-host\n") {
   110  			return errors.New("got bad CORGE")
   111  		}
   112  		if !strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n") {
   113  			return errors.New("got bad GRAULT")
   114  		}
   115  		if !strings.Contains(stdout, "\nGARPLY=\n") {
   116  			return errors.New("got bad GARPLY")
   117  		}
   118  		if !strings.Contains(stdout, "\nWALDO=\n") {
   119  			return errors.New("got bad WALDO")
   120  		}
   121  
   122  		return nil
   123  	})
   124  }
   125  
   126  func TestComposeExecWithUser(t *testing.T) {
   127  	base := testutil.NewBase(t)
   128  	var dockerComposeYAML = fmt.Sprintf(`
   129  version: '3.1'
   130  
   131  services:
   132    svc0:
   133      image: %s
   134      command: "sleep infinity"
   135  `, testutil.CommonImage)
   136  
   137  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   138  	defer comp.CleanUp()
   139  	projectName := comp.ProjectName()
   140  	t.Logf("projectName=%q", projectName)
   141  
   142  	base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
   143  	defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
   144  
   145  	testCases := map[string]string{
   146  		"":             "uid=0(root) gid=0(root)",
   147  		"1000":         "uid=1000 gid=0(root)",
   148  		"1000:users":   "uid=1000 gid=100(users)",
   149  		"guest":        "uid=405(guest) gid=100(users)",
   150  		"nobody":       "uid=65534(nobody) gid=65534(nobody)",
   151  		"nobody:users": "uid=65534(nobody) gid=100(users)",
   152  	}
   153  
   154  	for userStr, expected := range testCases {
   155  		args := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY"}
   156  		if userStr != "" {
   157  			args = append(args, "--user", userStr)
   158  		}
   159  		args = append(args, "svc0", "id")
   160  		base.ComposeCmd(args...).AssertOutContains(expected)
   161  	}
   162  }
   163  
   164  func TestComposeExecTTY(t *testing.T) {
   165  	// `-i` in `compose run & exec` is only supported in compose v2.
   166  	base := testutil.NewBase(t)
   167  	if testutil.GetTarget() == testutil.Nerdctl {
   168  		testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
   169  	}
   170  
   171  	var dockerComposeYAML = fmt.Sprintf(`
   172  version: '3.1'
   173  
   174  services:
   175    svc0:
   176      image: %s
   177    svc1:
   178      image: %s
   179  `, testutil.CommonImage, testutil.CommonImage)
   180  
   181  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   182  	defer comp.CleanUp()
   183  	projectName := comp.ProjectName()
   184  	t.Logf("projectName=%q", projectName)
   185  
   186  	testContainer := testutil.Identifier(t)
   187  	base.ComposeCmd("-f", comp.YAMLFullPath(), "run", "-d", "-i=false", "--name", testContainer, "svc0", "sleep", "1h").AssertOK()
   188  	defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
   189  	base.EnsureContainerStarted(testContainer)
   190  
   191  	const sttyPartialOutput = "speed 38400 baud"
   192  	// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
   193  	// unbuffer(1) can be installed with `apt-get install expect`.
   194  	unbuffer := []string{"unbuffer"}
   195  	base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "svc0", "stty").AssertOutContains(sttyPartialOutput)             // `-it`
   196  	base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "svc0", "stty").AssertOutContains(sttyPartialOutput) // `-t`
   197  	base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "--no-TTY", "svc0", "stty").AssertFail()                         // `-i`
   198  	base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "svc0", "stty").AssertFail()
   199  }
   200  
   201  func TestComposeExecWithIndex(t *testing.T) {
   202  	base := testutil.NewBase(t)
   203  	var dockerComposeYAML = fmt.Sprintf(`
   204  version: '3.1'
   205  
   206  services:
   207    svc0:
   208      image: %s
   209      command: "sleep infinity"
   210      deploy:
   211        replicas: 3
   212  `, testutil.CommonImage)
   213  
   214  	comp := testutil.NewComposeDir(t, dockerComposeYAML)
   215  	t.Cleanup(func() {
   216  		comp.CleanUp()
   217  	})
   218  	projectName := comp.ProjectName()
   219  	t.Logf("projectName=%q", projectName)
   220  
   221  	base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "svc0").AssertOK()
   222  	t.Cleanup(func() {
   223  		base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
   224  	})
   225  
   226  	// try 5 times to ensure that results are stable
   227  	for i := 0; i < 5; i++ {
   228  		for _, j := range []string{"1", "2", "3"} {
   229  			name := fmt.Sprintf("%s-svc0-%s", projectName, j)
   230  			host := fmt.Sprintf("%s.%s_default", name, projectName)
   231  			var (
   232  				expectIP string
   233  				realIP   string
   234  			)
   235  			//  docker and nerdctl have different DNS resolution behaviors.
   236  			// it uses the ID in the /etc/hosts file, so we need to fetch the ID first.
   237  			if testutil.GetTarget() == testutil.Docker {
   238  				base.Cmd("ps", "--filter", fmt.Sprintf("name=%s", name), "--format", "{{.ID}}").AssertOutWithFunc(func(stdout string) error {
   239  					host = strings.TrimSpace(stdout)
   240  					return nil
   241  				})
   242  			}
   243  			cmds := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "--index", j, "svc0"}
   244  			base.ComposeCmd(append(cmds, "cat", "/etc/hosts")...).
   245  				AssertOutWithFunc(func(stdout string) error {
   246  					lines := strings.Split(stdout, "\n")
   247  					for _, line := range lines {
   248  						if !strings.Contains(line, host) {
   249  							continue
   250  						}
   251  						fields := strings.Fields(line)
   252  						if len(fields) == 0 {
   253  							continue
   254  						}
   255  						expectIP = fields[0]
   256  						return nil
   257  					}
   258  					return errors.New("fail to get the expected ip address")
   259  				})
   260  			base.ComposeCmd(append(cmds, "ip", "addr", "show", "dev", "eth0")...).
   261  				AssertOutWithFunc(func(stdout string) error {
   262  					ip := findIP(stdout)
   263  					if ip == nil {
   264  						return errors.New("fail to get the real ip address")
   265  					}
   266  					realIP = ip.String()
   267  					return nil
   268  				})
   269  			assert.Equal(t, realIP, expectIP)
   270  		}
   271  	}
   272  }
   273  
   274  func findIP(output string) net.IP {
   275  	var ip string
   276  	lines := strings.Split(output, "\n")
   277  	for _, line := range lines {
   278  		if !strings.Contains(line, "inet ") {
   279  			continue
   280  		}
   281  		fields := strings.Fields(line)
   282  		if len(fields) <= 1 {
   283  			continue
   284  		}
   285  		ip = strings.Split(fields[1], "/")[0]
   286  		break
   287  	}
   288  	return net.ParseIP(ip)
   289  }