Repository: compose-spec/compose-go
Pull request #524 (2024-01-15 07:53:06)

fix regression in cycle detection
cycle tracker must fork as we walk through `extends` services, to be considered as a tree 

closes https://github.com/docker/compose/issues/11343


Patch:
diff --git a/loader/extends.go b/loader/extends.go
index 4bfd2d6..a5614f4 100644
--- a/loader/extends.go
+++ b/loader/extends.go
@@ -26,7 +26,7 @@ import (
 	"github.com/compose-spec/compose-go/v2/types"
 )
 
-func ApplyExtends(ctx context.Context, dict map[string]any, workingdir string, opts *Options, ct *cycleTracker, post ...PostProcessor) error {
+func ApplyExtends(ctx context.Context, dict map[string]any, workingdir string, opts *Options, tracker *cycleTracker, post ...PostProcessor) error {
 	a, ok := dict["services"]
 	if !ok {
 		return nil
@@ -38,7 +38,8 @@ func ApplyExtends(ctx context.Context, dict map[string]any, workingdir string, o
 		if !ok {
 			continue
 		}
-		if err := ct.Add(ctx.Value(consts.ComposeFileKey{}).(string), name); err != nil {
+		ct, err := tracker.Add(ctx.Value(consts.ComposeFileKey{}).(string), name)
+		if err != nil {
 			return err
 		}
 		var (
diff --git a/loader/loader.go b/loader/loader.go
index e075668..58ca98a 100644
--- a/loader/loader.go
+++ b/loader/loader.go
@@ -144,7 +144,7 @@ type cycleTracker struct {
 	loaded []serviceRef
 }
 
-func (ct *cycleTracker) Add(filename, service string) error {
+func (ct *cycleTracker) Add(filename, service string) (*cycleTracker, error) {
 	toAdd := serviceRef{filename: filename, service: service}
 	for _, loaded := range ct.loaded {
 		if toAdd == loaded {
@@ -161,12 +161,16 @@ func (ct *cycleTracker) Add(filename, service string) error {
 				errLines = append(errLines, fmt.Sprintf("  extends %s in %s", service.service, service.filename))
 			}
 
-			return errors.New(strings.Join(errLines, "\n"))
+			return nil, errors.New(strings.Join(errLines, "\n"))
 		}
 	}
 
-	ct.loaded = append(ct.loaded, toAdd)
-	return nil
+	var branch []serviceRef
+	branch = append(branch, ct.loaded...)
+	branch = append(branch, toAdd)
+	return &cycleTracker{
+		loaded: branch,
+	}, nil
 }
 
 // WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to


Test patch:
diff --git a/loader/extends_test.go b/loader/extends_test.go
new file mode 100644
index 0000000..6fa9060
--- /dev/null
+++ b/loader/extends_test.go
@@ -0,0 +1,66 @@
+/*
+   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 TestExtends(t *testing.T) {
+	yaml := `
+name: test-extends
+services:
+  test1:
+    extends:
+      file: testdata/extends/base.yaml
+      service: base
+    hostname: test1
+
+  test2:
+    extends:
+      file: testdata/extends/base.yaml
+      service: base
+    hostname: test2
+
+  test3:
+    extends:
+      file: testdata/extends/base.yaml
+      service: another
+    hostname: test3
+`
+	abs, err := filepath.Abs(".")
+	assert.NilError(t, err)
+
+	p, err := LoadWithContext(context.Background(), types.ConfigDetails{
+		ConfigFiles: []types.ConfigFile{
+			{
+				Content:  []byte(yaml),
+				Filename: "(inline)",
+			},
+		},
+		WorkingDir: abs,
+	})
+	assert.NilError(t, err)
+	assert.DeepEqual(t, p.Services["test1"].Hostname, "test1")
+	assert.Equal(t, p.Services["test2"].Hostname, "test2")
+	assert.Equal(t, p.Services["test3"].Hostname, "test3")
+}
diff --git a/loader/testdata/extends/base.yaml b/loader/testdata/extends/base.yaml
new file mode 100644
index 0000000..8cc2f4a
--- /dev/null
+++ b/loader/testdata/extends/base.yaml
@@ -0,0 +1,7 @@
+services:
+  base:
+    image: base
+
+  another:
+    extends: base
+