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 }