github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/compose_up_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 "os" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/containerd/log" 28 "github.com/containerd/nerdctl/pkg/composer/serviceparser" 29 "github.com/containerd/nerdctl/pkg/rootlessutil" 30 "github.com/docker/go-connections/nat" 31 32 "github.com/containerd/nerdctl/pkg/testutil" 33 "github.com/containerd/nerdctl/pkg/testutil/nettestutil" 34 35 "gotest.tools/v3/assert" 36 ) 37 38 func TestComposeUp(t *testing.T) { 39 base := testutil.NewBase(t) 40 testComposeUp(t, base, fmt.Sprintf(` 41 version: '3.1' 42 43 services: 44 45 wordpress: 46 image: %s 47 restart: always 48 ports: 49 - 8080:80 50 environment: 51 WORDPRESS_DB_HOST: db 52 WORDPRESS_DB_USER: exampleuser 53 WORDPRESS_DB_PASSWORD: examplepass 54 WORDPRESS_DB_NAME: exampledb 55 volumes: 56 - wordpress:/var/www/html 57 58 db: 59 image: %s 60 restart: always 61 environment: 62 MYSQL_DATABASE: exampledb 63 MYSQL_USER: exampleuser 64 MYSQL_PASSWORD: examplepass 65 MYSQL_RANDOM_ROOT_PASSWORD: '1' 66 volumes: 67 - db:/var/lib/mysql 68 69 volumes: 70 wordpress: 71 db: 72 `, testutil.WordpressImage, testutil.MariaDBImage)) 73 } 74 75 func testComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) { 76 comp := testutil.NewComposeDir(t, dockerComposeYAML) 77 defer comp.CleanUp() 78 79 projectName := comp.ProjectName() 80 t.Logf("projectName=%q", projectName) 81 82 base.ComposeCmd(append(append([]string{"-f", comp.YAMLFullPath()}, opts...), "up", "-d")...).AssertOK() 83 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 84 base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertOK() 85 base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK() 86 87 checkWordpress := func() error { 88 resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false) 89 if err != nil { 90 return err 91 } 92 respBody, err := io.ReadAll(resp.Body) 93 if err != nil { 94 return err 95 } 96 if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) { 97 t.Logf("respBody=%q", respBody) 98 return fmt.Errorf("respBody does not contain %q", testutil.WordpressIndexHTMLSnippet) 99 } 100 return nil 101 } 102 103 var wordpressWorking bool 104 for i := 0; i < 30; i++ { 105 t.Logf("(retry %d)", i) 106 err := checkWordpress() 107 if err == nil { 108 wordpressWorking = true 109 break 110 } 111 // NOTE: "<h1>Error establishing a database connection</h1>" is expected for the first few iterations 112 t.Log(err) 113 time.Sleep(3 * time.Second) 114 } 115 116 if !wordpressWorking { 117 t.Fatal("wordpress is not working") 118 } 119 t.Log("wordpress seems functional") 120 121 base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() 122 base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail() 123 base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail() 124 } 125 126 func TestComposeUpBuild(t *testing.T) { 127 testutil.RequiresBuild(t) 128 base := testutil.NewBase(t) 129 defer base.Cmd("builder", "prune").Run() 130 131 const dockerComposeYAML = ` 132 services: 133 web: 134 build: . 135 ports: 136 - 8080:80 137 ` 138 dockerfile := fmt.Sprintf(`FROM %s 139 COPY index.html /usr/share/nginx/html/index.html 140 `, testutil.NginxAlpineImage) 141 indexHTML := t.Name() 142 143 comp := testutil.NewComposeDir(t, dockerComposeYAML) 144 defer comp.CleanUp() 145 projectName := comp.ProjectName() 146 t.Logf("projectName=%q", projectName) 147 148 comp.WriteFile("Dockerfile", dockerfile) 149 comp.WriteFile("index.html", indexHTML) 150 151 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() 152 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 153 154 resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false) 155 assert.NilError(t, err) 156 respBody, err := io.ReadAll(resp.Body) 157 assert.NilError(t, err) 158 t.Logf("respBody=%q", respBody) 159 assert.Assert(t, strings.Contains(string(respBody), indexHTML)) 160 } 161 162 func TestComposeUpNetWithStaticIP(t *testing.T) { 163 if rootlessutil.IsRootless() { 164 t.Skip("Static IP assignment is not supported rootless mode yet.") 165 } 166 base := testutil.NewBase(t) 167 staticIP := "172.20.0.12" 168 var dockerComposeYAML = fmt.Sprintf(` 169 version: '3.1' 170 171 services: 172 svc0: 173 image: %s 174 networks: 175 net0: 176 ipv4_address: %s 177 178 networks: 179 net0: 180 ipam: 181 config: 182 - subnet: 172.20.0.0/24 183 `, testutil.NginxAlpineImage, staticIP) 184 comp := testutil.NewComposeDir(t, dockerComposeYAML) 185 defer comp.CleanUp() 186 projectName := comp.ProjectName() 187 t.Logf("projectName=%q", projectName) 188 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 189 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 190 191 svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") 192 inspectCmd := base.Cmd("inspect", svc0, "--format", "\"{{range .NetworkSettings.Networks}} {{.IPAddress}}{{end}}\"") 193 result := inspectCmd.Run() 194 stdoutContent := result.Stdout() + result.Stderr() 195 assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent) 196 if !strings.Contains(stdoutContent, staticIP) { 197 log.L.Errorf("test failed, the actual container ip is %s", stdoutContent) 198 t.Fail() 199 return 200 } 201 } 202 203 func TestComposeUpMultiNet(t *testing.T) { 204 base := testutil.NewBase(t) 205 206 var dockerComposeYAML = fmt.Sprintf(` 207 version: '3.1' 208 209 services: 210 svc0: 211 image: %s 212 networks: 213 - net0 214 - net1 215 - net2 216 svc1: 217 image: %s 218 networks: 219 - net0 220 - net1 221 svc2: 222 image: %s 223 networks: 224 - net2 225 226 networks: 227 net0: {} 228 net1: {} 229 net2: {} 230 `, testutil.NginxAlpineImage, testutil.NginxAlpineImage, testutil.NginxAlpineImage) 231 comp := testutil.NewComposeDir(t, dockerComposeYAML) 232 defer comp.CleanUp() 233 234 projectName := comp.ProjectName() 235 t.Logf("projectName=%q", projectName) 236 237 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 238 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 239 240 svc0 := serviceparser.DefaultContainerName(projectName, "svc0", "1") 241 svc1 := serviceparser.DefaultContainerName(projectName, "svc1", "1") 242 svc2 := serviceparser.DefaultContainerName(projectName, "svc2", "1") 243 244 base.Cmd("exec", svc0, "ping", "-c", "1", "svc0").AssertOK() 245 base.Cmd("exec", svc0, "ping", "-c", "1", "svc1").AssertOK() 246 base.Cmd("exec", svc0, "ping", "-c", "1", "svc2").AssertOK() 247 base.Cmd("exec", svc1, "ping", "-c", "1", "svc0").AssertOK() 248 base.Cmd("exec", svc2, "ping", "-c", "1", "svc0").AssertOK() 249 base.Cmd("exec", svc1, "ping", "-c", "1", "svc2").AssertFail() 250 } 251 252 func TestComposeUpOsEnvVar(t *testing.T) { 253 base := testutil.NewBase(t) 254 const containerName = "nginxAlpine" 255 var dockerComposeYAML = fmt.Sprintf(` 256 version: '3.1' 257 258 services: 259 svc1: 260 image: %s 261 container_name: %s 262 ports: 263 - ${ADDRESS:-127.0.0.1}:8080:80 264 `, testutil.NginxAlpineImage, containerName) 265 266 comp := testutil.NewComposeDir(t, dockerComposeYAML) 267 defer comp.CleanUp() 268 projectName := comp.ProjectName() 269 t.Logf("projectName=%q", projectName) 270 271 base.Env = append(os.Environ(), "ADDRESS=0.0.0.0") 272 273 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 274 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 275 276 inspect := base.InspectContainer(containerName) 277 inspect80TCP := (*inspect.NetworkSettings.Ports)["80/tcp"] 278 expected := nat.PortBinding{ 279 HostIP: "0.0.0.0", 280 HostPort: "8080", 281 } 282 assert.Equal(base.T, expected, inspect80TCP[0]) 283 } 284 285 func TestComposeUpDotEnvFile(t *testing.T) { 286 base := testutil.NewBase(t) 287 288 var dockerComposeYAML = ` 289 version: '3.1' 290 291 services: 292 svc3: 293 image: ghcr.io/stargz-containers/nginx:$TAG 294 ` 295 296 comp := testutil.NewComposeDir(t, dockerComposeYAML) 297 defer comp.CleanUp() 298 projectName := comp.ProjectName() 299 t.Logf("projectName=%q", projectName) 300 301 envFile := `TAG=1.19-alpine-org` 302 comp.WriteFile(".env", envFile) 303 304 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 305 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 306 } 307 308 func TestComposeUpEnvFileNotFoundError(t *testing.T) { 309 base := testutil.NewBase(t) 310 311 var dockerComposeYAML = ` 312 version: '3.1' 313 314 services: 315 svc4: 316 image: ghcr.io/stargz-containers/nginx:$TAG 317 ` 318 319 comp := testutil.NewComposeDir(t, dockerComposeYAML) 320 defer comp.CleanUp() 321 projectName := comp.ProjectName() 322 t.Logf("projectName=%q", projectName) 323 324 envFile := `TAG=1.19-alpine-org` 325 comp.WriteFile("envFile", envFile) 326 327 //env-file is relative to the current working directory and not the project directory 328 base.ComposeCmd("-f", comp.YAMLFullPath(), "--env-file", "envFile", "up", "-d").AssertFail() 329 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 330 } 331 332 func TestComposeUpWithScale(t *testing.T) { 333 base := testutil.NewBase(t) 334 335 var dockerComposeYAML = fmt.Sprintf(` 336 version: '3.1' 337 338 services: 339 test: 340 image: %s 341 command: "sleep infinity" 342 `, testutil.AlpineImage) 343 344 comp := testutil.NewComposeDir(t, dockerComposeYAML) 345 defer comp.CleanUp() 346 projectName := comp.ProjectName() 347 t.Logf("projectName=%q", projectName) 348 349 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--scale", "test=2").AssertOK() 350 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 351 352 base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutContains(serviceparser.DefaultContainerName(projectName, "test", "2")) 353 } 354 355 func TestComposeIPAMConfig(t *testing.T) { 356 base := testutil.NewBase(t) 357 358 var dockerComposeYAML = fmt.Sprintf(` 359 version: '3.1' 360 361 services: 362 foo: 363 image: %s 364 command: "sleep infinity" 365 366 networks: 367 default: 368 ipam: 369 config: 370 - subnet: 10.1.100.0/24 371 `, testutil.AlpineImage) 372 373 comp := testutil.NewComposeDir(t, dockerComposeYAML) 374 defer comp.CleanUp() 375 projectName := comp.ProjectName() 376 t.Logf("projectName=%q", projectName) 377 378 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 379 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 380 381 base.Cmd("inspect", "-f", `{{json .NetworkSettings.Networks }}`, serviceparser.DefaultContainerName(projectName, "foo", "1")).AssertOutContains("10.1.100.") 382 } 383 384 func TestComposeUpRemoveOrphans(t *testing.T) { 385 base := testutil.NewBase(t) 386 387 var ( 388 dockerComposeYAMLOrphan = fmt.Sprintf(` 389 version: '3.1' 390 391 services: 392 test: 393 image: %s 394 command: "sleep infinity" 395 `, testutil.AlpineImage) 396 397 dockerComposeYAMLFull = fmt.Sprintf(` 398 %s 399 orphan: 400 image: %s 401 command: "sleep infinity" 402 `, dockerComposeYAMLOrphan, testutil.AlpineImage) 403 ) 404 405 compOrphan := testutil.NewComposeDir(t, dockerComposeYAMLOrphan) 406 defer compOrphan.CleanUp() 407 compFull := testutil.NewComposeDir(t, dockerComposeYAMLFull) 408 defer compFull.CleanUp() 409 410 projectName := fmt.Sprintf("nerdctl-compose-test-%d", time.Now().Unix()) 411 t.Logf("projectName=%q", projectName) 412 413 orphanContainer := serviceparser.DefaultContainerName(projectName, "orphan", "1") 414 415 base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK() 416 defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run() 417 base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d").AssertOK() 418 base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutContains(orphanContainer) 419 base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "up", "-d", "--remove-orphans").AssertOK() 420 base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutNotContains(orphanContainer) 421 } 422 423 func TestComposeUpIdempotent(t *testing.T) { 424 base := testutil.NewBase(t) 425 426 var dockerComposeYAML = fmt.Sprintf(` 427 version: '3.1' 428 429 services: 430 test: 431 image: %s 432 command: "sleep infinity" 433 `, testutil.AlpineImage) 434 435 comp := testutil.NewComposeDir(t, dockerComposeYAML) 436 defer comp.CleanUp() 437 projectName := comp.ProjectName() 438 t.Logf("projectName=%q", projectName) 439 440 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 441 defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() 442 base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() 443 base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() 444 } 445 446 func TestComposeUpWithExternalNetwork(t *testing.T) { 447 containerName1 := testutil.Identifier(t) + "-1" 448 containerName2 := testutil.Identifier(t) + "-2" 449 networkName := testutil.Identifier(t) + "-network" 450 var dockerComposeYaml1 = fmt.Sprintf(` 451 version: "3" 452 services: 453 %s: 454 image: %s 455 container_name: %s 456 networks: 457 %s: 458 aliases: 459 - nginx-1 460 networks: 461 %s: 462 external: true 463 `, containerName1, testutil.NginxAlpineImage, containerName1, networkName, networkName) 464 var dockerComposeYaml2 = fmt.Sprintf(` 465 version: "3" 466 services: 467 %s: 468 image: %s 469 container_name: %s 470 networks: 471 %s: 472 aliases: 473 - nginx-2 474 networks: 475 %s: 476 external: true 477 `, containerName2, testutil.NginxAlpineImage, containerName2, networkName, networkName) 478 comp1 := testutil.NewComposeDir(t, dockerComposeYaml1) 479 defer comp1.CleanUp() 480 comp2 := testutil.NewComposeDir(t, dockerComposeYaml2) 481 defer comp2.CleanUp() 482 base := testutil.NewBase(t) 483 // Create the test network 484 base.Cmd("network", "create", networkName).AssertOK() 485 defer base.Cmd("network", "rm", networkName).Run() 486 // Run the first compose 487 base.ComposeCmd("-f", comp1.YAMLFullPath(), "up", "-d").AssertOK() 488 defer base.ComposeCmd("-f", comp1.YAMLFullPath(), "down", "-v").Run() 489 // Run the second compose 490 base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() 491 defer base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").Run() 492 // Down the second compose 493 base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").AssertOK() 494 // Run the second compose again 495 base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() 496 base.Cmd("exec", containerName1, "wget", "-qO-", "http://"+containerName2).AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet) 497 } 498 499 func TestComposeUpWithBypass4netns(t *testing.T) { 500 // docker does not support bypass4netns mode 501 testutil.DockerIncompatible(t) 502 if !rootlessutil.IsRootless() { 503 t.Skip("test needs rootless") 504 } 505 testutil.RequireKernelVersion(t, ">= 5.9.0-0") 506 testutil.RequireSystemService(t, "bypass4netnsd") 507 base := testutil.NewBase(t) 508 testComposeUp(t, base, fmt.Sprintf(` 509 version: '3.1' 510 511 services: 512 513 wordpress: 514 image: %s 515 restart: always 516 ports: 517 - 8080:80 518 environment: 519 WORDPRESS_DB_HOST: db 520 WORDPRESS_DB_USER: exampleuser 521 WORDPRESS_DB_PASSWORD: examplepass 522 WORDPRESS_DB_NAME: exampledb 523 volumes: 524 - wordpress:/var/www/html 525 labels: 526 - nerdctl/bypass4netns=1 527 528 db: 529 image: %s 530 restart: always 531 environment: 532 MYSQL_DATABASE: exampledb 533 MYSQL_USER: exampleuser 534 MYSQL_PASSWORD: examplepass 535 MYSQL_RANDOM_ROOT_PASSWORD: '1' 536 volumes: 537 - db:/var/lib/mysql 538 labels: 539 - nerdctl/bypass4netns=1 540 541 volumes: 542 wordpress: 543 db: 544 `, testutil.WordpressImage, testutil.MariaDBImage)) 545 } 546 547 func TestComposeUpProfile(t *testing.T) { 548 base := testutil.NewBase(t) 549 serviceRegular := testutil.Identifier(t) + "-regular" 550 serviceProfiled := testutil.Identifier(t) + "-profiled" 551 552 dockerComposeYAML := fmt.Sprintf(` 553 services: 554 %s: 555 image: %[3]s 556 557 %[2]s: 558 image: %[3]s 559 profiles: 560 - test-profile 561 `, serviceRegular, serviceProfiled, testutil.NginxAlpineImage) 562 563 // * Test with profile 564 // Should run both the services: 565 // - matching active profile 566 // - one without profile 567 comp1 := testutil.NewComposeDir(t, dockerComposeYAML) 568 defer comp1.CleanUp() 569 base.ComposeCmd("-f", comp1.YAMLFullPath(), "--profile", "test-profile", "up", "-d").AssertOK() 570 571 psCmd := base.Cmd("ps", "-a", "--format={{.Names}}") 572 psCmd.AssertOutContains(serviceRegular) 573 psCmd.AssertOutContains(serviceProfiled) 574 base.ComposeCmd("-f", comp1.YAMLFullPath(), "--profile", "test-profile", "down", "-v").AssertOK() 575 576 // * Test without profile 577 // Should run: 578 // - service without profile 579 comp2 := testutil.NewComposeDir(t, dockerComposeYAML) 580 defer comp2.CleanUp() 581 base.ComposeCmd("-f", comp2.YAMLFullPath(), "up", "-d").AssertOK() 582 defer base.ComposeCmd("-f", comp2.YAMLFullPath(), "down", "-v").AssertOK() 583 584 psCmd = base.Cmd("ps", "-a", "--format={{.Names}}") 585 psCmd.AssertOutContains(serviceRegular) 586 psCmd.AssertOutNotContains(serviceProfiled) 587 }