github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_run_network_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 "fmt" 21 "io" 22 "net" 23 "regexp" 24 "runtime" 25 "strings" 26 "testing" 27 28 "github.com/containerd/errdefs" 29 "github.com/containerd/nerdctl/pkg/rootlessutil" 30 "github.com/containerd/nerdctl/pkg/testutil" 31 "github.com/containerd/nerdctl/pkg/testutil/nettestutil" 32 "gotest.tools/v3/assert" 33 "gotest.tools/v3/icmd" 34 ) 35 36 // TestRunInternetConnectivity tests Internet connectivity with `apk update` 37 func TestRunInternetConnectivity(t *testing.T) { 38 base := testutil.NewBase(t) 39 customNet := testutil.Identifier(t) 40 base.Cmd("network", "create", customNet).AssertOK() 41 defer base.Cmd("network", "rm", customNet).Run() 42 43 type testCase struct { 44 args []string 45 } 46 customNetID := base.InspectNetwork(customNet).ID 47 testCases := []testCase{ 48 { 49 args: []string{"--net", "bridge"}, 50 }, 51 { 52 args: []string{"--net", customNet}, 53 }, 54 { 55 args: []string{"--net", customNetID}, 56 }, 57 { 58 args: []string{"--net", customNetID[:12]}, 59 }, 60 { 61 args: []string{"--net", "host"}, 62 }, 63 } 64 for _, tc := range testCases { 65 tc := tc // IMPORTANT 66 name := "default" 67 if len(tc.args) > 0 { 68 name = strings.Join(tc.args, "_") 69 } 70 t.Run(name, func(t *testing.T) { 71 args := []string{"run", "--rm"} 72 args = append(args, tc.args...) 73 args = append(args, testutil.AlpineImage, "apk", "update") 74 cmd := base.Cmd(args...) 75 cmd.AssertOutContains("OK") 76 }) 77 } 78 } 79 80 // TestRunHostLookup tests hostname lookup 81 func TestRunHostLookup(t *testing.T) { 82 base := testutil.NewBase(t) 83 // key: container name, val: network name 84 m := map[string]string{ 85 "c0-in-n0": "n0", 86 "c1-in-n0": "n0", 87 "c2-in-n1": "n1", 88 "c3-in-bridge": "bridge", 89 } 90 customNets := valuesOfMapStringString(m) 91 defer func() { 92 for name := range m { 93 base.Cmd("rm", "-f", name).Run() 94 } 95 for netName := range customNets { 96 if netName == "bridge" { 97 continue 98 } 99 base.Cmd("network", "rm", netName).Run() 100 } 101 }() 102 103 // Create networks 104 for netName := range customNets { 105 if netName == "bridge" { 106 continue 107 } 108 base.Cmd("network", "create", netName).AssertOK() 109 } 110 111 // Create nginx containers 112 for name, netName := range m { 113 cmd := base.Cmd("run", 114 "-d", 115 "--name", name, 116 "--hostname", name+"-foobar", 117 "--net", netName, 118 testutil.NginxAlpineImage, 119 ) 120 t.Logf("creating host lookup testing container with command: %q", strings.Join(cmd.Command, " ")) 121 cmd.AssertOK() 122 } 123 124 testWget := func(srcContainer, targetHostname string, expected bool) { 125 t.Logf("resolving %q in container %q (should success: %+v)", targetHostname, srcContainer, expected) 126 cmd := base.Cmd("exec", srcContainer, "wget", "-qO-", "http://"+targetHostname) 127 if expected { 128 cmd.AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) 129 } else { 130 cmd.AssertFail() 131 } 132 } 133 134 // Tests begin 135 testWget("c0-in-n0", "c1-in-n0", true) 136 testWget("c0-in-n0", "c1-in-n0.n0", true) 137 testWget("c0-in-n0", "c1-in-n0-foobar", true) 138 testWget("c0-in-n0", "c1-in-n0-foobar.n0", true) 139 testWget("c0-in-n0", "c2-in-n1", false) 140 testWget("c0-in-n0", "c2-in-n1.n1", false) 141 testWget("c0-in-n0", "c3-in-bridge", false) 142 testWget("c1-in-n0", "c0-in-n0", true) 143 testWget("c1-in-n0", "c0-in-n0.n0", true) 144 testWget("c1-in-n0", "c0-in-n0-foobar", true) 145 testWget("c1-in-n0", "c0-in-n0-foobar.n0", true) 146 } 147 148 func TestRunPortWithNoHostPort(t *testing.T) { 149 if rootlessutil.IsRootless() { 150 t.Skip("Auto port assign is not supported rootless mode yet") 151 } 152 153 type testCase struct { 154 containerPort string 155 runShouldSuccess bool 156 } 157 testCases := []testCase{ 158 { 159 containerPort: "80", 160 runShouldSuccess: true, 161 }, 162 { 163 containerPort: "80-81", 164 runShouldSuccess: true, 165 }, 166 { 167 containerPort: "80-81/tcp", 168 runShouldSuccess: true, 169 }, 170 } 171 tID := testutil.Identifier(t) 172 for i, tc := range testCases { 173 i := i 174 tc := tc 175 tcName := fmt.Sprintf("%+v", tc) 176 t.Run(tcName, func(t *testing.T) { 177 testContainerName := fmt.Sprintf("%s-%d", tID, i) 178 base := testutil.NewBase(t) 179 defer base.Cmd("rm", "-f", testContainerName).Run() 180 pFlag := tc.containerPort 181 cmd := base.Cmd("run", "-d", 182 "--name", testContainerName, 183 "-p", pFlag, 184 testutil.NginxAlpineImage) 185 var result *icmd.Result 186 stdoutContent := "" 187 if tc.runShouldSuccess { 188 cmd.AssertOK() 189 } else { 190 cmd.AssertFail() 191 return 192 } 193 portCmd := base.Cmd("port", testContainerName) 194 portCmd.Base.T.Helper() 195 result = portCmd.Run() 196 stdoutContent = result.Stdout() + result.Stderr() 197 assert.Assert(cmd.Base.T, result.ExitCode == 0, stdoutContent) 198 regexExpression := regexp.MustCompile(`80\/tcp.*?->.*?0.0.0.0:(?P<portNumber>\d{1,5}).*?`) 199 match := regexExpression.FindStringSubmatch(stdoutContent) 200 paramsMap := make(map[string]string) 201 for i, name := range regexExpression.SubexpNames() { 202 if i > 0 && i <= len(match) { 203 paramsMap[name] = match[i] 204 } 205 } 206 if _, ok := paramsMap["portNumber"]; !ok { 207 t.Fail() 208 return 209 } 210 connectURL := fmt.Sprintf("http://%s:%s", "127.0.0.1", paramsMap["portNumber"]) 211 resp, err := nettestutil.HTTPGet(connectURL, 30, false) 212 assert.NilError(t, err) 213 respBody, err := io.ReadAll(resp.Body) 214 assert.NilError(t, err) 215 assert.Assert(t, strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet)) 216 }) 217 } 218 219 } 220 221 func TestUniqueHostPortAssignement(t *testing.T) { 222 if rootlessutil.IsRootless() { 223 t.Skip("Auto port assign is not supported rootless mode yet") 224 } 225 226 type testCase struct { 227 containerPort string 228 runShouldSuccess bool 229 } 230 231 testCases := []testCase{ 232 { 233 containerPort: "80", 234 runShouldSuccess: true, 235 }, 236 { 237 containerPort: "80-81", 238 runShouldSuccess: true, 239 }, 240 { 241 containerPort: "80-81/tcp", 242 runShouldSuccess: true, 243 }, 244 } 245 246 tID := testutil.Identifier(t) 247 248 for i, tc := range testCases { 249 i := i 250 tc := tc 251 tcName := fmt.Sprintf("%+v", tc) 252 t.Run(tcName, func(t *testing.T) { 253 testContainerName1 := fmt.Sprintf("%s-%d-1", tID, i) 254 testContainerName2 := fmt.Sprintf("%s-%d-2", tID, i) 255 base := testutil.NewBase(t) 256 defer base.Cmd("rm", "-f", testContainerName1, testContainerName2).Run() 257 258 pFlag := tc.containerPort 259 cmd1 := base.Cmd("run", "-d", 260 "--name", testContainerName1, "-p", 261 pFlag, 262 testutil.NginxAlpineImage) 263 264 cmd2 := base.Cmd("run", "-d", 265 "--name", testContainerName2, "-p", 266 pFlag, 267 testutil.NginxAlpineImage) 268 var result *icmd.Result 269 stdoutContent := "" 270 if tc.runShouldSuccess { 271 cmd1.AssertOK() 272 cmd2.AssertOK() 273 } else { 274 cmd1.AssertFail() 275 cmd2.AssertFail() 276 return 277 } 278 portCmd1 := base.Cmd("port", testContainerName1) 279 portCmd2 := base.Cmd("port", testContainerName2) 280 portCmd1.Base.T.Helper() 281 portCmd2.Base.T.Helper() 282 result = portCmd1.Run() 283 stdoutContent = result.Stdout() + result.Stderr() 284 assert.Assert(t, result.ExitCode == 0, stdoutContent) 285 port1, err := extractHostPort(stdoutContent, "80") 286 assert.NilError(t, err) 287 result = portCmd2.Run() 288 stdoutContent = result.Stdout() + result.Stderr() 289 assert.Assert(t, result.ExitCode == 0, stdoutContent) 290 port2, err := extractHostPort(stdoutContent, "80") 291 assert.NilError(t, err) 292 assert.Assert(t, port1 != port2, "Host ports are not unique") 293 294 // Make HTTP GET request to container 1 295 connectURL1 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port1) 296 resp1, err := nettestutil.HTTPGet(connectURL1, 30, false) 297 assert.NilError(t, err) 298 respBody1, err := io.ReadAll(resp1.Body) 299 assert.NilError(t, err) 300 assert.Assert(t, strings.Contains(string(respBody1), testutil.NginxAlpineIndexHTMLSnippet)) 301 302 // Make HTTP GET request to container 2 303 connectURL2 := fmt.Sprintf("http://%s:%s", "127.0.0.1", port2) 304 resp2, err := nettestutil.HTTPGet(connectURL2, 30, false) 305 assert.NilError(t, err) 306 respBody2, err := io.ReadAll(resp2.Body) 307 assert.NilError(t, err) 308 assert.Assert(t, strings.Contains(string(respBody2), testutil.NginxAlpineIndexHTMLSnippet)) 309 }) 310 } 311 } 312 313 func TestRunPort(t *testing.T) { 314 baseTestRunPort(t, testutil.NginxAlpineImage, testutil.NginxAlpineIndexHTMLSnippet, true) 315 } 316 317 func TestRunWithInvalidPortThenCleanUp(t *testing.T) { 318 // docker does not set label restriction to 4096 bytes 319 testutil.DockerIncompatible(t) 320 t.Parallel() 321 base := testutil.NewBase(t) 322 containerName := testutil.Identifier(t) 323 defer base.Cmd("rm", "-f", containerName).Run() 324 base.Cmd("run", "--rm", "--name", containerName, "-p", "22200-22299:22200-22299", testutil.CommonImage).AssertFail() 325 base.Cmd("run", "--rm", "--name", containerName, "-p", "22200-22299:22200-22299", testutil.CommonImage).AssertCombinedOutContains(errdefs.ErrInvalidArgument.Error()) 326 base.Cmd("run", "--rm", "--name", containerName, testutil.CommonImage).AssertOK() 327 } 328 329 func TestRunContainerWithStaticIP(t *testing.T) { 330 if rootlessutil.IsRootless() { 331 t.Skip("Static IP assignment is not supported rootless mode yet.") 332 } 333 networkName := "test-network" 334 networkSubnet := "172.0.0.0/16" 335 base := testutil.NewBase(t) 336 cmd := base.Cmd("network", "create", networkName, "--subnet", networkSubnet) 337 cmd.AssertOK() 338 defer base.Cmd("network", "rm", networkName).Run() 339 testCases := []struct { 340 ip string 341 shouldSuccess bool 342 useNetwork bool 343 checkTheIPAddress bool 344 }{ 345 { 346 ip: "172.0.0.2", 347 shouldSuccess: true, 348 useNetwork: true, 349 checkTheIPAddress: true, 350 }, 351 { 352 ip: "192.0.0.2", 353 shouldSuccess: false, 354 useNetwork: true, 355 checkTheIPAddress: false, 356 }, 357 // XXX see https://github.com/containerd/nerdctl/issues/3101 358 // seems the incompatibility is coming from CNI plugin upgrade. This test has been disabled in main. 359 /* 360 { 361 ip: "10.4.0.2", 362 shouldSuccess: true, 363 useNetwork: false, 364 checkTheIPAddress: false, 365 }, 366 */ 367 } 368 tID := testutil.Identifier(t) 369 for i, tc := range testCases { 370 i := i 371 tc := tc 372 tcName := fmt.Sprintf("%+v", tc) 373 t.Run(tcName, func(t *testing.T) { 374 testContainerName := fmt.Sprintf("%s-%d", tID, i) 375 base := testutil.NewBase(t) 376 defer base.Cmd("rm", "-f", testContainerName).Run() 377 args := []string{ 378 "run", "-d", "--name", testContainerName, 379 } 380 if tc.useNetwork { 381 args = append(args, []string{"--network", networkName}...) 382 } 383 args = append(args, []string{"--ip", tc.ip, testutil.NginxAlpineImage}...) 384 cmd := base.Cmd(args...) 385 if !tc.shouldSuccess { 386 cmd.AssertFail() 387 return 388 } 389 cmd.AssertOK() 390 391 if tc.checkTheIPAddress { 392 inspectCmd := base.Cmd("inspect", testContainerName, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") 393 result := inspectCmd.Run() 394 stdoutContent := result.Stdout() + result.Stderr() 395 assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent) 396 if !strings.Contains(stdoutContent, tc.ip) { 397 t.Fail() 398 return 399 } 400 } 401 }) 402 } 403 } 404 405 func TestRunDNS(t *testing.T) { 406 base := testutil.NewBase(t) 407 408 base.Cmd("run", "--rm", "--dns", "8.8.8.8", testutil.CommonImage, 409 "cat", "/etc/resolv.conf").AssertOutContains("nameserver 8.8.8.8\n") 410 base.Cmd("run", "--rm", "--dns-search", "test", testutil.CommonImage, 411 "cat", "/etc/resolv.conf").AssertOutContains("search test\n") 412 base.Cmd("run", "--rm", "--dns-search", "test", "--dns-search", "test1", testutil.CommonImage, 413 "cat", "/etc/resolv.conf").AssertOutContains("search test test1\n") 414 base.Cmd("run", "--rm", "--dns-opt", "no-tld-query", "--dns-option", "attempts:10", testutil.CommonImage, 415 "cat", "/etc/resolv.conf").AssertOutContains("options no-tld-query attempts:10\n") 416 cmd := base.Cmd("run", "--rm", "--dns", "8.8.8.8", "--dns-search", "test", "--dns-option", "attempts:10", testutil.CommonImage, 417 "cat", "/etc/resolv.conf") 418 cmd.AssertOutContains("nameserver 8.8.8.8\n") 419 cmd.AssertOutContains("search test\n") 420 cmd.AssertOutContains("options attempts:10\n") 421 } 422 423 func TestSharedNetworkStack(t *testing.T) { 424 if runtime.GOOS != "linux" { 425 t.Skip("--network=container:<container name|id> only supports linux now") 426 } 427 base := testutil.NewBase(t) 428 429 containerName := testutil.Identifier(t) 430 defer base.Cmd("rm", "-f", containerName).AssertOK() 431 base.Cmd("run", "-d", "--name", containerName, 432 testutil.NginxAlpineImage).AssertOK() 433 base.EnsureContainerStarted(containerName) 434 435 containerNameJoin := testutil.Identifier(t) + "-network" 436 defer base.Cmd("rm", "-f", containerNameJoin).AssertOK() 437 base.Cmd("run", 438 "-d", 439 "--name", containerNameJoin, 440 "--network=container:"+containerName, 441 testutil.CommonImage, 442 "sleep", "infinity").AssertOK() 443 444 base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80"). 445 AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) 446 447 base.Cmd("restart", containerName).AssertOK() 448 base.Cmd("stop", "--time=1", containerNameJoin).AssertOK() 449 base.Cmd("start", containerNameJoin).AssertOK() 450 base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80"). 451 AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) 452 } 453 454 func TestRunContainerWithMACAddress(t *testing.T) { 455 base := testutil.NewBase(t) 456 tID := testutil.Identifier(t) 457 networkBridge := "testNetworkBridge" + tID 458 networkMACvlan := "testNetworkMACvlan" + tID 459 networkIPvlan := "testNetworkIPvlan" + tID 460 base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK() 461 base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK() 462 base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK() 463 t.Cleanup(func() { 464 base.Cmd("network", "rm", networkBridge).Run() 465 base.Cmd("network", "rm", networkMACvlan).Run() 466 base.Cmd("network", "rm", networkIPvlan).Run() 467 }) 468 tests := []struct { 469 Network string 470 WantErr bool 471 Expect string 472 }{ 473 {"host", true, "conflicting options"}, 474 {"none", true, "can't open '/sys/class/net/eth0/address'"}, 475 {"container:whatever" + tID, true, "conflicting options"}, 476 {"bridge", false, ""}, 477 {networkBridge, false, ""}, 478 {networkMACvlan, false, ""}, 479 {networkIPvlan, true, "not support"}, 480 } 481 for _, test := range tests { 482 macAddress, err := nettestutil.GenerateMACAddress() 483 if err != nil { 484 t.Errorf("failed to generate MAC address: %s", err) 485 } 486 if test.Expect == "" && !test.WantErr { 487 test.Expect = macAddress 488 } 489 cmd := base.Cmd("run", "--rm", "--network", test.Network, "--mac-address", macAddress, testutil.CommonImage, "cat", "/sys/class/net/eth0/address") 490 if test.WantErr { 491 cmd.AssertFail() 492 cmd.AssertCombinedOutContains(test.Expect) 493 } else { 494 cmd.AssertOK() 495 cmd.AssertOutContains(test.Expect) 496 } 497 } 498 } 499 500 func TestHostsFileMounts(t *testing.T) { 501 base := testutil.NewBase(t) 502 503 base.Cmd("run", "--rm", testutil.CommonImage, 504 "sh", "-euxc", "echo >> /etc/hosts").AssertOK() 505 base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, 506 "sh", "-euxc", "echo >> /etc/hosts").AssertOK() 507 base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts:ro", "--network", "host", testutil.CommonImage, 508 "sh", "-euxc", "echo >> /etc/hosts").AssertFail() 509 // add a line into /etc/hosts and remove it. 510 base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, 511 "sh", "-euxc", "echo >> /etc/hosts").AssertOK() 512 base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage, 513 "sh", "-euxc", "head -n -1 /etc/hosts > temp && cat temp > /etc/hosts").AssertOK() 514 515 base.Cmd("run", "--rm", testutil.CommonImage, 516 "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() 517 base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, 518 "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() 519 base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "--network", "host", testutil.CommonImage, 520 "sh", "-euxc", "echo >> /etc/resolv.conf").AssertFail() 521 // add a line into /etc/resolv.conf and remove it. 522 base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, 523 "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK() 524 base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage, 525 "sh", "-euxc", "head -n -1 /etc/resolv.conf > temp && cat temp > /etc/resolv.conf").AssertOK() 526 } 527 528 func TestRunContainerWithStaticIP6(t *testing.T) { 529 if rootlessutil.IsRootless() { 530 t.Skip("Static IP6 assignment is not supported rootless mode yet.") 531 } 532 networkName := "test-network" 533 networkSubnet := "2001:db8:5::/64" 534 _, subnet, err := net.ParseCIDR(networkSubnet) 535 assert.Assert(t, err == nil) 536 base := testutil.NewBaseWithIPv6Compatible(t) 537 base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK() 538 t.Cleanup(func() { 539 base.Cmd("network", "rm", networkName).Run() 540 }) 541 testCases := []struct { 542 ip string 543 shouldSuccess bool 544 checkTheIPAddress bool 545 }{ 546 { 547 ip: "", 548 shouldSuccess: true, 549 checkTheIPAddress: false, 550 }, 551 { 552 ip: "2001:db8:5::6", 553 shouldSuccess: true, 554 checkTheIPAddress: true, 555 }, 556 { 557 ip: "2001:db8:4::6", 558 shouldSuccess: false, 559 checkTheIPAddress: false, 560 }, 561 } 562 tID := testutil.Identifier(t) 563 for i, tc := range testCases { 564 i := i 565 tc := tc 566 tcName := fmt.Sprintf("%+v", tc) 567 t.Run(tcName, func(t *testing.T) { 568 testContainerName := fmt.Sprintf("%s-%d", tID, i) 569 base := testutil.NewBaseWithIPv6Compatible(t) 570 args := []string{ 571 "run", "--rm", "--name", testContainerName, "--network", networkName, 572 } 573 if tc.ip != "" { 574 args = append(args, "--ip6", tc.ip) 575 } 576 args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...) 577 cmd := base.Cmd(args...) 578 if !tc.shouldSuccess { 579 cmd.AssertFail() 580 return 581 } 582 cmd.AssertOutWithFunc(func(stdout string) error { 583 ip := findIPv6(stdout) 584 if !subnet.Contains(ip) { 585 return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) 586 } 587 if tc.checkTheIPAddress { 588 if ip.String() != tc.ip { 589 return fmt.Errorf("expected ip %s, got %s", tc.ip, ip) 590 } 591 } 592 return nil 593 }) 594 }) 595 } 596 }