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  }