github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/tiltfile_docker_compose_test.go (about) 1 package tiltfile 2 3 import ( 4 "fmt" 5 "path/filepath" 6 "strconv" 7 "testing" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 "golang.org/x/mod/semver" 12 13 "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" 14 ctrltiltfile "github.com/tilt-dev/tilt/internal/controllers/apis/tiltfile" 15 "github.com/tilt-dev/tilt/internal/dockercompose" 16 "github.com/tilt-dev/tilt/pkg/model" 17 ) 18 19 const simpleConfig = `version: '3' 20 services: 21 foo: 22 build: ./foo 23 command: sleep 100 24 ports: 25 - "12312:80"` 26 27 const configWithMounts = `version: '3.2' 28 services: 29 foo: 30 build: ./foo 31 command: sleep 100 32 volumes: 33 - ./foo:/foo 34 # these volumes are currently unsupported, but included here to ensure we don't blow up on them 35 - bar:/bar 36 - type: volume 37 source: baz 38 target: /baz 39 ports: 40 - "12312:80" 41 volumes: 42 bar: {} 43 baz: {}` 44 45 const barServiceConfig = `version: '3' 46 services: 47 bar: 48 image: bar-image 49 expose: 50 - "3000" 51 depends_on: 52 - foo 53 ` 54 55 const twoServiceConfig = `version: '3' 56 services: 57 foo: 58 build: ./foo 59 command: sleep 100 60 ports: 61 - "12312:80" 62 bar: 63 image: bar-image 64 expose: 65 - "3000" 66 depends_on: 67 - foo 68 ` 69 70 const twoServiceConfigWithProfiles = `version: '3' 71 services: 72 foo: 73 build: ./foo 74 command: sleep 100 75 ports: 76 - "12312:80" 77 bar: 78 image: bar-image 79 expose: 80 - "3000" 81 depends_on: 82 - foo 83 profiles: 84 - barprofile 85 ` 86 87 const threeServiceConfig = `version: '3' 88 services: 89 db: 90 image: db-image 91 foo: 92 image: foo-image 93 command: sleep 100 94 ports: 95 - "12312:80" 96 depends_on: 97 - db 98 bar: 99 image: bar-image 100 expose: 101 - "3000" 102 depends_on: 103 - db 104 - foo 105 ` 106 107 // YAML for Foo config looks a little different from the above after being read into 108 // a struct and YAML'd back out... 109 func (f *fixture) simpleConfigAfterParse() string { 110 return fmt.Sprintf(`build: 111 context: %s 112 dockerfile: Dockerfile 113 command: 114 - sleep 115 - "100" 116 networks: 117 default: null 118 ports: 119 - mode: ingress 120 target: 80 121 published: "12312" 122 protocol: tcp`, f.JoinPath("foo")) 123 } 124 125 func TestDockerComposeNothingError(t *testing.T) { 126 f := newFixture(t) 127 128 f.file("Tiltfile", "docker_compose(None)") 129 130 f.loadErrString("Nothing to compose") 131 } 132 133 func TestBuildURL(t *testing.T) { 134 f := newFixture(t) 135 136 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 137 138 f.file("docker-compose.yml", `services: 139 app: 140 command: sh -c 'node server.js' 141 build: https://github.com/tilt-dev/tilt-docker-compose-example.git 142 ports: 143 - published: 3000 144 target: 30 145 `) 146 f.load() 147 } 148 149 func TestDockerComposeBadTypeError(t *testing.T) { 150 f := newFixture(t) 151 152 f.file("Tiltfile", "docker_compose(True)") 153 154 f.loadErrString("expected blob | path (string). Actual type: starlark.Bool") 155 } 156 157 func TestDockerComposeManifest(t *testing.T) { 158 f := newFixture(t) 159 160 f.dockerfile(filepath.Join("foo", "Dockerfile")) 161 f.file("docker-compose.yml", simpleConfig) 162 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 163 164 f.load() 165 f.assertDcManifest("foo", 166 dcServiceYAML(f.simpleConfigAfterParse()), 167 dockerComposeManagedImage(f.JoinPath("foo", "Dockerfile"), f.JoinPath("foo")), 168 dcPublishedPorts(12312), 169 ) 170 171 expectedConfFiles := []string{ 172 "Tiltfile", 173 ".tiltignore", 174 "docker-compose.yml", 175 f.JoinPath("foo", ".dockerignore"), 176 } 177 f.assertConfigFiles(expectedConfFiles...) 178 } 179 180 func TestDockerComposeEnvFile(t *testing.T) { 181 f := newFixture(t) 182 183 f.file("docker-compose.yml", `services: 184 bar: 185 image: bar-image 186 ports: 187 - "$BAR_PORT:$BAR_PORT" 188 `) 189 f.file("local.env", "BAR_PORT=4000\n") 190 f.file("Tiltfile", "docker_compose('docker-compose.yml', env_file='local.env')") 191 192 f.load() 193 f.assertDcManifest("bar", dcPublishedPorts(4000)) 194 195 expectedConfFiles := []string{ 196 "Tiltfile", 197 ".tiltignore", 198 "local.env", 199 "docker-compose.yml", 200 } 201 f.assertConfigFiles(expectedConfFiles...) 202 } 203 204 func TestDockerComposeServiceEnvFile(t *testing.T) { 205 f := newFixture(t) 206 207 f.file("docker-compose.yml", `services: 208 bar: 209 image: bar-image 210 env_file: 211 - bar.env 212 `) 213 f.file("bar.env", "BAR_PORT=4000\n") 214 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 215 216 f.load() 217 f.assertDcManifest("bar") 218 219 expectedConfFiles := []string{ 220 "Tiltfile", 221 ".tiltignore", 222 "docker-compose.yml", 223 "bar.env", 224 } 225 f.assertConfigFiles(expectedConfFiles...) 226 } 227 228 func TestDockerComposeProjectName(t *testing.T) { 229 f := newFixture(t) 230 231 f.dockerfile(filepath.Join("foo", "Dockerfile")) 232 f.file("docker-compose.yml", simpleConfig) 233 f.file("Tiltfile", `docker_compose('docker-compose.yml', project_name='hello')`) 234 235 f.load() 236 m := f.assertDcManifest("foo") 237 require.Equal(t, "hello", m.DockerComposeTarget().Spec.Project.Name) 238 } 239 240 func TestDockerComposeConflict(t *testing.T) { 241 f := newFixture(t) 242 243 f.dockerfile(filepath.Join("foo", "Dockerfile")) 244 f.file("docker-compose.yml", simpleConfig) 245 f.file("Tiltfile", ` 246 local_resource("foo", "foo") 247 docker_compose('docker-compose.yml') 248 `) 249 250 f.loadErrString(`local_resource named "foo" already exists`) 251 } 252 253 func TestDockerComposeYAMLBlob(t *testing.T) { 254 f := newFixture(t) 255 256 f.dockerfile(filepath.Join("foo", "Dockerfile")) 257 f.file("docker-compose.yml", simpleConfig) 258 f.file("Tiltfile", "docker_compose(read_file('docker-compose.yml'))") 259 260 f.load() 261 f.assertDcManifest("foo", 262 dcServiceYAML(f.simpleConfigAfterParse()), 263 dockerComposeManagedImage(f.JoinPath("foo", "Dockerfile"), f.JoinPath("foo")), 264 dcPublishedPorts(12312), 265 ) 266 267 expectedConfFiles := []string{ 268 "Tiltfile", 269 ".tiltignore", 270 "docker-compose.yml", 271 f.JoinPath("foo", ".dockerignore"), 272 } 273 f.assertConfigFiles(expectedConfFiles...) 274 } 275 276 func TestDockerComposeTwoInlineBlobs(t *testing.T) { 277 f := newFixture(t) 278 279 f.dockerfile(filepath.Join("foo", "Dockerfile")) 280 f.file("Tiltfile", fmt.Sprintf(`docker_compose([blob("""\n%s\n"""), blob("""\n%s\n""")])`, simpleConfig, barServiceConfig)) 281 282 f.load() 283 284 assert.Equal(t, 2, len(f.loadResult.Manifests)) 285 } 286 287 func TestDockerComposeBlobAndFileUsesFileDirForProjectPath(t *testing.T) { 288 f := newFixture(t) 289 290 f.dockerfile(filepath.Join("foo", "Dockerfile")) 291 f.file("docker-compose.yml", simpleConfig) 292 f.file("Tiltfile", fmt.Sprintf(`docker_compose([blob("""\n%s\n"""), 'docker-compose.yml'])`, barServiceConfig)) 293 294 f.load() 295 296 assert.Equal(t, 2, len(f.loadResult.Manifests)) 297 f.assertDcManifest("foo", 298 dcServiceYAML(f.simpleConfigAfterParse()), 299 dockerComposeManagedImage(f.JoinPath("foo", "Dockerfile"), f.JoinPath("foo")), 300 dcPublishedPorts(12312), 301 ) 302 } 303 304 func TestDockerComposeManifestNoDockerfile(t *testing.T) { 305 f := newFixture(t) 306 307 f.file("docker-compose.yml", `version: '3' 308 services: 309 bar: 310 image: redis:alpine`) 311 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 312 313 expectedYAML := `image: redis:alpine 314 networks: 315 default: null` 316 317 f.load("bar") 318 f.assertDcManifest("bar", 319 dcServiceYAML(expectedYAML), 320 noImage(), 321 // TODO(maia): assert m.tiltFilename 322 ) 323 324 expectedConfFiles := []string{"Tiltfile", ".tiltignore", "docker-compose.yml"} 325 f.assertConfigFiles(expectedConfFiles...) 326 } 327 328 func TestDockerComposeManifestAlternateDockerfile(t *testing.T) { 329 f := newFixture(t) 330 331 f.dockerfile("baz/alternate-Dockerfile") 332 f.file("docker-compose.yml", fmt.Sprintf(` 333 version: '3' 334 services: 335 baz: 336 build: 337 context: %s 338 dockerfile: alternate-Dockerfile`, f.JoinPath("baz"))) 339 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 340 341 expectedYAML := fmt.Sprintf(`build: 342 context: %s 343 dockerfile: alternate-Dockerfile 344 networks: 345 default: null`, 346 f.JoinPath("baz")) 347 348 f.load("baz") 349 f.assertDcManifest("baz", 350 dcServiceYAML(expectedYAML), 351 dockerComposeManagedImage(f.JoinPath("baz", "alternate-Dockerfile"), f.JoinPath("baz")), 352 // TODO(maia): assert m.tiltFilename 353 ) 354 355 expectedConfFiles := []string{"Tiltfile", ".tiltignore", "docker-compose.yml", "baz/.dockerignore"} 356 f.assertConfigFiles(expectedConfFiles...) 357 } 358 359 func TestDockerComposeManifestAbsoluteDockerfile(t *testing.T) { 360 f := newFixture(t) 361 362 dockerfilePath := f.JoinPath("baz", "Dockerfile") 363 f.dockerfile(dockerfilePath) 364 f.file("docker-compose.yml", fmt.Sprintf(` 365 version: '3' 366 services: 367 baz: 368 build: 369 context: %s 370 dockerfile: %s`, f.JoinPath("baz"), dockerfilePath)) 371 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 372 373 expectedYAML := fmt.Sprintf(`build: 374 context: %s 375 dockerfile: %s 376 networks: 377 default: null`, 378 f.JoinPath("baz"), 379 dockerfilePath) 380 381 f.load("baz") 382 f.assertDcManifest("baz", 383 dcServiceYAML(expectedYAML), 384 dockerComposeManagedImage(f.JoinPath("baz", "alternate-Dockerfile"), f.JoinPath("baz")), 385 // TODO(maia): assert m.tiltFilename 386 ) 387 388 expectedConfFiles := []string{"Tiltfile", ".tiltignore", "docker-compose.yml", "baz/.dockerignore"} 389 f.assertConfigFiles(expectedConfFiles...) 390 } 391 392 func TestDockerComposeManifestAlternateDockerfileAndDockerIgnore(t *testing.T) { 393 f := newFixture(t) 394 395 f.dockerfile("baz/alternate-Dockerfile") 396 f.dockerignore("baz/alternate-Dockerfile.dockerignore") 397 f.file("docker-compose.yml", fmt.Sprintf(` 398 version: '3' 399 services: 400 baz: 401 build: 402 context: %s 403 dockerfile: alternate-Dockerfile`, f.JoinPath("baz"))) 404 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 405 406 expectedYAML := fmt.Sprintf(`build: 407 context: %s 408 dockerfile: alternate-Dockerfile 409 networks: 410 default: null`, 411 f.JoinPath("baz")) 412 413 f.load("baz") 414 f.assertDcManifest("baz", 415 dcServiceYAML(expectedYAML), 416 dockerComposeManagedImage(f.JoinPath("baz", "alternate-Dockerfile"), f.JoinPath("baz")), 417 // TODO(maia): assert m.tiltFilename 418 ) 419 420 expectedConfFiles := []string{ 421 "Tiltfile", 422 ".tiltignore", 423 "docker-compose.yml", 424 "baz/alternate-Dockerfile.dockerignore", 425 } 426 f.assertConfigFiles(expectedConfFiles...) 427 } 428 429 func TestMultipleDockerComposeDifferentDirs(t *testing.T) { 430 f := newFixture(t) 431 432 f.dockerfile(filepath.Join("foo", "Dockerfile")) 433 f.file("docker-compose1.yml", simpleConfig) 434 435 f.dockerfile(filepath.Join("subdir", "foo", "Dockerfile")) 436 f.file(filepath.Join("subdir", "Tiltfile"), `docker_compose('docker-compose2.yml')`) 437 f.file(filepath.Join("subdir", "docker-compose2.yml"), simpleConfig) 438 439 tf := ` 440 include('./subdir/Tiltfile') 441 dc_resource('foo', project_name='subdir', new_name='foo2') 442 docker_compose('docker-compose1.yml')` 443 f.file("Tiltfile", tf) 444 445 f.load() 446 447 assert.Equal(t, 2, len(f.loadResult.Manifests)) 448 } 449 450 func TestMultipleDockerComposeNameConflict(t *testing.T) { 451 f := newFixture(t) 452 453 f.dockerfile(filepath.Join("foo", "Dockerfile")) 454 f.file("docker-compose1.yml", simpleConfig) 455 456 f.dockerfile(filepath.Join("subdir", "foo", "Dockerfile")) 457 f.file(filepath.Join("subdir", "Tiltfile"), `docker_compose('docker-compose2.yml')`) 458 f.file(filepath.Join("subdir", "docker-compose2.yml"), simpleConfig) 459 460 tf := ` 461 include('./subdir/Tiltfile') 462 docker_compose('docker-compose1.yml')` 463 f.file("Tiltfile", tf) 464 465 f.loadErrString(`dc_resource named "foo" already exists`) 466 } 467 468 func TestDockerComposeNewNameWithDependencies(t *testing.T) { 469 for _, testCase := range []struct { 470 name string 471 renames map[string]string 472 }{ 473 { 474 "default", 475 make(map[string]string), 476 }, 477 { 478 "rename db", 479 map[string]string{"db": "db2"}, 480 }, 481 { 482 "rename foo", 483 map[string]string{"foo": "foo2"}, 484 }, 485 { 486 "rename bar", 487 map[string]string{"bar": "bar2"}, 488 }, 489 { 490 "rename foo + bar", 491 map[string]string{ 492 "foo": "foo2", 493 "bar": "bar2", 494 }, 495 }, 496 { 497 "rename db + foo + bar", 498 map[string]string{ 499 "db": "db2", 500 "foo": "foo2", 501 "bar": "bar2", 502 }, 503 }, 504 } { 505 t.Run(testCase.name, func(t *testing.T) { 506 f := newFixture(t) 507 508 f.file("docker-compose.yml", threeServiceConfig) 509 510 tf := "docker_compose('docker-compose.yml')\n" 511 512 allNames := map[string]string{ 513 "db": "db", 514 "foo": "foo", 515 "bar": "bar", 516 } 517 518 for oldName, newName := range testCase.renames { 519 tf += fmt.Sprintf("dc_resource('%s', new_name='%s')\n", oldName, newName) 520 allNames[oldName] = newName 521 } 522 523 f.file("Tiltfile", tf) 524 525 f.load() 526 527 f.assertNextManifest(model.ManifestName(allNames["db"]), resourceDeps()) 528 f.assertNextManifest(model.ManifestName(allNames["foo"]), resourceDeps(allNames["db"])) 529 f.assertNextManifest(model.ManifestName(allNames["bar"]), resourceDeps(allNames["db"], allNames["foo"])) 530 }) 531 } 532 } 533 534 func TestMultipleDockerComposeSameDir(t *testing.T) { 535 f := newFixture(t) 536 537 f.dockerfile(filepath.Join("foo", "Dockerfile")) 538 f.file("docker-compose1.yml", simpleConfig) 539 f.file("docker-compose2.yml", barServiceConfig) 540 541 tf := ` 542 docker_compose('docker-compose1.yml') 543 docker_compose('docker-compose2.yml')` 544 f.file("Tiltfile", tf) 545 546 f.load() 547 548 assert.Equal(t, 2, len(f.loadResult.Manifests)) 549 } 550 551 func TestDockerComposeAndK8sSupported(t *testing.T) { 552 f := newFixture(t) 553 554 f.setupFooAndBar() 555 f.file("docker-compose.yml", simpleConfig) 556 tf := `docker_compose('docker-compose.yml') 557 k8s_yaml('bar.yaml')` 558 f.file("Tiltfile", tf) 559 560 f.load() 561 562 assert.Equal(t, 2, len(f.loadResult.Manifests)) 563 } 564 565 func TestResourceConflictCombinations(t *testing.T) { 566 tt := [][2]string{ 567 {`docker_compose('docker-compose.yml') 568 k8s_yaml('foo.yaml')`, `dc_resource named "foo" already exists`}, 569 {`k8s_yaml('foo.yaml') 570 docker_compose('docker-compose.yml')`, `dc_resource named "foo" already exists`}, 571 {`docker_compose('docker-compose.yml') 572 local_resource('foo', 'echo hello')`, `dc_resource named "foo" already exists`}, 573 {`local_resource('foo', 'echo hello') 574 docker_compose('docker-compose.yml')`, `local_resource named "foo" already exists`}, 575 } 576 577 for i, tc := range tt { 578 t.Run(strconv.Itoa(i), func(t *testing.T) { 579 f := newFixture(t) 580 f.setupFooAndBar() 581 f.file("docker-compose.yml", simpleConfig) 582 f.file("Tiltfile", tc[0]) 583 f.loadErrString(tc[1]) 584 }) 585 } 586 } 587 588 func TestDockerComposeResourceCreationFromAbsPath(t *testing.T) { 589 f := newFixture(t) 590 591 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 592 f.dockerfile(filepath.Join("foo", "Dockerfile")) 593 f.file("docker-compose.yml", ` 594 version: '3' 595 services: 596 foo: 597 build: ./foo 598 command: sleep 100 599 ports: 600 - "12312:80"`) 601 f.file("Tiltfile", fmt.Sprintf("docker_compose(%q)", configPath)) 602 603 f.load("foo") 604 f.assertDcManifest("foo") 605 } 606 607 func TestDockerComposeMultiStageBuild(t *testing.T) { 608 f := newFixture(t) 609 610 df := `FROM alpine as builder 611 ADD ./src /app 612 RUN echo hi 613 614 FROM alpine 615 COPY --from=builder /app /app 616 RUN echo bye` 617 f.file(filepath.Join("foo", "Dockerfile"), df) 618 f.file(filepath.Join("foo", "docker-compose.yml"), `version: '3' 619 services: 620 foo: 621 build: 622 context: ./ 623 command: sleep 100 624 ports: 625 - "12312:80"`) 626 f.file("Tiltfile", "docker_compose('foo/docker-compose.yml')") 627 f.load("foo") 628 f.assertDcManifest("foo", 629 dcServiceYAML(f.simpleConfigAfterParse()), 630 dockerComposeManagedImage(f.JoinPath("foo", "Dockerfile"), f.JoinPath("foo")), 631 dcPublishedPorts(12312), 632 ) 633 634 expectedConfFiles := []string{ 635 "Tiltfile", 636 ".tiltignore", 637 filepath.Join("foo", "docker-compose.yml"), 638 filepath.Join("foo", ".dockerignore"), 639 } 640 f.assertConfigFiles(expectedConfFiles...) 641 } 642 643 func TestDockerComposeHonorsDockerIgnore(t *testing.T) { 644 f := newFixture(t) 645 646 df := `FROM alpine 647 648 ADD . /app 649 COPY ./thing.go /stuff 650 RUN echo hi` 651 f.file(filepath.Join("foo", "Dockerfile"), df) 652 653 f.file("docker-compose.yml", simpleConfig) 654 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 655 656 // the build context is ./foo so tmp should be ignored 657 f.file(filepath.Join("foo", ".dockerignore"), "tmp") 658 // this dockerignore is unrelated despite being a sibling to docker-compose.yml, so won't be used 659 f.file(".dockerignore", "foo/tmp2") 660 661 f.load("foo") 662 663 f.assertNextManifest("foo", 664 fileChangeMatches(filepath.Join("foo", "tmp2")), 665 fileChangeFilters(filepath.Join("foo", "tmp")), 666 ) 667 } 668 669 func TestDockerComposeIgnoresFileChangesOnMountedVolumes(t *testing.T) { 670 f := newFixture(t) 671 672 df := `FROM alpine 673 674 ADD . /app 675 COPY ./thing.go /stuff 676 RUN echo hi` 677 f.file(filepath.Join("foo", "Dockerfile"), df) 678 679 f.file("docker-compose.yml", configWithMounts) 680 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 681 682 f.load("foo") 683 684 f.assertNextManifest("foo", 685 // ensure that DC syncs *are* ignored for file watching, i.e., won't trigger builds 686 fileChangeFilters(filepath.Join("foo", "blah")), 687 ) 688 } 689 690 func TestDockerComposeWithDockerBuild(t *testing.T) { 691 f := newFixture(t) 692 693 f.dockerfile(filepath.Join("foo", "Dockerfile")) 694 f.file("docker-compose.yml", simpleConfig) 695 f.file("Tiltfile", `docker_build('gcr.io/foo', './foo') 696 docker_compose('docker-compose.yml') 697 dc_resource('foo', 'gcr.io/foo') 698 `) 699 700 f.load() 701 702 m := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 703 iTarget := m.ImageTargetAt(0) 704 705 // Make sure there's no live update in the default case. 706 assert.True(t, iTarget.IsDockerBuild()) 707 assert.True(t, liveupdate.IsEmptySpec(iTarget.LiveUpdateSpec)) 708 709 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 710 assert.Equal(t, m.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 711 } 712 713 func TestDockerComposeWithDockerBuildAutoAssociate(t *testing.T) { 714 f := newFixture(t) 715 716 f.dockerfile(filepath.Join("foo", "Dockerfile")) 717 f.file("docker-compose.yml", `version: '3' 718 services: 719 foo: 720 image: gcr.io/as_specified_in_config 721 build: ./foo 722 command: sleep 100 723 ports: 724 - "12312:80"`) 725 f.file("Tiltfile", `docker_build('gcr.io/as_specified_in_config', './foo') 726 docker_compose('docker-compose.yml') 727 `) 728 729 f.load() 730 731 // don't need a dc_resource call if the docker_build image matches the 732 // `Image` specified in dc.yml 733 m := f.assertNextManifest("foo", db(image("gcr.io/as_specified_in_config"))) 734 iTarget := m.ImageTargetAt(0) 735 736 // Make sure there's no live update in the default case. 737 assert.True(t, iTarget.IsDockerBuild()) 738 assert.True(t, liveupdate.IsEmptySpec(iTarget.LiveUpdateSpec)) 739 740 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 741 assert.Equal(t, m.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 742 } 743 744 // I.e. make sure that we handle de/normalization between `fooimage` <--> `docker.io/library/fooimage` 745 func TestDockerComposeWithDockerBuildLocalRef(t *testing.T) { 746 f := newFixture(t) 747 748 f.dockerfile(filepath.Join("foo", "Dockerfile")) 749 f.file("docker-compose.yml", simpleConfig) 750 f.file("Tiltfile", `docker_build('fooimage', './foo') 751 docker_compose('docker-compose.yml') 752 dc_resource('foo', 'fooimage') 753 `) 754 755 f.load() 756 757 m := f.assertNextManifest("foo", db(image("fooimage"))) 758 assert.True(t, m.ImageTargetAt(0).IsDockerBuild()) 759 760 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 761 assert.Equal(t, m.DockerComposeTarget().Spec.Project.ConfigPaths, 762 []string{configPath}) 763 } 764 765 func TestDockerComposeWithProfiles(t *testing.T) { 766 t.Run("include resource without profile", func(t *testing.T) { 767 f := newFixture(t) 768 769 f.setupFoo() 770 f.file("docker-compose.yml", twoServiceConfigWithProfiles) 771 f.file("Tiltfile", `docker_compose('docker-compose.yml') 772 dc_resource('foo') 773 `) 774 f.load() 775 776 _ = f.assertNextManifest("foo") 777 f.assertNoMoreManifests() 778 }) 779 780 t.Run("include specified profile", func(t *testing.T) { 781 f := newFixture(t) 782 783 f.setupFoo() 784 f.file("docker-compose.yml", twoServiceConfigWithProfiles) 785 f.file("Tiltfile", `docker_compose('docker-compose.yml', profiles=["barprofile"]) 786 dc_resource('foo') 787 dc_resource('bar') 788 `) 789 f.load() 790 791 _ = f.assertNextManifest("foo") 792 _ = f.assertNextManifest("bar") 793 f.assertNoMoreManifests() 794 }) 795 796 t.Run("include specified profile from env var", func(t *testing.T) { 797 f := newFixture(t) 798 t.Setenv("COMPOSE_PROFILES", "barprofile") 799 800 f.setupFoo() 801 f.file("docker-compose.yml", twoServiceConfigWithProfiles) 802 f.file("Tiltfile", `docker_compose('docker-compose.yml') 803 dc_resource('foo') 804 dc_resource('bar') 805 `) 806 f.load() 807 808 _ = f.assertNextManifest("foo") 809 _ = f.assertNextManifest("bar") 810 f.assertNoMoreManifests() 811 }) 812 813 t.Run("must include profile to have resource", func(t *testing.T) { 814 f := newFixture(t) 815 816 f.setupFoo() 817 f.file("docker-compose.yml", twoServiceConfigWithProfiles) 818 f.file("Tiltfile", `docker_compose('docker-compose.yml') 819 dc_resource('bar') 820 `) 821 f.loadErrString("Error in dc_resource: no Docker Compose service found with name \"bar\".") 822 f.assertNoMoreManifests() 823 }) 824 } 825 826 func TestMultipleDockerComposeWithDockerBuild(t *testing.T) { 827 f := newFixture(t) 828 829 f.dockerfile(filepath.Join("foo", "Dockerfile")) 830 f.dockerfile(filepath.Join("bar", "Dockerfile")) 831 f.file("docker-compose.yml", twoServiceConfig) 832 f.file("Tiltfile", `docker_build('gcr.io/foo', './foo') 833 docker_build('gcr.io/bar', './bar') 834 docker_compose('docker-compose.yml') 835 dc_resource('foo', 'gcr.io/foo') 836 dc_resource('bar', 'gcr.io/bar') 837 `) 838 839 f.load() 840 841 foo := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 842 assert.True(t, foo.ImageTargetAt(0).IsDockerBuild()) 843 844 bar := f.assertNextManifest("bar", db(image("gcr.io/bar"))) 845 assert.True(t, foo.ImageTargetAt(0).IsDockerBuild()) 846 847 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 848 assert.Equal(t, foo.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 849 assert.Equal(t, bar.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 850 } 851 852 func TestMultipleDockerComposeWithDockerBuildImageNames(t *testing.T) { 853 f := newFixture(t) 854 855 f.dockerfile(filepath.Join("foo", "Dockerfile")) 856 f.dockerfile(filepath.Join("bar", "Dockerfile")) 857 config := `version: '3' 858 services: 859 foo: 860 image: gcr.io/foo 861 bar: 862 image: gcr.io/bar 863 depends_on: [foo] 864 ` 865 f.file("docker-compose.yml", config) 866 f.file("Tiltfile", ` 867 docker_build('gcr.io/foo', './foo') 868 docker_build('gcr.io/bar', './bar') 869 docker_compose('docker-compose.yml') 870 `) 871 872 f.load() 873 874 foo := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 875 assert.True(t, foo.ImageTargetAt(0).IsDockerBuild()) 876 877 bar := f.assertNextManifest("bar", db(image("gcr.io/bar"))) 878 assert.True(t, bar.ImageTargetAt(0).IsDockerBuild()) 879 880 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 881 assert.Equal(t, foo.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 882 assert.Equal(t, bar.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 883 } 884 885 func TestDCImageRefSuggestion(t *testing.T) { 886 f := newFixture(t) 887 888 f.setupFoo() 889 f.file("docker-compose.yml", `version: '3' 890 services: 891 foo: 892 image: gcr.io/foo 893 `) 894 f.file("Tiltfile", ` 895 docker_build('gcr.typo.io/foo', 'foo') 896 docker_compose('docker-compose.yml') 897 `) 898 f.loadAssertWarnings(`Image not used in any Docker Compose config: 899 ✕ gcr.typo.io/foo 900 Did you mean… 901 - gcr.io/foo 902 Skipping this image build 903 If this is deliberate, suppress this warning with: update_settings(suppress_unused_image_warnings=["gcr.typo.io/foo"])`) 904 } 905 906 func TestDockerComposeOnlySomeWithDockerBuild(t *testing.T) { 907 f := newFixture(t) 908 909 f.dockerfile(filepath.Join("foo", "Dockerfile")) 910 f.file("docker-compose.yml", twoServiceConfig) 911 f.file("Tiltfile", `img_name = 'gcr.io/foo' 912 docker_build(img_name, './foo') 913 docker_compose('docker-compose.yml') 914 dc_resource('foo', img_name) 915 `) 916 917 f.load() 918 919 foo := f.assertNextManifest("foo", db(image("gcr.io/foo"))) 920 assert.True(t, foo.ImageTargetAt(0).IsDockerBuild()) 921 922 bar := f.assertNextManifest("bar") 923 assert.Empty(t, bar.ImageTargets) 924 925 configPath := f.TempDirFixture.JoinPath("docker-compose.yml") 926 assert.Equal(t, foo.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 927 assert.Equal(t, bar.DockerComposeTarget().Spec.Project.ConfigPaths, []string{configPath}) 928 } 929 930 func TestDockerComposeResourceNoImageMatch(t *testing.T) { 931 f := newFixture(t) 932 933 f.dockerfile(filepath.Join("foo", "Dockerfile")) 934 f.file("docker-compose.yml", simpleConfig) 935 f.file("Tiltfile", `docker_build('gcr.io/foo', './foo') 936 docker_compose('docker-compose.yml') 937 dc_resource('no-svc-with-this-name-eek', 'gcr.io/foo') 938 `) 939 f.loadErrString("no Docker Compose service found with name") 940 } 941 942 func TestDockerComposeLoadConfigFilesOnFailure(t *testing.T) { 943 f := newFixture(t) 944 945 f.dockerfile(filepath.Join("foo", "Dockerfile")) 946 f.file("docker-compose.yml", simpleConfig) 947 f.file("Tiltfile", `docker_build('gcr.io/foo', './foo') 948 docker_compose('docker-compose.yml') 949 fail("deliberate exit") 950 `) 951 f.loadErrString("deliberate exit") 952 953 // Make sure that even though tiltfile execution failed, we still 954 // loaded config files correctly. 955 f.assertConfigFiles(".tiltignore", "Tiltfile", "docker-compose.yml", "foo/Dockerfile") 956 } 957 958 func TestDockerComposeDoesntSupportEntrypointOverride(t *testing.T) { 959 f := newFixture(t) 960 961 f.dockerfile(filepath.Join("foo", "Dockerfile")) 962 f.file("docker-compose.yml", simpleConfig) 963 f.file("Tiltfile", `docker_build('gcr.io/foo', './foo', entrypoint='./foo') 964 docker_compose('docker-compose.yml') 965 dc_resource('foo', 'gcr.io/foo') 966 `) 967 968 f.loadErrString("docker_build/custom_build.entrypoint not supported for Docker Compose resources") 969 } 970 971 func TestDefaultRegistryWithDockerCompose(t *testing.T) { 972 f := newFixture(t) 973 974 f.dockerfile(filepath.Join("foo", "Dockerfile")) 975 f.file("docker-compose.yml", simpleConfig) 976 f.file("Tiltfile", ` 977 docker_compose('docker-compose.yml') 978 default_registry('bar.com') 979 `) 980 981 f.loadErrString("default_registry is not supported with docker compose") 982 } 983 984 func TestDockerComposeLabels(t *testing.T) { 985 f := newFixture(t) 986 987 f.dockerfile(filepath.Join("foo", "Dockerfile")) 988 f.file("docker-compose.yml", simpleConfig) 989 f.file("Tiltfile", ` 990 docker_compose('docker-compose.yml') 991 dc_resource("foo", labels="test") 992 `) 993 994 f.load("foo") 995 f.assertNextManifest("foo", resourceLabels("test")) 996 } 997 998 func TestMultitleDockerComposeLabels(t *testing.T) { 999 f := newFixture(t) 1000 1001 f.dockerfile(filepath.Join("foo", "Dockerfile")) 1002 f.file("docker-compose.yml", simpleConfig) 1003 f.file("docker-compose2.yml", barServiceConfig) 1004 f.file("Tiltfile", ` 1005 docker_compose('docker-compose.yml') 1006 dc_resource("foo", labels="test") 1007 1008 docker_compose('docker-compose2.yml') 1009 dc_resource("bar", labels="run") 1010 `) 1011 1012 f.load() 1013 f.assertNextManifest("foo", resourceLabels("test")) 1014 f.assertNextManifest("bar", resourceLabels("run")) 1015 } 1016 1017 func TestTriggerModeDC(t *testing.T) { 1018 for _, testCase := range []struct { 1019 name string 1020 globalSetting triggerMode 1021 dcResourceSetting triggerMode 1022 specifyAutoInit bool 1023 autoInit bool 1024 expectedTriggerMode model.TriggerMode 1025 }{ 1026 {"default", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 1027 {"explicit global auto", TriggerModeAuto, TriggerModeUnset, false, false, model.TriggerModeAuto}, 1028 {"explicit global manual", TriggerModeManual, TriggerModeUnset, false, false, model.TriggerModeManualWithAutoInit}, 1029 {"dc auto", TriggerModeUnset, TriggerModeUnset, false, false, model.TriggerModeAuto}, 1030 {"dc manual", TriggerModeUnset, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 1031 {"dc manual, auto_init=False", TriggerModeUnset, TriggerModeManual, true, false, model.TriggerModeManual}, 1032 {"dc manual, auto_init=True", TriggerModeUnset, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 1033 {"dc override auto", TriggerModeManual, TriggerModeAuto, false, false, model.TriggerModeAuto}, 1034 {"dc override manual", TriggerModeAuto, TriggerModeManual, false, false, model.TriggerModeManualWithAutoInit}, 1035 {"dc override manual, auto_init=False", TriggerModeAuto, TriggerModeManual, true, false, model.TriggerModeManual}, 1036 {"dc override manual, auto_init=True", TriggerModeAuto, TriggerModeManual, true, true, model.TriggerModeManualWithAutoInit}, 1037 } { 1038 t.Run(testCase.name, func(t *testing.T) { 1039 f := newFixture(t) 1040 1041 f.dockerfile(filepath.Join("foo", "Dockerfile")) 1042 f.file("docker-compose.yml", simpleConfig) 1043 1044 var globalTriggerModeDirective string 1045 switch testCase.globalSetting { 1046 case TriggerModeUnset: 1047 globalTriggerModeDirective = "" 1048 default: 1049 globalTriggerModeDirective = fmt.Sprintf("trigger_mode(%s)", testCase.globalSetting.String()) 1050 } 1051 1052 var dcResourceDirective string 1053 switch testCase.dcResourceSetting { 1054 case TriggerModeUnset: 1055 dcResourceDirective = "" 1056 default: 1057 autoInitOption := "" 1058 if testCase.specifyAutoInit { 1059 autoInitOption = ", auto_init=" 1060 if testCase.autoInit { 1061 autoInitOption += "True" 1062 } else { 1063 autoInitOption += "False" 1064 } 1065 } 1066 dcResourceDirective = fmt.Sprintf("dc_resource('foo', trigger_mode=%s%s)", testCase.dcResourceSetting.String(), autoInitOption) 1067 } 1068 1069 f.file("Tiltfile", fmt.Sprintf(` 1070 %s 1071 docker_compose('docker-compose.yml') 1072 %s 1073 `, globalTriggerModeDirective, dcResourceDirective)) 1074 1075 f.load() 1076 1077 f.assertNumManifests(1) 1078 f.assertNextManifest("foo", testCase.expectedTriggerMode) 1079 }) 1080 } 1081 } 1082 1083 func TestDCResourceNoImage(t *testing.T) { 1084 f := newFixture(t) 1085 1086 f.setupFoo() 1087 f.file("docker-compose.yml", simpleConfig) 1088 f.file("Tiltfile", ` 1089 docker_compose('docker-compose.yml') 1090 dc_resource('foo', trigger_mode=TRIGGER_MODE_AUTO) 1091 `) 1092 1093 f.load() 1094 } 1095 1096 func TestDCDependsOnInferredFromComposeFile(t *testing.T) { 1097 f := newFixture(t) 1098 1099 f.dockerfile(filepath.Join("foo", "Dockerfile")) 1100 f.file("docker-compose.yml", twoServiceConfig) 1101 f.file("Tiltfile", ` 1102 docker_compose('docker-compose.yml') 1103 `) 1104 1105 f.load() 1106 f.assertNextManifest("foo", resourceDeps()) 1107 f.assertNextManifest("bar", resourceDeps("foo")) 1108 } 1109 1110 func TestDCDependsOnResourceDepSpecified(t *testing.T) { 1111 f := newFixture(t) 1112 1113 f.dockerfile(filepath.Join("foo", "Dockerfile")) 1114 f.file("docker-compose.yml", twoServiceConfig) 1115 f.file("Tiltfile", ` 1116 docker_compose('docker-compose.yml') 1117 dc_resource('bar', resource_deps=['foo']) 1118 `) 1119 1120 f.load() 1121 f.assertNextManifest("foo", resourceDeps()) 1122 f.assertNextManifest("bar", resourceDeps("foo")) 1123 } 1124 1125 func TestDockerComposeVersionWarnings(t *testing.T) { 1126 type tc struct { 1127 version string 1128 warning string 1129 error string 1130 } 1131 tcs := []tc{ 1132 {version: "v1.28.0", error: "Tilt requires Docker Compose v1.28.3+ (you have v1.28.0). Please upgrade and re-launch Tilt."}, 1133 {version: "v2.0.0-rc.3", warning: "Using Docker Compose v2.0.0-rc.3 (version < 2.2) may result in errors or broken functionality.\n" + 1134 "For best results, we recommend upgrading to Docker Compose >= v2.2.0."}, 1135 {version: "v1.29.2" /* no errors or warnings */}, 1136 {version: "v2.2.0" /* no errors or warnings */}, 1137 } 1138 1139 for _, tc := range tcs { 1140 t.Run(tc.version, func(t *testing.T) { 1141 f := newFixture(t) 1142 1143 f.dockerfile(filepath.Join("foo", "Dockerfile")) 1144 f.file("docker-compose.yml", simpleConfig) 1145 f.file("Tiltfile", "docker_compose('docker-compose.yml')") 1146 1147 f.load("foo") 1148 1149 loader := f.newTiltfileLoader() 1150 if tl, ok := loader.(tiltfileLoader); ok { 1151 dcCli := dockercompose.NewFakeDockerComposeClient(t, f.ctx) 1152 dcCli.ConfigOutput = simpleConfig 1153 dcCli.VersionOutput = semver.Canonical(tc.version) 1154 tl.dcCli = dcCli 1155 loader = tl 1156 } else { 1157 require.Fail(t, "Could not set up fake Docker Compose client") 1158 } 1159 1160 f.loadResult = loader.Load(f.ctx, ctrltiltfile.MainTiltfile(f.JoinPath("Tiltfile"), nil), nil) 1161 if tc.error == "" { 1162 require.NoError(t, f.loadResult.Error, "Tiltfile load result had unexpected error") 1163 } else { 1164 require.Contains(t, f.loadResult.Error.Error(), tc.error) 1165 } 1166 1167 if tc.warning != "" { 1168 require.Len(t, f.warnings, 1) 1169 require.Contains(t, f.warnings[0], tc.warning) 1170 } else { 1171 require.Empty(t, f.warnings, "Tiltfile load result had unexpected warning(s)") 1172 } 1173 }) 1174 } 1175 } 1176 1177 func (f *fixture) assertDcManifest(name model.ManifestName, opts ...interface{}) model.Manifest { 1178 f.t.Helper() 1179 m := f.assertNextManifest(name) 1180 1181 if !m.IsDC() { 1182 f.t.Error("expected a docker-compose manifest") 1183 } 1184 dcInfo := m.DockerComposeTarget() 1185 1186 for _, opt := range opts { 1187 switch opt := opt.(type) { 1188 case dcServiceYAMLHelper: 1189 assert.YAMLEq(f.t, opt.yaml, dcInfo.ServiceYAML, "docker compose YAML") 1190 case noImageHelper: 1191 assert.Empty(f.t, m.ImageTargets, "Manifest should have had no ImageTargets") 1192 case dockerComposeImageHelper: 1193 ok, iTarget := assertImageTargetType(f.t, m.ImageTargets, model.DockerComposeBuild{}) 1194 if ok { 1195 assert.Equal(f.t, opt.buildContext, iTarget.DockerComposeBuildInfo().Context, 1196 "Build context path did not match") 1197 } 1198 case dcPublishedPortsHelper: 1199 assert.Equal(f.t, opt.ports, dcInfo.PublishedPorts(), "docker compose published ports") 1200 default: 1201 f.t.Fatalf("unexpected arg to assertDcManifest: %T %v", opt, opt) 1202 } 1203 } 1204 return m 1205 } 1206 1207 func assertImageTargetType(t *testing.T, iTargets []model.ImageTarget, 1208 buildDetailsType interface{}) (bool, model.ImageTarget) { 1209 t.Helper() 1210 if !assert.Len(t, iTargets, 1, "Manifest should have exactly one image target") { 1211 return false, model.ImageTarget{} 1212 } 1213 if !assert.IsType(t, buildDetailsType, iTargets[0].BuildDetails, "BuildDetails was not of expected type") { 1214 return false, model.ImageTarget{} 1215 } 1216 return true, iTargets[0] 1217 } 1218 1219 type dcServiceYAMLHelper struct { 1220 yaml string 1221 } 1222 1223 func dcServiceYAML(yaml string) dcServiceYAMLHelper { 1224 return dcServiceYAMLHelper{yaml} 1225 } 1226 1227 type dockerComposeImageHelper struct { 1228 dfPath string 1229 buildContext string 1230 } 1231 1232 func dockerComposeManagedImage(dfPath string, buildContext string) dockerComposeImageHelper { 1233 return dockerComposeImageHelper{ 1234 dfPath: dfPath, 1235 buildContext: buildContext, 1236 } 1237 } 1238 1239 type noImageHelper struct{} 1240 1241 func noImage() noImageHelper { 1242 return noImageHelper{} 1243 } 1244 1245 type dcPublishedPortsHelper struct { 1246 ports []int 1247 } 1248 1249 func dcPublishedPorts(ports ...int) dcPublishedPortsHelper { 1250 return dcPublishedPortsHelper{ports: ports} 1251 }