diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdf252786a3..10f89dc726b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,6 +190,9 @@ jobs: check-latest: true cache: true + - name: Build example provider + run: make example-provider + - name: Build uses: docker/bake-action@v6 with: diff --git a/Makefile b/Makefile index 946947d0e0c..ceae368035c 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,8 @@ install: binary install $(or $(DESTDIR),./bin/build)/docker-compose ~/.docker/cli-plugins/docker-compose .PHONY: e2e-compose -e2e-compose: ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test +e2e-compose: example-provider + ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 ./pkg/e2e .PHONY: e2e-compose-standalone @@ -156,3 +157,6 @@ pre-commit: validate check-dependencies lint build test e2e-compose help: ## Show help @echo Please specify a build target. The choices are: @grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +example-provider: + go build -o bin/build/example-provider docs/examples/provider.go \ No newline at end of file diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 10b53e53f85..217d9bc4af6 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -180,11 +180,6 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF ctx = context.WithValue(ctx, tracing.MetricsKey{}, metrics) - project, err = project.WithServicesEnvironmentResolved(true) - if err != nil { - return err - } - return fn(ctx, project, args) }) } diff --git a/docs/examples/provider.go b/docs/examples/provider.go index e500e8d5c51..2959c8bba7b 100644 --- a/docs/examples/provider.go +++ b/docs/examples/provider.go @@ -39,20 +39,30 @@ func main() { } } +type options struct { + db string + size int +} + func composeCommand() *cobra.Command { c := &cobra.Command{ Use: "compose EVENT", TraverseChildren: true, } c.PersistentFlags().String("project-name", "", "compose project name") // unused + + var options options + upCmd := &cobra.Command{ - Use: "up", - Run: up, + Use: "up", + Run: func(_ *cobra.Command, args []string) { + up(options, args) + }, Args: cobra.ExactArgs(1), } - upCmd.Flags().String("type", "", "Database type (mysql, postgres, etc.)") + upCmd.Flags().StringVar(&options.db, "type", "", "Database type (mysql, postgres, etc.)") _ = upCmd.MarkFlagRequired("type") - upCmd.Flags().Int("size", 10, "Database size in GB") + upCmd.Flags().IntVar(&options.size, "size", 10, "Database size in GB") upCmd.Flags().String("name", "", "Name of the database to be created") _ = upCmd.MarkFlagRequired("name") @@ -71,13 +81,13 @@ func composeCommand() *cobra.Command { const lineSeparator = "\n" -func up(_ *cobra.Command, args []string) { +func up(options options, args []string) { servicename := args[0] fmt.Printf(`{ "type": "debug", "message": "Starting %s" }%s`, servicename, lineSeparator) - for i := 0; i < 100; i += 10 { + for i := 0; i < options.size; i++ { time.Sleep(1 * time.Second) - fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i, lineSeparator) + fmt.Printf(`{ "type": "info", "message": "Processing ... %d%%" }%s`, i*100/options.size, lineSeparator) } fmt.Printf(`{ "type": "setenv", "message": "URL=https://magic.cloud/%s" }%s`, servicename, lineSeparator) } diff --git a/go.mod b/go.mod index 536b528c81f..1bb7ebff3e0 100644 --- a/go.mod +++ b/go.mod @@ -212,3 +212,5 @@ exclude ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 ) + +replace github.com/compose-spec/compose-go/v2 => github.com/ndeloof/compose-go/v2 v2.0.1-0.20250623114502-42c1cc814431 diff --git a/go.sum b/go.sum index 9b2461f9dca..b2486291ecd 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,6 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.6.5-0.20250605125952-a0d3b94b8dc9 h1:VULSSHxkc7u/U349sHp1RbnWAcnf7JD0HY3rGeZrMaM= -github.com/compose-spec/compose-go/v2 v2.6.5-0.20250605125952-a0d3b94b8dc9/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= @@ -361,6 +359,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ndeloof/compose-go/v2 v2.0.1-0.20250623114502-42c1cc814431 h1:hQ2liObnBBybZ4yrrfKczUXk2qDDYkzMfC8mtBdlpJU= +github.com/ndeloof/compose-go/v2 v2.0.1-0.20250623114502-42c1cc814431/go.mod h1:TmjkIB9W73fwVxkYY+u2uhMbMUakjiif79DlYgXsyvU= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 5f10e398621..1655382dee1 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -27,6 +27,7 @@ import ( "sync" "time" + "github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/types" "github.com/containerd/platforms" containerType "github.com/docker/docker/api/types/container" @@ -109,7 +110,28 @@ func (c *convergence) apply(ctx context.Context, project *types.Project, options }) } +// mutation of project.Environment requires a mutex as we create services concurrently +var mux sync.Mutex + func (c *convergence) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error { //nolint:gocyclo + mux.Lock() + service.LoaderEnv.OverrideBy(project.Environment) + for key, val := range service.Environment { + if val != nil { + newVal, err := dotenv.ExpandVariables(*val, nil, service.LoaderEnv.Resolve) + if err != nil { + return err + } + service.Environment[key] = &newVal + } + } + + err := service.WithEnvironmentResolved(false) + mux.Unlock() + if err != nil { + return err + } + if service.Provider != nil { return c.service.runPlugin(ctx, project, service, "up") } diff --git a/pkg/compose/plugins.go b/pkg/compose/plugins.go index 66cfc53fcfd..07ad64dd4ee 100644 --- a/pkg/compose/plugins.go +++ b/pkg/compose/plugins.go @@ -70,15 +70,12 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project, return err } - for name, s := range project.Services { - if _, ok := s.DependsOn[service.Name]; ok { - prefix := strings.ToUpper(service.Name) + "_" - for key, val := range variables { - s.Environment[prefix+key] = &val - } - project.Services[name] = s - } + mux.Lock() + prefix := strings.ToUpper(service.Name) + "_" + for key, val := range variables { + project.Environment[prefix+key] = val } + mux.Unlock() return nil } diff --git a/pkg/e2e/fixtures/interpolation/.env b/pkg/e2e/fixtures/interpolation/.env new file mode 100644 index 00000000000..5de888f66e0 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/.env @@ -0,0 +1,5 @@ +FOO=FOO-from-dot-env +TEST1=dot-env +TEST2=dot-env +IMPLICIT_ENV_FILE_TO_ENV_FILE=IMPLICIT_ENV_FILE_VALUE +IMPLICIT_ENV_FILE_TO_OVERRIDE=IMPLICIT_ENV_FILE_TO_OVERRIDE_VALUE \ No newline at end of file diff --git a/pkg/e2e/fixtures/interpolation/compose.override.yaml b/pkg/e2e/fixtures/interpolation/compose.override.yaml new file mode 100644 index 00000000000..3eaf2f89246 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/compose.override.yaml @@ -0,0 +1,5 @@ +services: + test: + environment: + - IMPLICIT_OVERRIDE_FILE=implicit_override_value + - IMPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=${IMPLICIT_ENV_FILE_TO_OVERRIDE} diff --git a/pkg/e2e/fixtures/interpolation/compose.yaml b/pkg/e2e/fixtures/interpolation/compose.yaml new file mode 100644 index 00000000000..5e49d524afe --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/compose.yaml @@ -0,0 +1,24 @@ +services: + test: + image: alpine + command: env + environment: + - FOO + - BAR=bar_from_environment + - BY_PROVIDER_FROM_ENV=${EXAMPLE_URL} + - INTERPOLATED=${TEST1}-${TEST2} + - BY_IMPLICIT_OVERRIDE_FILE=${IMPLICIT_OVERRIDE_FILE} + - BY_IMPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=${IMPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE} + - BY_EXPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=${EXPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE} + env_file: + - env_file.env + depends_on: + - example + + example: + provider: + type: example-provider + options: + type: test + name: example + size: 0 diff --git a/pkg/e2e/fixtures/interpolation/env_file.env b/pkg/e2e/fixtures/interpolation/env_file.env new file mode 100644 index 00000000000..f29f9c1a54f --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/env_file.env @@ -0,0 +1,10 @@ +ZOT=${FOO:-ZOT} +QIX=some ${FOO} value +TEST1=env_file +TEST2=env_file +BAR_FROM_ENV_FILE=${BAR} +BY_PROVIDER_FROM_ENV_FILE: ${EXAMPLE_URL} +BY_OS_FROM_ENV_FILE: ${OS_TO_ENV_FILE} +BY_CMD_FROM_ENV_FILE: ${CMD_TO_ENV_FILE:-CMD_TO_ENV_FILE_DEFAULT_VALUE} +BY_IMPLICIT_ENV_FILE: ${IMPLICIT_ENV_FILE_TO_ENV_FILE} +BY_EXPLICIT_ENV_FILE: ${EXPLICIT_ENV_FILE_TO_ENV_FILE} diff --git a/pkg/e2e/fixtures/interpolation/explicit_env_file.env b/pkg/e2e/fixtures/interpolation/explicit_env_file.env new file mode 100644 index 00000000000..01cf1e10900 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/explicit_env_file.env @@ -0,0 +1 @@ +EXPLICIT_ENV_FILE_TO_ENV_FILE=EXPLICIT_ENV_FILE_VALUE diff --git a/pkg/e2e/fixtures/interpolation/extends/.env b/pkg/e2e/fixtures/interpolation/extends/.env new file mode 100644 index 00000000000..7b841f77f8a --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/extends/.env @@ -0,0 +1,3 @@ +FOO=FOO-from-extends +TEST1=extends +TEST2=extends diff --git a/pkg/e2e/fixtures/interpolation/extends/compose.yaml b/pkg/e2e/fixtures/interpolation/extends/compose.yaml new file mode 100644 index 00000000000..9dc022ef114 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/extends/compose.yaml @@ -0,0 +1,12 @@ +services: + test: + extends: + service: test + file: ../compose.yaml + environment: + - BAR=BAR-from-extends + + example: + extends: + service: example + file: ../compose.yaml \ No newline at end of file diff --git a/pkg/e2e/fixtures/interpolation/include/compose.yaml b/pkg/e2e/fixtures/interpolation/include/compose.yaml new file mode 100644 index 00000000000..b0938b3da75 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/include/compose.yaml @@ -0,0 +1,3 @@ +include: + - path: ../compose.yaml + env_file: include.env \ No newline at end of file diff --git a/pkg/e2e/fixtures/interpolation/include/include.env b/pkg/e2e/fixtures/interpolation/include/include.env new file mode 100644 index 00000000000..55b4604784d --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/include/include.env @@ -0,0 +1,3 @@ +FOO=FOO-from-include +TEST1=include +TEST2=include diff --git a/pkg/e2e/fixtures/interpolation/override/override.yaml b/pkg/e2e/fixtures/interpolation/override/override.yaml new file mode 100644 index 00000000000..e037cb39ea0 --- /dev/null +++ b/pkg/e2e/fixtures/interpolation/override/override.yaml @@ -0,0 +1,5 @@ +services: + test: + environment: + - EXPLICIT_OVERRIDE_FILE=explicit_override_value + - EXPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=${IMPLICIT_ENV_FILE_TO_OVERRIDE} diff --git a/pkg/e2e/interpolation_test.go b/pkg/e2e/interpolation_test.go new file mode 100644 index 00000000000..19a2d9e3528 --- /dev/null +++ b/pkg/e2e/interpolation_test.go @@ -0,0 +1,185 @@ +/* + Copyright 2022 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package e2e + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" +) + +func Test_interpolation(t *testing.T) { + provider, err := findExecutable("example-provider") + assert.NilError(t, err) + path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider)) + c := NewParallelCLI(t, WithEnv("PATH="+path, "TEST1=os.Env")) + + const projectName = "interpolation" + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := c.RunDockerComposeCmd(t, "-f", "fixtures/interpolation/compose.yaml", "--project-name", projectName, "up") + env := getEnv(res.Combined(), false) + + assert.Check(t, slices.Contains(env, "FOO=FOO-from-dot-env")) + assert.Check(t, slices.Contains(env, "BAR=bar_from_environment")) + assert.Check(t, slices.Contains(env, "ZOT=FOO-from-dot-env")) + assert.Check(t, slices.Contains(env, "QIX=some FOO-from-dot-env value")) + assert.Check(t, slices.Contains(env, "BAR_FROM_ENV_FILE=bar_from_environment")) + assert.Check(t, slices.Contains(env, "INTERPOLATED=os.Env-dot-env")) + + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV=https://magic.cloud/example")) + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV_FILE=https://magic.cloud/example")) +} + +func Test_interpolationWithInclude(t *testing.T) { + provider, err := findExecutable("example-provider") + assert.NilError(t, err) + path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider)) + c := NewParallelCLI(t, WithEnv("PATH="+path, "TEST1=os.Env")) + + const projectName = "interpolation-include" + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := c.RunDockerComposeCmd(t, "-f", "fixtures/interpolation/include/compose.yaml", "--project-name", projectName, "up") + env := getEnv(res.Combined(), false) + + assert.Check(t, slices.Contains(env, "FOO=FOO-from-include")) + assert.Check(t, slices.Contains(env, "BAR=bar_from_environment")) + assert.Check(t, slices.Contains(env, "ZOT=FOO-from-include")) + assert.Check(t, slices.Contains(env, "QIX=some FOO-from-include value")) + assert.Check(t, slices.Contains(env, "INTERPOLATED=os.Env-include")) + + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV=https://magic.cloud/example")) + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV_FILE=https://magic.cloud/example")) +} + +func Test_interpolationWithExtends(t *testing.T) { + provider, err := findExecutable("example-provider") + assert.NilError(t, err) + path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider)) + c := NewParallelCLI(t, WithEnv("PATH="+path, "TEST1=os.Env")) + + const projectName = "interpolation-extends" + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := c.RunDockerComposeCmd(t, "-f", "fixtures/interpolation/extends/compose.yaml", "--project-name", projectName, "up") + env := getEnv(res.Combined(), false) + + assert.Check(t, slices.Contains(env, "FOO=FOO-from-extends")) + assert.Check(t, slices.Contains(env, "BAR=BAR-from-extends")) + assert.Check(t, slices.Contains(env, "ZOT=FOO-from-extends")) + assert.Check(t, slices.Contains(env, "QIX=some FOO-from-extends value")) + assert.Check(t, slices.Contains(env, "INTERPOLATED=os.Env-extends")) + + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV=https://magic.cloud/example")) + assert.Check(t, slices.Contains(env, "BY_PROVIDER_FROM_ENV_FILE=https://magic.cloud/example")) +} + +func TestInterpolationInEnvFile(t *testing.T) { + provider, err := findExecutable("example-provider") + assert.NilError(t, err) + path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider)) + c := NewParallelCLI(t, WithEnv("PATH="+path)) + + const projectName = "interpolation-env-file" + t.Log("interpolation in env file from os env and implicit env file") + cmd := c.NewDockerComposeCmd(t, "-f", "fixtures/interpolation/compose.yaml", "--project-name", projectName, "up") + cmd.Env = append(cmd.Env, "OS_TO_ENV_FILE=OS_TO_ENV_FILE_VALUE") + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := icmd.RunCmd(cmd) + assert.NilError(t, res.Error, res.Combined()) + env := getEnv(res.Combined(), false) + assert.Check(t, slices.Contains(env, "BY_OS_FROM_ENV_FILE=OS_TO_ENV_FILE_VALUE"), env) + assert.Check(t, slices.Contains(env, "BY_IMPLICIT_ENV_FILE=IMPLICIT_ENV_FILE_VALUE"), env) + assert.Check(t, slices.Contains(env, "BY_CMD_FROM_ENV_FILE=CMD_TO_ENV_FILE_DEFAULT_VALUE"), env) + + t.Log("interpolation in env file from command env and explicit env file") + cmd = c.NewDockerComposeCmd(t, "-f", "fixtures/interpolation/compose.yaml", "--project-name", projectName, + "--env-file", "fixtures/interpolation/explicit_env_file.env", "run", + "--env", "BY_CMD_FROM_ENV_FILE=CMD_TO_ENV_FILE_VALUE", "--rm", "test") + + res = icmd.RunCmd(cmd) + env = getEnv(res.Combined(), true) + assert.Check(t, slices.Contains(env, "BY_EXPLICIT_ENV_FILE=EXPLICIT_ENV_FILE_VALUE"), env) + assert.Check(t, slices.Contains(env, "BY_CMD_FROM_ENV_FILE=CMD_TO_ENV_FILE_VALUE"), env) +} + +func TestOverrideFiles(t *testing.T) { + provider, err := findExecutable("example-provider") + assert.NilError(t, err) + path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider)) + c := NewParallelCLI(t, WithEnv("PATH="+path)) + + const projectName = "interpolation-override" + + t.Run("interpolation from implicit override file", func(t *testing.T) { + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := c.RunDockerComposeCmd(t, "--project-directory", "fixtures/interpolation", "--project-name", projectName, "up") + env := getEnv(res.Combined(), false) + + assert.Check(t, slices.Contains(env, "BY_IMPLICIT_OVERRIDE_FILE=implicit_override_value"), env) + assert.Check(t, slices.Contains(env, "BY_IMPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=IMPLICIT_ENV_FILE_TO_OVERRIDE_VALUE"), env) + }) + + t.Run("interpolation from explicit override file", func(t *testing.T) { + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + }) + + res := c.RunDockerComposeCmd(t, "-f", "fixtures/interpolation/compose.yaml", + "-f", "fixtures/interpolation/override/override.yaml", "--project-name", projectName, "up") + env := getEnv(res.Combined(), false) + + assert.Check(t, slices.Contains(env, "BY_EXPLICIT_OVERRIDE_FILE=explicit_override_value"), env) + assert.Check(t, slices.Contains(env, "BY_EXPLICIT_OVERRIDE_FROM_IMPLICIT_ENV_FILE=IMPLICIT_ENV_FILE_TO_OVERRIDE_VALUE"), env) + }) +} + +func getEnv(out string, run bool) []string { + var env []string + scanner := bufio.NewScanner(strings.NewReader(out)) + for scanner.Scan() { + line := scanner.Text() + if !run && strings.HasPrefix(line, "test-1 | ") { + env = append(env, line[10:]) + } + if run && strings.Contains(line, "=") && len(strings.Split(line, "=")) == 2 { + env = append(env, line) + } + } + slices.Sort(env) + return env +}