Repository: compose-spec/compose-go
Pull request #579 (2024-02-16 14:39:08)

process include with a relative-path local resource loader
As we process `include`, use relative path to configure local resource loader so that included resources point to the target path, but still respect the `--no-path-resolution` flag

fixes https://github.com/docker/compose/issues/11508


Patch:
diff --git a/loader/include.go b/loader/include.go
index cc50734..21d5871 100644
--- a/loader/include.go
+++ b/loader/include.go
@@ -71,7 +71,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
 					break
 				}
 			}
-			r.Path[i] = absPath(configDetails.WorkingDir, p)
+			r.Path[i] = p
 		}
 
 		mainFile := r.Path[0]
@@ -85,13 +85,18 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
 		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: r.ProjectDirectory,
+			WorkingDir: relworkingdir,
 		})
 
 		if len(r.EnvFile) == 0 {
@@ -107,7 +112,7 @@ func ApplyInclude(ctx context.Context, configDetails types.ConfigDetails, model
 		}
 
 		config := types.ConfigDetails{
-			WorkingDir:  r.ProjectDirectory,
+			WorkingDir:  relworkingdir,
 			ConfigFiles: types.ToConfigFiles(r.Path),
 			Environment: configDetails.Environment.Clone().Merge(envFromFile),
 		}


Test patch:
diff --git a/loader/include_test.go b/loader/include_test.go
new file mode 100644
index 0000000..c0d17ff
--- /dev/null
+++ b/loader/include_test.go
@@ -0,0 +1,91 @@
+/*
+   Copyright 2020 The Compose Specification 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 loader
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"gotest.tools/v3/assert"
+)
+
+func TestLoadWithMultipleInclude(t *testing.T) {
+	// include same service twice should not trigger an error
+	p, err := Load(buildConfigDetails(`
+name: 'test-multi-include'
+
+include:
+  - path: ./testdata/subdir/compose-test-extends-imported.yaml
+    env_file: ./testdata/subdir/extra.env
+  - path: ./testdata/compose-include.yaml
+
+services:
+  foo:
+    image: busybox
+    depends_on:
+      - imported
+`, map[string]string{"SOURCE": "override"}), func(options *Options) {
+		options.SkipNormalization = true
+		options.ResolvePaths = true
+	})
+	assert.NilError(t, err)
+	imported, err := p.GetService("imported")
+	assert.NilError(t, err)
+	assert.Equal(t, imported.ContainerName, "override")
+
+	// include 2 different services with same name should trigger an error
+	p, err = Load(buildConfigDetails(`
+name: 'test-multi-include'
+
+include:
+  - path: ./testdata/subdir/compose-test-extends-imported.yaml
+    env_file: ./testdata/subdir/extra.env
+  - path: ./testdata/compose-include.yaml
+    env_file: ./testdata/subdir/extra.env
+
+
+services:
+  bar:
+    image: busybox
+`, map[string]string{"SOURCE": "override"}), func(options *Options) {
+		options.SkipNormalization = true
+		options.ResolvePaths = true
+	})
+	assert.ErrorContains(t, err, "services.bar conflicts with imported resource", err)
+}
+
+func TestIncludeRelative(t *testing.T) {
+	wd, err := filepath.Abs(filepath.Join("testdata", "include"))
+	assert.NilError(t, err)
+	p, err := LoadWithContext(context.Background(), types.ConfigDetails{
+		ConfigFiles: []types.ConfigFile{
+			{
+				Filename: filepath.Join("testdata", "include", "compose.yaml"),
+			},
+		},
+		WorkingDir: wd,
+	}, func(options *Options) {
+		options.projectName = "test-include-relative"
+		options.ResolvePaths = false
+	})
+	assert.NilError(t, err)
+	included := p.Services["included"]
+	assert.Equal(t, included.Build.Context, ".")
+	assert.Equal(t, included.Volumes[0].Source, ".")
+}
diff --git a/loader/loader_test.go b/loader/loader_test.go
index 41cb7a8..c01d7bb 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2611,51 +2611,6 @@ func TestLoadDependsOnCycle(t *testing.T) {
 	assert.Error(t, err, "dependency cycle detected: service2 -> service3 -> service1", err)
 }
 
-func TestLoadWithMultipleInclude(t *testing.T) {
-	// include same service twice should not trigger an error
-	p, err := Load(buildConfigDetails(`
-name: 'test-multi-include'
-
-include:
-  - path: ./testdata/subdir/compose-test-extends-imported.yaml
-    env_file: ./testdata/subdir/extra.env
-  - path: ./testdata/compose-include.yaml
-
-services:
-  foo:
-    image: busybox
-    depends_on:
-      - imported
-`, map[string]string{"SOURCE": "override"}), func(options *Options) {
-		options.SkipNormalization = true
-		options.ResolvePaths = true
-	})
-	assert.NilError(t, err)
-	imported, err := p.GetService("imported")
-	assert.NilError(t, err)
-	assert.Equal(t, imported.ContainerName, "override")
-
-	// include 2 different services with same name should trigger an error
-	p, err = Load(buildConfigDetails(`
-name: 'test-multi-include'
-
-include:
-  - path: ./testdata/subdir/compose-test-extends-imported.yaml
-    env_file: ./testdata/subdir/extra.env
-  - path: ./testdata/compose-include.yaml
-    env_file: ./testdata/subdir/extra.env
-
-
-services:
-  bar:
-    image: busybox
-`, map[string]string{"SOURCE": "override"}), func(options *Options) {
-		options.SkipNormalization = true
-		options.ResolvePaths = true
-	})
-	assert.ErrorContains(t, err, "services.bar conflicts with imported resource", err)
-}
-
 func TestLoadWithDependsOn(t *testing.T) {
 	p, err := loadYAML(`
 name: test-depends-on
diff --git a/loader/testdata/include/compose.yaml b/loader/testdata/include/compose.yaml
new file mode 100644
index 0000000..bb8ad4d
--- /dev/null
+++ b/loader/testdata/include/compose.yaml
@@ -0,0 +1,2 @@
+include:
+  - path: included.yaml
diff --git a/loader/testdata/include/included.yaml b/loader/testdata/include/included.yaml
new file mode 100644
index 0000000..9713a91
--- /dev/null
+++ b/loader/testdata/include/included.yaml
@@ -0,0 +1,5 @@
+services:
+  included:
+    build: .
+    volumes:
+      - ./:/mnt