Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix service entry merge #50711

Merged
merged 9 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 15 additions & 4 deletions pilot/pkg/model/endpointshards.go
Expand Up @@ -94,15 +94,26 @@ func (es *EndpointShards) Keys() []ShardKey {

// CopyEndpoints takes a snapshot of all endpoints. As input, it takes a map of port name to number, to allow it to group
// the results by service port number. This is a bit weird, but lets us efficiently construct the format the caller needs.
func (es *EndpointShards) CopyEndpoints(portMap map[string]int) map[int][]*IstioEndpoint {
func (es *EndpointShards) CopyEndpoints(portMap map[string]int, ports sets.Set[int]) map[int][]*IstioEndpoint {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we initialize the ports sets from portMap in this function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that makes sense. I was thinking it was faster to do it this way, but it shouldn't be

es.RLock()
defer es.RUnlock()
res := map[int][]*IstioEndpoint{}
for _, v := range es.Shards {
for _, ep := range v {
portNum, f := portMap[ep.ServicePortName]
if !f {
continue
// use the port name as the key, unless LegacyClusterPortKey is set and takes precedence
// In EDS we match on port *name*. But for historical reasons, we match on port number for CDS.
var portNum int
if ep.LegacyClusterPortKey != 0 {
if !ports.Contains(ep.LegacyClusterPortKey) {
continue
}
portNum = ep.LegacyClusterPortKey
} else {
pn, f := portMap[ep.ServicePortName]
if !f {
continue
}
portNum = pn
}
res[portNum] = append(res[portNum], ep)
}
Expand Down
4 changes: 3 additions & 1 deletion pilot/pkg/model/push_context.go
Expand Up @@ -1445,8 +1445,10 @@ func (ps *PushContext) initServiceRegistry(env *Environment, configsUpdate sets.

for _, s := range allServices {
portMap := map[string]int{}
ports := sets.New[int]()
for _, port := range s.Ports {
portMap[port.Name] = port.Port
ports.Insert(port.Port)
}

svcKey := s.Key()
Expand All @@ -1455,7 +1457,7 @@ func (ps *PushContext) initServiceRegistry(env *Environment, configsUpdate sets.
}
shards, ok := env.EndpointIndex.ShardsForService(string(s.Hostname), s.Attributes.Namespace)
if ok {
instancesByPort := shards.CopyEndpoints(portMap)
instancesByPort := shards.CopyEndpoints(portMap, ports)
// Iterate over the instances and add them to the service index to avoid overiding the existing port instances.
for port, instances := range instancesByPort {
ps.ServiceIndex.instancesByPort[svcKey][port] = instances
Expand Down
4 changes: 4 additions & 0 deletions pilot/pkg/model/service.go
Expand Up @@ -486,6 +486,10 @@ type IstioEndpoint struct {

// ServicePortName tracks the name of the port, this is used to select the IstioEndpoint by service port.
ServicePortName string
// LegacyClusterPortKey provides an alternative key from ServicePortName to support legacy quirks in the API.
// Basically, EDS merges by port name, but CDS historically ignored port name and matched on number.
// Note that for Kubernetes Service, this is identical - its only ServiceEntry where these checks can differ
LegacyClusterPortKey int

// ServiceAccount holds the associated service account.
ServiceAccount string
Expand Down
17 changes: 11 additions & 6 deletions pilot/pkg/serviceregistry/serviceentry/conversion.go
Expand Up @@ -284,7 +284,9 @@ func (s *Controller) convertEndpoint(service *model.Service, servicePort *networ
Address: addr,
EndpointPort: instancePort,
ServicePortName: servicePort.Name,
Network: network.ID(wle.Network),

LegacyClusterPortKey: int(servicePort.Number),
Network: network.ID(wle.Network),
Locality: model.Locality{
Label: locality,
ClusterID: clusterID,
Expand Down Expand Up @@ -341,11 +343,12 @@ func (s *Controller) convertServiceEntryToInstances(cfg config.Config, services
}
out = append(out, &model.ServiceInstance{
Endpoint: &model.IstioEndpoint{
Address: string(service.Hostname),
EndpointPort: endpointPort,
ServicePortName: serviceEntryPort.Name,
Labels: nil,
TLSMode: model.DisabledTLSModeLabel,
Address: string(service.Hostname),
EndpointPort: endpointPort,
ServicePortName: serviceEntryPort.Name,
LegacyClusterPortKey: int(serviceEntryPort.Number),
Labels: nil,
TLSMode: model.DisabledTLSModeLabel,
},
Service: service,
ServicePort: convertPort(serviceEntryPort),
Expand Down Expand Up @@ -395,6 +398,8 @@ func convertWorkloadInstanceToServiceInstance(workloadInstance *model.WorkloadIn
}
ep := workloadInstance.Endpoint.ShallowCopy()
ep.ServicePortName = serviceEntryPort.Name
ep.LegacyClusterPortKey = int(serviceEntryPort.Number)

ep.EndpointPort = targetPort
out = append(out, &model.ServiceInstance{
Endpoint: ep,
Expand Down
93 changes: 93 additions & 0 deletions pilot/pkg/xds/cds_test.go
Expand Up @@ -31,6 +31,7 @@ import (
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
"istio.io/istio/pkg/ptr"
"istio.io/istio/pkg/slices"
"istio.io/istio/pkg/test/util/assert"
"istio.io/istio/pkg/util/sets"
)
Expand Down Expand Up @@ -232,3 +233,95 @@ func TestSAN(t *testing.T) {
})
}
}

func TestServiceEntryMerge(t *testing.T) {
// Regression test for https://github.com/istio/istio/issues/50478
s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: `apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: se1
spec:
hosts:
- example.com
ports:
- name: port1
number: 80
protocol: HTTP
resolution: DNS
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: se2
spec:
hosts:
- example.com
ports:
- name: port1
number: 8080
protocol: HTTP
resolution: DNS
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: se3
spec:
hosts:
- example.com
ports:
- name: port1
number: 80
protocol: HTTP
resolution: DNS
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: se4
spec:
hosts:
- example.com
ports:
- name: port1
number: 80
targetPort: 1234
protocol: HTTP
resolution: DNS
---
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata:
name: se5
spec:
hosts:
- example.com
ports:
- name: port1
number: 80
targetPort: 999
protocol: HTTP
resolution: DNS
endpoints:
- address: endpoint.example.com
- address: endpoint-port-override.example.com
ports:
port1: 2345
`})

res := xdstest.ExtractClusterEndpoints(s.Clusters(s.SetupProxy(nil)))
// TODO(https://github.com/istio/istio/issues/50749) order should be deterministic
slices.Sort(res["outbound|80||example.com"])
assert.Equal(t, res, map[string][]string{
"outbound|8080||example.com": {"example.com:8080"},
// Kind of weird to have multiple here, but it is what it is...
// If we had targetPort, etc, set here this would be required
"outbound|80||example.com": {
"endpoint-port-override.example.com:2345",
"endpoint.example.com:999",
"example.com:1234",
"example.com:80",
"example.com:80",
},
})
}
9 changes: 9 additions & 0 deletions releasenotes/notes/se-conflict.yaml
@@ -0,0 +1,9 @@
apiVersion: release-notes/v2
kind: bug-fix
area: networking
zirain marked this conversation as resolved.
Show resolved Hide resolved
issue:
- 50478
releaseNotes:
- |
**Fixed** a behavioral change in Istio 1.20 that caused merging of ServiceEntries with the same hostname and port names
to give unexpected results.