Repository: compose-spec/compose-go
Pull request #351 (2023-02-15 19:49:50)

env: fix path resolution with `extends`
* Fix an issue with `WithEnvFile` helper to not pass empty values
* Rewrite `env_file` paths from `extends` file to be relative to the included file rather than parent (this was a regression)
* Normalize `extends.file` path to be absolute when doing the same for other paths

See docker/compose#10258.


Patch:
diff --git a/cli/options.go b/cli/options.go
index a85ca6a..3a3a7f5 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -190,7 +190,11 @@ func WithOsEnv(o *ProjectOptions) error {
 // WithEnvFile set an alternate env file
 // deprecated - use WithEnvFiles
 func WithEnvFile(file string) ProjectOptionsFn {
-	return WithEnvFiles(file)
+	var files []string
+	if file != "" {
+		files = []string{file}
+	}
+	return WithEnvFiles(files...)
 }
 
 // WithEnvFiles set alternate env files
diff --git a/loader/loader.go b/loader/loader.go
index a7a8ca9..d8ff2ef 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -583,6 +583,10 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
 				}
 				baseService.Volumes[i].Source = resolveMaybeUnixPath(vol.Source, baseFileParent, lookupEnv)
 			}
+
+			for i, envFile := range baseService.EnvFile {
+				baseService.EnvFile[i] = resolveMaybeUnixPath(envFile, baseFileParent, lookupEnv)
+			}
 		}
 
 		serviceConfig, err = _merge(baseService, serviceConfig)
diff --git a/loader/normalize.go b/loader/normalize.go
index b4c3acc..c3aa91d 100644
--- a/loader/normalize.go
+++ b/loader/normalize.go
@@ -90,6 +90,11 @@ func normalize(project *types.Project, resolvePaths bool) error {
 		}
 		s.Environment = s.Environment.Resolve(fn)
 
+		if extendFile := s.Extends["file"]; extendFile != nil && *extendFile != "" {
+			p := absPath(project.WorkingDir, *extendFile)
+			s.Extends["file"] = &p
+		}
+
 		err := relocateLogDriver(&s)
 		if err != nil {
 			return err


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 055d140..a548f33 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -26,11 +26,12 @@ import (
 	"testing"
 	"time"
 
-	"github.com/compose-spec/compose-go/types"
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/sirupsen/logrus"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
+
+	"github.com/compose-spec/compose-go/types"
 )
 
 func buildConfigDetails(yaml string, env map[string]string) types.ConfigDetails {
@@ -1853,16 +1854,29 @@ func TestLoadWithExtends(t *testing.T) {
 	actual, err := Load(configDetails)
 	assert.NilError(t, err)
 
+	extendsDir, err := filepath.Abs(filepath.Join("testdata", "subdir"))
+	assert.NilError(t, err)
+
+	expectedEnvFilePath := filepath.Join(extendsDir, "extra.env")
+
+	expectedExtendsPath := filepath.Join(
+		extendsDir,
+		"compose-test-extends-imported.yaml",
+	)
+
 	expServices := types.Services{
 		{
 			Name:  "importer",
 			Image: "nginx",
 			Extends: types.ExtendsConfig{
-				"file":    strPtr("compose-test-extends-imported.yaml"),
+				"file":    &expectedExtendsPath,
 				"service": strPtr("imported"),
 			},
-			Environment: types.MappingWithEquals{},
-			Networks:    map[string]*types.ServiceNetworkConfig{"default": nil},
+			Environment: types.MappingWithEquals{
+				"SOURCE": strPtr("extends"),
+			},
+			EnvFile:  []string{expectedEnvFilePath},
+			Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
 			Volumes: []types.ServiceVolumeConfig{{
 				Type:   "bind",
 				Source: "/opt/data",
@@ -1889,6 +1903,13 @@ func TestLoadWithExtendsWithContextUrl(t *testing.T) {
 	actual, err := Load(configDetails)
 	assert.NilError(t, err)
 
+	expectedExtendsPath, err := filepath.Abs(
+		filepath.Join(
+			"testdata",
+			"compose-test-extends-with-context-url-imported.yaml",
+		),
+	)
+	assert.NilError(t, err)
 	expServices := types.Services{
 		{
 			Name: "importer-with-https-url",
@@ -1897,7 +1918,7 @@ func TestLoadWithExtendsWithContextUrl(t *testing.T) {
 				Dockerfile: "Dockerfile",
 			},
 			Extends: types.ExtendsConfig{
-				"file":    strPtr("compose-test-extends-with-context-url-imported.yaml"),
+				"file":    &expectedExtendsPath,
 				"service": strPtr("imported-with-https-url"),
 			},
 			Environment: types.MappingWithEquals{},
diff --git a/loader/testdata/compose-test-extends.yaml b/loader/testdata/compose-test-extends.yaml
index d167cdc..ac49e4c 100644
--- a/loader/testdata/compose-test-extends.yaml
+++ b/loader/testdata/compose-test-extends.yaml
@@ -1,5 +1,5 @@
 services:
   importer:
     extends:
-      file: compose-test-extends-imported.yaml
+      file: subdir/compose-test-extends-imported.yaml
       service: imported
diff --git a/loader/testdata/compose-test-extends-imported.yaml b/loader/testdata/subdir/compose-test-extends-imported.yaml
similarity index 72%
rename from loader/testdata/compose-test-extends-imported.yaml
rename to loader/testdata/subdir/compose-test-extends-imported.yaml
index c38f39f..2ee24e8 100644
--- a/loader/testdata/compose-test-extends-imported.yaml
+++ b/loader/testdata/subdir/compose-test-extends-imported.yaml
@@ -1,5 +1,7 @@
 services:
   imported:
     image: nginx
+    env_file:
+      - extra.env
     volumes:
       - /opt/data:/var/lib/mysql
diff --git a/loader/testdata/subdir/extra.env b/loader/testdata/subdir/extra.env
new file mode 100644
index 0000000..a735d7f
--- /dev/null
+++ b/loader/testdata/subdir/extra.env
@@ -0,0 +1,1 @@
+SOURCE=extends
diff --git a/types/types_test.go b/types/types_test.go
index 2d7f283..3d9a741 100644
--- a/types/types_test.go
+++ b/types/types_test.go
@@ -313,6 +313,7 @@ func TestMarshalServiceEntrypoint(t *testing.T) {
 	}
 
 	for _, tc := range tcs {
+		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()