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  }