diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 9d8da3c6d81b..b7fa0fd09169 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -123,6 +123,14 @@ func toServiceConfigObjConfigsMap(s any) (map[any]any, error) { return m, nil } +// servicePortKey identifies a port during a merge; the same published +// port may be exposed for both tcp and udp, so the protocol is part of +// the identity. +type servicePortKey struct { + published uint32 + protocol string +} + func toServicePortConfigsMap(s any) (map[any]any, error) { ports, ok := s.([]types.ServicePortConfig) if !ok { @@ -130,7 +138,13 @@ func toServicePortConfigsMap(s any) (map[any]any, error) { } m := map[any]any{} for _, p := range ports { - m[p.Published] = p + protocol := p.Protocol + if protocol == "" { + // long syntax allows the protocol to be omitted, in which + // case it defaults to tcp + protocol = "tcp" + } + m[servicePortKey{published: p.Published, protocol: protocol}] = p } return m, nil } @@ -172,7 +186,12 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[any]any) error { for _, v := range m { s = append(s, v.(types.ServicePortConfig)) } - sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published }) + sort.Slice(s, func(i, j int) bool { + if s[i].Published != s[j].Published { + return s[i].Published < s[j].Published + } + return s[i].Protocol < s[j].Protocol + }) dst.Set(reflect.ValueOf(s)) return nil } diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index 11c1e888ead2..ac75b14989bd 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -280,6 +280,58 @@ func TestLoadMultipleServicePorts(t *testing.T) { }, }, }, + { + name: "same_published_different_protocol", + portBase: map[string]any{ + "ports": []any{ + "443:443", + "443:443/udp", + }, + }, + portOverride: map[string]any{}, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 443, + Target: 443, + Protocol: "tcp", + }, + { + Mode: "ingress", + Published: 443, + Target: 443, + Protocol: "udp", + }, + }, + }, + { + name: "override_same_published_different_protocol", + portBase: map[string]any{ + "ports": []any{ + "443:443", + "443:443/udp", + }, + }, + portOverride: map[string]any{ + "ports": []any{ + "443:8443/udp", + }, + }, + expected: []types.ServicePortConfig{ + { + Mode: "ingress", + Published: 443, + Target: 443, + Protocol: "tcp", + }, + { + Mode: "ingress", + Published: 443, + Target: 8443, + Protocol: "udp", + }, + }, + }, { name: "override_same_published", portBase: map[string]any{