istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/eds_sh_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package xds_test 15 16 import ( 17 "fmt" 18 "sort" 19 "testing" 20 "time" 21 22 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 23 endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 24 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 25 "google.golang.org/protobuf/types/known/structpb" 26 27 meshconfig "istio.io/api/mesh/v1alpha1" 28 "istio.io/istio/pilot/pkg/model" 29 "istio.io/istio/pilot/pkg/serviceregistry" 30 "istio.io/istio/pilot/pkg/serviceregistry/aggregate" 31 "istio.io/istio/pilot/pkg/serviceregistry/memory" 32 "istio.io/istio/pilot/pkg/serviceregistry/provider" 33 v3 "istio.io/istio/pilot/pkg/xds/v3" 34 "istio.io/istio/pilot/test/xds" 35 "istio.io/istio/pilot/test/xdstest" 36 "istio.io/istio/pkg/cluster" 37 "istio.io/istio/pkg/config/mesh" 38 "istio.io/istio/pkg/config/protocol" 39 "istio.io/istio/pkg/network" 40 ) 41 42 // Testing the Split Horizon EDS. 43 44 type expectedResults struct { 45 weights map[string]uint32 46 } 47 48 func (r expectedResults) getAddrs() []string { 49 var out []string 50 for addr := range r.weights { 51 out = append(out, addr) 52 } 53 sort.Strings(out) 54 return out 55 } 56 57 // The test will setup 3 networks with various number of endpoints for the same service within 58 // each network. It creates an instance of memory registry for each cluster and populate it 59 // with Service, Instances and an ingress gateway service. 60 // It then conducts an EDS query from each network expecting results to match the design of 61 // the Split Horizon EDS - all local endpoints + endpoint per remote network that also has 62 // endpoints for the service. 63 func TestSplitHorizonEds(t *testing.T) { 64 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{NetworksWatcher: mesh.NewFixedNetworksWatcher(nil)}) 65 66 // Set up a cluster registry for network 1 with 1 instance for the service 'service5' 67 // Network has 1 gateway 68 initRegistry(s, 1, []string{"159.122.219.1"}, 1) 69 // Set up a cluster registry for network 2 with 2 instances for the service 'service5' 70 // Network has 1 gateway 71 initRegistry(s, 2, []string{"159.122.219.2"}, 2) 72 // Set up a cluster registry for network 3 with 3 instances for the service 'service5' 73 // Network has 2 gateways 74 initRegistry(s, 3, []string{"159.122.219.3", "179.114.119.3"}, 3) 75 // Set up a cluster registry for network 4 with 4 instances for the service 'service5' 76 // but without any gateway, which is treated as accessible directly. 77 initRegistry(s, 4, []string{}, 4) 78 79 // Push contexts needs to be updated 80 s.Discovery.ConfigUpdate(&model.PushRequest{Full: true}) 81 time.Sleep(time.Millisecond * 200) // give time for cache to clear 82 83 tests := []struct { 84 network string 85 sidecarID string 86 want expectedResults 87 }{ 88 { 89 // Verify that EDS from network1 will return 1 local endpoint with local VIP + 2 remote 90 // endpoints weighted accordingly with the IP of the ingress gateway. 91 network: "network1", 92 sidecarID: sidecarID("10.1.0.1", "app3"), 93 want: expectedResults{ 94 weights: map[string]uint32{ 95 // 1 local endpoint 96 "10.1.0.1": 2, 97 98 // 2 endopints on network 2, go through single gateway. 99 "159.122.219.2": 4, 100 101 // 3 endpoints on network 3, weights split across 2 gateways. 102 "159.122.219.3": 3, 103 "179.114.119.3": 3, 104 105 // no gateway defined for network 4 - treat as directly reachable. 106 "10.4.0.1": 2, 107 "10.4.0.2": 2, 108 "10.4.0.3": 2, 109 "10.4.0.4": 2, 110 }, 111 }, 112 }, 113 { 114 // Verify that EDS from network2 will return 2 local endpoints with local VIPs + 2 remote 115 // endpoints weighted accordingly with the IP of the ingress gateway. 116 network: "network2", 117 sidecarID: sidecarID("10.2.0.1", "app3"), 118 want: expectedResults{ 119 weights: map[string]uint32{ 120 // 2 local endpoints 121 "10.2.0.1": 2, 122 "10.2.0.2": 2, 123 124 // 1 endpoint on network 1, accessed via gateway. 125 "159.122.219.1": 2, 126 127 // 3 endpoints on network 3, weights split across 2 gateways. 128 "159.122.219.3": 3, 129 "179.114.119.3": 3, 130 131 // no gateway defined for network 4 - treat as directly reachable. 132 "10.4.0.1": 2, 133 "10.4.0.2": 2, 134 "10.4.0.3": 2, 135 "10.4.0.4": 2, 136 }, 137 }, 138 }, 139 { 140 // Verify that EDS from network3 will return 3 local endpoints with local VIPs + 2 remote 141 // endpoints weighted accordingly with the IP of the ingress gateway. 142 network: "network3", 143 sidecarID: sidecarID("10.3.0.1", "app3"), 144 want: expectedResults{ 145 weights: map[string]uint32{ 146 // 3 local endpoints. 147 "10.3.0.1": 2, 148 "10.3.0.2": 2, 149 "10.3.0.3": 2, 150 151 // 1 endpoint on network 1, accessed via gateway. 152 "159.122.219.1": 2, 153 154 // 2 endpoint on network 2, accessed via gateway. 155 "159.122.219.2": 4, 156 157 // no gateway defined for network 4 - treat as directly reachable. 158 "10.4.0.1": 2, 159 "10.4.0.2": 2, 160 "10.4.0.3": 2, 161 "10.4.0.4": 2, 162 }, 163 }, 164 }, 165 { 166 // Verify that EDS from network4 will return 4 local endpoint with local VIP + 4 remote 167 // endpoints weighted accordingly with the IP of the ingress gateway. 168 network: "network4", 169 sidecarID: sidecarID("10.4.0.1", "app3"), 170 want: expectedResults{ 171 weights: map[string]uint32{ 172 // 4 local endpoints. 173 "10.4.0.1": 2, 174 "10.4.0.2": 2, 175 "10.4.0.3": 2, 176 "10.4.0.4": 2, 177 178 // 1 endpoint on network 1, accessed via gateway. 179 "159.122.219.1": 2, 180 181 // 2 endpoint on network 2, accessed via gateway. 182 "159.122.219.2": 4, 183 184 // 3 endpoints on network 3, weights split across 2 gateways. 185 "159.122.219.3": 3, 186 "179.114.119.3": 3, 187 }, 188 }, 189 }, 190 } 191 for _, tt := range tests { 192 t.Run("from "+tt.network, func(t *testing.T) { 193 verifySplitHorizonResponse(t, s, tt.network, tt.sidecarID, tt.want) 194 }) 195 } 196 } 197 198 // Tests whether an EDS response from the provided network matches the expected results 199 func verifySplitHorizonResponse(t *testing.T, s *xds.FakeDiscoveryServer, network string, sidecarID string, expected expectedResults) { 200 t.Helper() 201 ads := s.ConnectADS().WithID(sidecarID) 202 203 metadata := &structpb.Struct{Fields: map[string]*structpb.Value{ 204 "ISTIO_VERSION": {Kind: &structpb.Value_StringValue{StringValue: "1.3"}}, 205 "NETWORK": {Kind: &structpb.Value_StringValue{StringValue: network}}, 206 }} 207 208 ads.RequestResponseAck(t, &discovery.DiscoveryRequest{ 209 Node: &core.Node{ 210 Id: ads.ID, 211 Metadata: metadata, 212 }, 213 TypeUrl: v3.ClusterType, 214 }) 215 216 clusterName := "outbound|1080||service5.default.svc.cluster.local" 217 res := ads.RequestResponseAck(t, &discovery.DiscoveryRequest{ 218 Node: &core.Node{ 219 Id: ads.ID, 220 Metadata: metadata, 221 }, 222 TypeUrl: v3.EndpointType, 223 ResourceNames: []string{clusterName}, 224 }) 225 cla := xdstest.UnmarshalClusterLoadAssignment(t, res.Resources)[0] 226 eps := cla.Endpoints 227 228 if len(eps) != 1 { 229 t.Fatalf("expecting 1 locality endpoint but got %d", len(eps)) 230 } 231 232 lbEndpoints := eps[0].LbEndpoints 233 if len(lbEndpoints) != len(expected.weights) { 234 t.Fatalf("unexpected number of endpoints.\nWant:\n%v\nGot:\n%v", expected.getAddrs(), getLbEndpointAddrs(lbEndpoints)) 235 } 236 237 for addr, weight := range expected.weights { 238 var match *endpoint.LbEndpoint 239 for _, ep := range lbEndpoints { 240 if ep.GetEndpoint().Address.GetSocketAddress().Address == addr { 241 match = ep 242 break 243 } 244 } 245 if match == nil { 246 t.Fatalf("couldn't find endpoint with address %s: found %v", addr, getLbEndpointAddrs(lbEndpoints)) 247 } 248 if match.LoadBalancingWeight.Value != weight { 249 t.Errorf("weight for endpoint %s is expected to be %d but got %d", addr, weight, match.LoadBalancingWeight.Value) 250 } 251 } 252 } 253 254 // initRegistry creates and initializes a memory registry that holds a single 255 // service with the provided amount of endpoints. It also creates a service for 256 // the ingress with the provided external IP 257 func initRegistry(server *xds.FakeDiscoveryServer, networkNum int, gatewaysIP []string, numOfEndpoints int) { 258 clusterID := cluster.ID(fmt.Sprintf("cluster%d", networkNum)) 259 networkID := network.ID(fmt.Sprintf("network%d", networkNum)) 260 memRegistry := memory.NewServiceDiscovery() 261 memRegistry.XdsUpdater = server.Discovery 262 memRegistry.ClusterID = clusterID 263 264 reg := serviceregistry.Simple{ 265 ClusterID: clusterID, 266 ProviderID: provider.Mock, 267 DiscoveryController: memRegistry, 268 } 269 server.Env().ServiceDiscovery.(*aggregate.Controller).AddRegistry(reg) 270 271 gws := make([]*meshconfig.Network_IstioNetworkGateway, 0) 272 for _, gatewayIP := range gatewaysIP { 273 if gatewayIP != "" { 274 gw := &meshconfig.Network_IstioNetworkGateway{ 275 Gw: &meshconfig.Network_IstioNetworkGateway_Address{ 276 Address: gatewayIP, 277 }, 278 Port: 80, 279 } 280 gws = append(gws, gw) 281 } 282 } 283 284 if len(gws) != 0 { 285 addNetwork(server, networkID, &meshconfig.Network{ 286 Gateways: gws, 287 }) 288 } 289 290 svcLabels := map[string]string{ 291 "version": "v1.1", 292 } 293 294 // Explicit test service, in the v2 memory registry. Similar with mock.MakeService, 295 // but easier to read. 296 memRegistry.AddService(&model.Service{ 297 Hostname: "service5.default.svc.cluster.local", 298 DefaultAddress: "10.10.0.1", 299 Ports: []*model.Port{ 300 { 301 Name: "http-main", 302 Port: 1080, 303 Protocol: protocol.HTTP, 304 }, 305 }, 306 Attributes: model.ServiceAttributes{Namespace: "default"}, 307 }) 308 istioEndpoints := make([]*model.IstioEndpoint, numOfEndpoints) 309 for i := 0; i < numOfEndpoints; i++ { 310 addr := fmt.Sprintf("10.%d.0.%d", networkNum, i+1) 311 istioEndpoints[i] = &model.IstioEndpoint{ 312 Address: addr, 313 EndpointPort: 2080, 314 ServicePortName: "http-main", 315 Network: networkID, 316 Locality: model.Locality{ 317 Label: "az", 318 ClusterID: clusterID, 319 }, 320 Labels: svcLabels, 321 TLSMode: model.IstioMutualTLSModeLabel, 322 } 323 } 324 memRegistry.SetEndpoints("service5.default.svc.cluster.local", "default", istioEndpoints) 325 } 326 327 func addNetwork(server *xds.FakeDiscoveryServer, id network.ID, network *meshconfig.Network) { 328 meshNetworks := server.Env().NetworksWatcher.Networks() 329 // copy old networks if they exist 330 c := map[string]*meshconfig.Network{} 331 if meshNetworks != nil { 332 for k, v := range meshNetworks.Networks { 333 c[k] = v 334 } 335 } 336 // add the new one 337 c[string(id)] = network 338 server.Env().NetworksWatcher.SetNetworks(&meshconfig.MeshNetworks{Networks: c}) 339 } 340 341 func getLbEndpointAddrs(eps []*endpoint.LbEndpoint) []string { 342 addrs := make([]string, 0) 343 for _, lbEp := range eps { 344 addrs = append(addrs, lbEp.GetEndpoint().Address.GetSocketAddress().Address) 345 } 346 sort.Strings(addrs) 347 return addrs 348 }