Repository: compose-spec/compose-go
Pull request #593 (2024-02-29 14:45:27)

let resourceloader compute relative working dir
When nesting includes + extends recursively, `WorkingDir` is set to a relative path, so `filepath` func fail to compute absolute/relative paths, this is why `ResourceLoader.Dir` was introduced. But I forgot to use it to compute include's relative workingdir. This PR fixes this mistake

closes https://github.com/docker/compose/issues/11552


Patch:
diff --git a/loader/include.go b/loader/include.go
index a42e0d0..e763141 100644
--- a/loader/include.go
+++ b/loader/include.go
@@ -60,43 +60,41 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
 			})
 		}
 
+		var relworkingdir string
 		for i, p := range r.Path {
 			for _, loader := range options.ResourceLoaders {
-				if loader.Accept(p) {
-					path, err := loader.Load(ctx, p)
-					if err != nil {
-						return err
+				if !loader.Accept(p) {
+					continue
+				}
+				path, err := loader.Load(ctx, p)
+				if err != nil {
+					return err
+				}
+				p = path
+
+				if i == 0 { // This is the "main" file, used to define project-directory. Others are overrides
+					relworkingdir = loader.Dir(path)
+					if r.ProjectDirectory == "" {
+						r.ProjectDirectory = filepath.Dir(path)
+					}
+
+					for _, f := range included {
+						if f == path {
+							included = append(included, path)
+							return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include "))
+						}
 					}
-					p = path
-					break
 				}
 			}
 			r.Path[i] = p
 		}
 
-		mainFile := r.Path[0]
-		for _, f := range included {
-			if f == mainFile {
-				included = append(included, mainFile)
-				return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include "))
-			}
-		}
-
-		if r.ProjectDirectory == "" {
-			r.ProjectDirectory = filepath.Dir(mainFile)
-		}
-		relworkingdir, err := filepath.Rel(configDetails.WorkingDir, r.ProjectDirectory)
-		if err != nil {
-			// included file path is not inside project working directory => use absolute path
-			relworkingdir = r.ProjectDirectory
-		}
-
 		loadOptions := options.clone()
 		loadOptions.ResolvePaths = true
 		loadOptions.SkipNormalization = true
 		loadOptions.SkipConsistencyCheck = true
 		loadOptions.ResourceLoaders = append(loadOptions.RemoteResourceLoaders(), localResourceLoader{
-			WorkingDir: relworkingdir,
+			WorkingDir: r.ProjectDirectory,
 		})
 
 		if len(r.EnvFile) == 0 {


Test patch:
diff --git a/loader/include_test.go b/loader/include_test.go
index 7c011e3..8188023 100644
--- a/loader/include_test.go
+++ b/loader/include_test.go
@@ -26,9 +26,21 @@ import (
 	"gotest.tools/v3/assert"
 )
 
+func TestLoadIncludeExtendsCombined(t *testing.T) {
+	_, err := LoadWithContext(context.Background(), types.ConfigDetails{
+		WorkingDir: "testdata/combined",
+		ConfigFiles: []types.ConfigFile{
+			{
+				Filename: "testdata/combined/compose.yaml",
+			},
+		},
+	}, withProjectName("test-load-combined", true))
+	assert.NilError(t, err)
+}
+
 func TestLoadWithMultipleInclude(t *testing.T) {
 	// include same service twice should not trigger an error
-	p, err := Load(buildConfigDetails(`
+	details := buildConfigDetails(`
 name: 'test-multi-include'
 
 include:
@@ -41,7 +53,9 @@ services:
     image: busybox
     depends_on:
       - imported
-`, map[string]string{"SOURCE": "override"}), func(options *Options) {
+`, map[string]string{"SOURCE": "override"})
+
+	p, err := Load(details, func(options *Options) {
 		options.SkipNormalization = true
 		options.ResolvePaths = true
 	})
@@ -49,9 +63,11 @@ services:
 	imported, err := p.GetService("imported")
 	assert.NilError(t, err)
 	assert.Equal(t, imported.ContainerName, "override")
+}
 
+func TestLoadWithMultipleIncludeConflict(t *testing.T) {
 	// include 2 different services with same name should trigger an error
-	_, err = Load(buildConfigDetails(`
+	details := buildConfigDetails(`
 name: 'test-multi-include'
 
 include:
@@ -64,7 +80,8 @@ include:
 services:
   bar:
     image: busybox
-`, map[string]string{"SOURCE": "override"}), func(options *Options) {
+`, map[string]string{"SOURCE": "override"})
+	_, err := Load(details, func(options *Options) {
 		options.SkipNormalization = true
 		options.ResolvePaths = true
 	})
diff --git a/loader/testdata/combined/compose.yaml b/loader/testdata/combined/compose.yaml
new file mode 100644
index 0000000..f7e1cc8
--- /dev/null
+++ b/loader/testdata/combined/compose.yaml
@@ -0,0 +1,4 @@
+include:
+  - path:
+      - dir/included.yaml
+
diff --git a/loader/testdata/combined/dir/extended.yaml b/loader/testdata/combined/dir/extended.yaml
new file mode 100644
index 0000000..be149e1
--- /dev/null
+++ b/loader/testdata/combined/dir/extended.yaml
@@ -0,0 +1,4 @@
+services:
+  service:
+    build: .
+
diff --git a/loader/testdata/combined/dir/included.yaml b/loader/testdata/combined/dir/included.yaml
new file mode 100644
index 0000000..ca5a8cb
--- /dev/null
+++ b/loader/testdata/combined/dir/included.yaml
@@ -0,0 +1,6 @@
+services:
+  service:
+    extends:
+      file: extended.yaml
+      service: service
+