diff --git a/loader/extends.go b/loader/extends.go index 07bf32ba..cdfed509 100644 --- a/loader/extends.go +++ b/loader/extends.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "path/filepath" - "strings" "github.com/compose-spec/compose-go/v2/consts" "github.com/compose-spec/compose-go/v2/override" @@ -106,11 +105,6 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a } 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{ @@ -127,30 +121,6 @@ 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) { diff --git a/loader/extends_test.go b/loader/extends_test.go index 19516277..22900e95 100644 --- a/loader/extends_test.go +++ b/loader/extends_test.go @@ -306,7 +306,7 @@ services: assert.Equal(t, extendsCount, 3) } -func TestRejectExtendsWithServiceRef(t *testing.T) { +func TestExtendsWithServiceRef(t *testing.T) { tests := []struct { name string yaml string @@ -317,54 +317,48 @@ func TestRejectExtendsWithServiceRef(t *testing.T) { yaml: ` name: test-extends_with_volumes_from services: - foo: - volumes_from: - - zot - bar: + bar: extends: - service: foo + file: ./testdata/extends/depends_on.yaml + service: with_volumes_from `, - wantErr: "service \"foo\" can't be used with `extends` as it declare `volumes_from`", + wantErr: `service "bar" depends on undefined service "zot"`, }, { name: "depends_on", yaml: ` name: test-extends_with_depends_on services: - foo: - depends_on: - - zot bar: extends: - service: foo + file: ./testdata/extends/depends_on.yaml + service: with_depends_on `, - wantErr: "service \"foo\" can't be used with `extends` as it declare `depends_on`", + wantErr: `service "bar" depends on undefined service "zot"`, }, { name: "shared ipc", yaml: ` name: test-extends_with_shared_ipc services: - foo: - ipc: "service:zot" bar: extends: - service: foo + file: ./testdata/extends/depends_on.yaml + service: with_ipc `, - wantErr: "service \"foo\" can't be used with `extends` as it shares `ipc` with another service", + wantErr: `service "bar" depends on undefined service "zot"`, }, { name: "shared network_mode", yaml: ` name: test-extends_with_shared_network_mode services: - foo: - network_mode: "container:123abc" bar: extends: - service: foo + file: ./testdata/extends/depends_on.yaml + service: with_network_mode `, - wantErr: "service \"foo\" can't be used with `extends` as it shares `network_mode` with another container", + wantErr: `service "bar" depends on undefined service "zot"`, }, } @@ -376,6 +370,17 @@ services: }}, }) assert.ErrorContains(t, err, tt.wantErr) + + // Do the same but with a local `zot` service matching the imported reference + _, err = LoadWithContext(context.Background(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{ + Content: []byte(tt.yaml + ` + zot: + image: zot +`), + }}, + }) + assert.NilError(t, err) }) } } diff --git a/loader/testdata/extends/depends_on.yaml b/loader/testdata/extends/depends_on.yaml new file mode 100644 index 00000000..defde838 --- /dev/null +++ b/loader/testdata/extends/depends_on.yaml @@ -0,0 +1,21 @@ +services: + with_depends_on: + image: foo + depends_on: + - zot + + with_volumes_from: + image: foo + volumes_from: + - zot + + with_ipc: + image: foo + ipc: "service:zot" + + with_network_mode: + image: foo + network_mode: "service:zot" + + zot: + image: hidden diff --git a/loader/validate.go b/loader/validate.go index f7f6e2aa..c5133f96 100644 --- a/loader/validate.go +++ b/loader/validate.go @@ -73,7 +73,7 @@ func checkConsistency(project *types.Project) error { for dependedService := range s.DependsOn { if _, err := project.GetService(dependedService); err != nil { - return fmt.Errorf("service %q depends on undefined service %s: %w", s.Name, dependedService, errdefs.ErrInvalid) + return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid) } } // Check there isn't a cycle in depends_on declarations diff --git a/loader/validate_test.go b/loader/validate_test.go index c480db4e..93ee77fd 100644 --- a/loader/validate_test.go +++ b/loader/validate_test.go @@ -258,7 +258,7 @@ func TestValidateDependsOn(t *testing.T) { }, } err := checkConsistency(&project) - assert.Error(t, err, `service "myservice" depends on undefined service missingservice: invalid compose project`) + assert.Error(t, err, `service "myservice" depends on undefined service "missingservice": invalid compose project`) } func TestValidateContainerName(t *testing.T) {