Repository: compose-spec/compose-go
Pull request #568 (2024-02-07 10:54:40)

validate `extends` source does not share resources with another service/container
applies https://github.com/compose-spec/compose-spec/blob/master/05-services.md#restrictions


Patch:
diff --git a/loader/extends.go b/loader/extends.go
index 7a224cf..862697e 100644
--- a/loader/extends.go
+++ b/loader/extends.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"path/filepath"
+	"strings"
 
 	"github.com/compose-spec/compose-go/v2/consts"
 	"github.com/compose-spec/compose-go/v2/override"
@@ -102,6 +103,12 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
 		return service, nil
 	}
 	source := deepClone(base).(map[string]any)
+
+	err = validateExtendSource(source, ref)
+	if err != nil {
+		return nil, err
+	}
+
 	for _, processor := range post {
 		processor.Apply(map[string]any{
 			"services": map[string]any{
@@ -117,6 +124,30 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
 	return merged, nil
 }
 
+// validateExtendSource check the source for `extends` doesn't refer to another container/service
+func validateExtendSource(source map[string]any, ref string) error {
+	forbidden := []string{"links", "volumes_from", "depends_on"}
+	for _, key := range forbidden {
+		if _, ok := source[key]; ok {
+			return fmt.Errorf("service %q can't be used with `extends` as it declare `%s`", ref, key)
+		}
+	}
+
+	sharedNamespace := []string{"network_mode", "ipc", "pid", "net", "cgroup", "userns_mode", "uts"}
+	for _, key := range sharedNamespace {
+		if v, ok := source[key]; ok {
+			val := v.(string)
+			if strings.HasPrefix(val, types.ContainerPrefix) {
+				return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another container", ref, key)
+			}
+			if strings.HasPrefix(val, types.ServicePrefix) {
+				return fmt.Errorf("service %q can't be used with `extends` as it shares `%s` with another service", ref, key)
+			}
+		}
+	}
+	return nil
+}
+
 func getExtendsBaseFromFile(ctx context.Context, name string, path string, opts *Options, ct *cycleTracker) (map[string]any, error) {
 	for _, loader := range opts.ResourceLoaders {
 		if !loader.Accept(path) {


Test patch:
diff --git a/loader/extends_test.go b/loader/extends_test.go
index a3ac442..5399df6 100644
--- a/loader/extends_test.go
+++ b/loader/extends_test.go
@@ -284,3 +284,77 @@ services:
 	assert.NilError(t, err)
 	assert.Equal(t, svcB.Build.Context, tmpdir)
 }
+
+func TestRejectExtendsWithServiceRef(t *testing.T) {
+	tests := []struct {
+		name    string
+		yaml    string
+		wantErr string
+	}{
+		{
+			name: "volumes_from",
+			yaml: `
+name: test-extends_with_volumes_from
+services:
+  foo:
+    volumes_from:
+      - zot
+  bar:
+    extends:
+      service: foo
+`,
+			wantErr: "service \"foo\" can't be used with `extends` as it declare `volumes_from`",
+		},
+		{
+			name: "depends_on",
+			yaml: `
+name: test-extends_with_depends_on
+services:
+  foo:
+    depends_on:
+      - zot
+  bar:
+    extends:
+      service: foo
+`,
+			wantErr: "service \"foo\" can't be used with `extends` as it declare `depends_on`",
+		},
+		{
+			name: "shared ipc",
+			yaml: `
+name: test-extends_with_shared_ipc
+services:
+  foo:
+    ipc: "service:zot"
+  bar:
+    extends:
+      service: foo
+`,
+			wantErr: "service \"foo\" can't be used with `extends` as it shares `ipc` with another service",
+		},
+		{
+			name: "shared network_mode",
+			yaml: `
+name: test-extends_with_shared_network_mode
+services:
+  foo:
+    network_mode: "container:123abc"
+  bar:
+    extends:
+      service: foo
+`,
+			wantErr: "service \"foo\" can't be used with `extends` as it shares `network_mode` with another container",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := LoadWithContext(context.Background(), types.ConfigDetails{
+				ConfigFiles: []types.ConfigFile{{
+					Content: []byte(tt.yaml),
+				}},
+			})
+			assert.ErrorContains(t, err, tt.wantErr)
+		})
+	}
+}
diff --git a/loader/testdata/extends/withdir/compose.yaml b/loader/testdata/extends/withdir/compose.yaml
new file mode 100644
index 0000000..a198a15
--- /dev/null
+++ b/loader/testdata/extends/withdir/compose.yaml
@@ -0,0 +1,5 @@
+services:
+  test:
+    extends:
+      file: dir/compose.yaml
+      service: test
diff --git a/loader/testdata/extends/withdir/dir/base.yaml b/loader/testdata/extends/withdir/dir/base.yaml
new file mode 100644
index 0000000..e650e1c
--- /dev/null
+++ b/loader/testdata/extends/withdir/dir/base.yaml
@@ -0,0 +1,4 @@
+services:
+  test:
+    image: hello-world
+
diff --git a/loader/testdata/extends/withdir/dir/compose.yaml b/loader/testdata/extends/withdir/dir/compose.yaml
new file mode 100644
index 0000000..cb075cc
--- /dev/null
+++ b/loader/testdata/extends/withdir/dir/compose.yaml
@@ -0,0 +1,5 @@
+services:
+  test:
+    extends:
+      file: base.yaml
+      service: test