Repository: compose-spec/compose-go
Pull request #466 (2023-09-28 14:32:08)

avoid to fail if a duplicate resource is the same as a previous added ones
If an exact same resource (service, network...) is included more than once via multiple `include` instructions we should not throw an error but skip it 


Patch:
diff --git a/loader/include.go b/loader/include.go
index ea78414..aaebfd3 100644
--- a/loader/include.go
+++ b/loader/include.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"path/filepath"
+	"reflect"
 
 	"github.com/compose-spec/compose-go/dotenv"
 	interp "github.com/compose-spec/compose-go/interpolation"
@@ -109,37 +110,55 @@ func loadInclude(ctx context.Context, filename string, configDetails types.Confi
 func importResources(model *types.Config, imported *types.Project, path []string) error {
 	services := mapByName(model.Services)
 	for _, service := range imported.Services {
-		if _, ok := services[service.Name]; ok {
+		if present, ok := services[service.Name]; ok {
+			if reflect.DeepEqual(present, service) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting service %s", path, service.Name)
 		}
 		model.Services = append(model.Services, service)
 	}
 	for _, service := range imported.DisabledServices {
-		if _, ok := services[service.Name]; ok {
+		if disabled, ok := services[service.Name]; ok {
+			if reflect.DeepEqual(disabled, service) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting service %s", path, service.Name)
 		}
 		model.Services = append(model.Services, service)
 	}
 	for n, network := range imported.Networks {
-		if _, ok := model.Networks[n]; ok {
+		if present, ok := model.Networks[n]; ok {
+			if reflect.DeepEqual(present, network) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting network %s", path, n)
 		}
 		model.Networks[n] = network
 	}
 	for n, volume := range imported.Volumes {
-		if _, ok := model.Volumes[n]; ok {
+		if present, ok := model.Volumes[n]; ok {
+			if reflect.DeepEqual(present, volume) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting volume %s", path, n)
 		}
 		model.Volumes[n] = volume
 	}
 	for n, secret := range imported.Secrets {
-		if _, ok := model.Secrets[n]; ok {
+		if present, ok := model.Secrets[n]; ok {
+			if reflect.DeepEqual(present, secret) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting secret %s", path, n)
 		}
 		model.Secrets[n] = secret
 	}
 	for n, config := range imported.Configs {
-		if _, ok := model.Configs[n]; ok {
+		if present, ok := model.Configs[n]; ok {
+			if reflect.DeepEqual(present, config) {
+				continue
+			}
 			return fmt.Errorf("imported compose file %s defines conflicting config %s", path, n)
 		}
 		model.Configs[n] = config


Test patch:
diff --git a/loader/loader_test.go b/loader/loader_test.go
index b40c447..44ffc15 100644
--- a/loader/loader_test.go
+++ b/loader/loader_test.go
@@ -2604,13 +2604,56 @@ func TestLoadWithIncludeCycle(t *testing.T) {
 		WorkingDir: filepath.Join(workingDir, "testdata"),
 		ConfigFiles: []types.ConfigFile{
 			{
-				Filename: filepath.Join(workingDir, "testdata", "compose-include.yaml"),
+				Filename: filepath.Join(workingDir, "testdata", "compose-include-cycle.yaml"),
 			},
 		},
 	})
 	assert.Check(t, strings.HasPrefix(err.Error(), "include cycle detected"))
 }
 
+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)
+	assert.Equal(t, p.Services[1].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, "defines conflicting service bar", err)
+}
+
 func TestLoadWithDependsOn(t *testing.T) {
 	p, err := loadYAML(`
 name: test-depends-on
diff --git a/loader/testdata/compose-include-cycle.yaml b/loader/testdata/compose-include-cycle.yaml
new file mode 100644
index 0000000..a34172b
--- /dev/null
+++ b/loader/testdata/compose-include-cycle.yaml
@@ -0,0 +1,6 @@
+include:
+  - compose-include-cycle.yaml
+
+services:
+  foo:
+    image: foo
diff --git a/loader/testdata/compose-include.yaml b/loader/testdata/compose-include.yaml
index 6fa5240..e201ab5 100644
--- a/loader/testdata/compose-include.yaml
+++ b/loader/testdata/compose-include.yaml
@@ -1,6 +1,6 @@
 include:
-  - compose-include.yaml
+    - path: ./subdir/compose-test-extends-imported.yaml
 
 services:
-  foo:
-    image: foo
+  bar:
+    image: bar
\ No newline at end of file