istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/delta_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  
    15  package xds_test
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  	"testing"
    21  
    22  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    23  
    24  	"istio.io/istio/pilot/pkg/features"
    25  	"istio.io/istio/pilot/pkg/model"
    26  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    27  	"istio.io/istio/pilot/test/xds"
    28  	"istio.io/istio/pilot/test/xdstest"
    29  	"istio.io/istio/pkg/config/protocol"
    30  	"istio.io/istio/pkg/config/schema/kind"
    31  	"istio.io/istio/pkg/slices"
    32  	"istio.io/istio/pkg/test"
    33  	"istio.io/istio/pkg/test/util/assert"
    34  	"istio.io/istio/pkg/util/sets"
    35  	"istio.io/istio/pkg/workloadapi"
    36  	xdsserver "istio.io/istio/pkg/xds"
    37  )
    38  
    39  func TestDeltaAds(t *testing.T) {
    40  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
    41  	ads := s.ConnectDeltaADS().WithType(v3.ClusterType)
    42  	ads.RequestResponseAck(nil)
    43  }
    44  
    45  func TestDeltaAdsClusterUpdate(t *testing.T) {
    46  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
    47  	ads := s.ConnectDeltaADS().WithType(v3.EndpointType)
    48  	nonce := ""
    49  	sendEDSReqAndVerify := func(add, remove, expect []string) {
    50  		t.Helper()
    51  		res := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{
    52  			ResponseNonce:            nonce,
    53  			ResourceNamesSubscribe:   add,
    54  			ResourceNamesUnsubscribe: remove,
    55  		})
    56  		nonce = res.Nonce
    57  		got := xdstest.MapKeys(xdstest.ExtractLoadAssignments(xdstest.UnmarshalClusterLoadAssignment(t, xdsserver.ResourcesToAny(res.Resources))))
    58  		if !reflect.DeepEqual(expect, got) {
    59  			t.Fatalf("expected clusters %v got %v", expect, got)
    60  		}
    61  	}
    62  
    63  	sendEDSReqAndVerify([]string{"outbound|80||local.default.svc.cluster.local"}, nil, []string{"outbound|80||local.default.svc.cluster.local"})
    64  	// Only send the one that is requested
    65  	sendEDSReqAndVerify([]string{"outbound|81||local.default.svc.cluster.local"}, nil, []string{"outbound|81||local.default.svc.cluster.local"})
    66  	ads.Request(&discovery.DeltaDiscoveryRequest{
    67  		ResponseNonce:            nonce,
    68  		ResourceNamesUnsubscribe: []string{"outbound|81||local.default.svc.cluster.local"},
    69  	})
    70  	ads.ExpectNoResponse()
    71  }
    72  
    73  func TestDeltaCDS(t *testing.T) {
    74  	base := sets.New("BlackHoleCluster", "PassthroughCluster", "InboundPassthroughClusterIpv4")
    75  	assertResources := func(resp *discovery.DeltaDiscoveryResponse, names ...string) {
    76  		t.Helper()
    77  		got := slices.Map(resp.Resources, (*discovery.Resource).GetName)
    78  
    79  		assert.Equal(t, sets.New(got...), sets.New(names...).Merge(base))
    80  	}
    81  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
    82  	addTestClientEndpoints(s.MemRegistry)
    83  	s.MemRegistry.AddHTTPService(edsIncSvc, edsIncVip, 8080)
    84  	s.MemRegistry.SetEndpoints(edsIncSvc, "",
    85  		newEndpointWithAccount("127.0.0.1", "hello-sa", "v1"))
    86  	// Wait until the above debounce, to ensure we can precisely check XDS responses without spurious pushes
    87  	s.EnsureSynced(t)
    88  
    89  	ads := s.ConnectDeltaADS().WithID("sidecar~127.0.0.1~test.default~default.svc.cluster.local")
    90  
    91  	// Initially we get everything
    92  	ads.Request(&discovery.DeltaDiscoveryRequest{
    93  		ResourceNamesSubscribe: []string{},
    94  	})
    95  	resp := ads.ExpectResponse()
    96  	assertResources(resp, "outbound|80||test-1.default", "outbound|8080||eds.test.svc.cluster.local", "inbound|80||")
    97  	assert.Equal(t, resp.RemovedResources, nil)
    98  
    99  	// On remove, just get the removal
   100  	s.MemRegistry.RemoveService("test-1.default")
   101  	resp = ads.ExpectResponse()
   102  	assertResources(resp, "inbound|80||") // currently we always send the inbound stuff. Not ideal, but acceptable
   103  	assert.Equal(t, resp.RemovedResources, []string{"outbound|80||test-1.default"})
   104  
   105  	// Another removal should behave the same
   106  	s.MemRegistry.RemoveService("eds.test.svc.cluster.local")
   107  	resp = ads.ExpectResponse()
   108  	assertResources(resp)
   109  	assert.Equal(t, resp.RemovedResources, []string{"inbound|80||", "outbound|8080||eds.test.svc.cluster.local"})
   110  }
   111  
   112  func TestDeltaCDSReconnect(t *testing.T) {
   113  	base := sets.New("BlackHoleCluster", "PassthroughCluster", "InboundPassthroughClusterIpv4")
   114  	assertResources := func(resp *discovery.DeltaDiscoveryResponse, names ...string) {
   115  		t.Helper()
   116  		got := slices.Map(resp.Resources, (*discovery.Resource).GetName)
   117  
   118  		assert.Equal(t, sets.New(got...), sets.New(names...).Merge(base))
   119  	}
   120  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
   121  	addTestClientEndpoints(s.MemRegistry)
   122  	s.MemRegistry.AddHTTPService(edsIncSvc, edsIncVip, 8080)
   123  	s.MemRegistry.SetEndpoints(edsIncSvc, "",
   124  		newEndpointWithAccount("127.0.0.1", "hello-sa", "v1"))
   125  	// Wait until the above debounce, to ensure we can precisely check XDS responses without spurious pushes
   126  	s.EnsureSynced(t)
   127  
   128  	ads := s.ConnectDeltaADS()
   129  
   130  	// Initially we get everything
   131  	resp := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{})
   132  	assertResources(resp, "outbound|80||test-1.default", "outbound|8080||eds.test.svc.cluster.local")
   133  	assert.Equal(t, resp.RemovedResources, nil)
   134  
   135  	// Disconnect and remove a service
   136  	ads.Cleanup()
   137  	s.MemRegistry.RemoveService("test-1.default")
   138  	s.MemRegistry.AddHTTPService("eds2.test.svc.cluster.local", "10.10.1.3", 8080)
   139  	s.EnsureSynced(t)
   140  	ads = s.ConnectDeltaADS()
   141  	resp = ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{
   142  		InitialResourceVersions: map[string]string{
   143  			"outbound|80||test-1.default":               "",
   144  			"outbound|8080||eds.test.svc.cluster.local": "",
   145  		},
   146  	})
   147  	assertResources(resp, "outbound|8080||eds.test.svc.cluster.local", "outbound|8080||eds2.test.svc.cluster.local")
   148  	assert.Equal(t, resp.RemovedResources, []string{"outbound|80||test-1.default"})
   149  
   150  	// Another removal should behave the same
   151  	s.MemRegistry.RemoveService("eds.test.svc.cluster.local")
   152  	resp = ads.ExpectResponse()
   153  	// ACK
   154  	ads.Request(&discovery.DeltaDiscoveryRequest{
   155  		TypeUrl:       resp.TypeUrl,
   156  		ResponseNonce: resp.Nonce,
   157  	})
   158  	assertResources(resp)
   159  	assert.Equal(t, resp.RemovedResources, []string{"outbound|8080||eds.test.svc.cluster.local"})
   160  
   161  	// Another removal should behave the same
   162  	s.MemRegistry.RemoveService("eds2.test.svc.cluster.local")
   163  	resp = ads.ExpectResponse()
   164  	assertResources(resp)
   165  	assert.Equal(t, resp.RemovedResources, []string{"outbound|8080||eds2.test.svc.cluster.local"})
   166  }
   167  
   168  func TestDeltaEDS(t *testing.T) {
   169  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   170  		ConfigString: mustReadFile(t, "tests/testdata/config/destination-rule-locality.yaml"),
   171  	})
   172  	addTestClientEndpoints(s.MemRegistry)
   173  	s.MemRegistry.AddHTTPService(edsIncSvc, edsIncVip, 8080)
   174  	s.MemRegistry.SetEndpoints(edsIncSvc, "",
   175  		newEndpointWithAccount("127.0.0.1", "hello-sa", "v1"))
   176  
   177  	// Wait until the above debounce, to ensure we can precisely check XDS responses without spurious pushes
   178  	s.EnsureSynced(t)
   179  
   180  	ads := s.ConnectDeltaADS().WithType(v3.EndpointType)
   181  	ads.Request(&discovery.DeltaDiscoveryRequest{
   182  		ResourceNamesSubscribe: []string{"outbound|80||test-1.default"},
   183  	})
   184  	resp := ads.ExpectResponse()
   185  	if len(resp.Resources) != 1 || resp.Resources[0].Name != "outbound|80||test-1.default" {
   186  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   187  	}
   188  	if len(resp.RemovedResources) != 0 {
   189  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   190  	}
   191  
   192  	ads.Request(&discovery.DeltaDiscoveryRequest{
   193  		ResourceNamesSubscribe: []string{"outbound|8080||" + edsIncSvc},
   194  	})
   195  	resp = ads.ExpectResponse()
   196  	if len(resp.Resources) != 1 || resp.Resources[0].Name != "outbound|8080||"+edsIncSvc {
   197  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   198  	}
   199  	if len(resp.RemovedResources) != 0 {
   200  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   201  	}
   202  
   203  	// update endpoint
   204  	s.MemRegistry.SetEndpoints(edsIncSvc, "",
   205  		newEndpointWithAccount("127.0.0.2", "hello-sa", "v1"))
   206  	resp = ads.ExpectResponse()
   207  	if len(resp.Resources) != 1 || resp.Resources[0].Name != "outbound|8080||"+edsIncSvc {
   208  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   209  	}
   210  	if len(resp.RemovedResources) != 0 {
   211  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   212  	}
   213  
   214  	t.Logf("update svc")
   215  	// update svc, only send the eds for this service
   216  	s.MemRegistry.AddHTTPService(edsIncSvc, "10.10.1.3", 8080)
   217  
   218  	resp = ads.ExpectResponse()
   219  	if len(resp.Resources) != 1 || resp.Resources[0].Name != "outbound|8080||"+edsIncSvc {
   220  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   221  	}
   222  	if len(resp.RemovedResources) != 0 {
   223  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   224  	}
   225  
   226  	// delete svc, only send eds for this service
   227  	s.MemRegistry.RemoveService(edsIncSvc)
   228  
   229  	resp = ads.ExpectResponse()
   230  	if len(resp.RemovedResources) != 1 || resp.RemovedResources[0] != "outbound|8080||"+edsIncSvc {
   231  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   232  	}
   233  	if len(resp.Resources) != 0 {
   234  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   235  	}
   236  }
   237  
   238  func TestDeltaReconnectRequests(t *testing.T) {
   239  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   240  		Services: []*model.Service{
   241  			{
   242  				Hostname:       "adsupdate.example.com",
   243  				DefaultAddress: "10.11.0.1",
   244  				Ports: []*model.Port{
   245  					{
   246  						Name:     "http-main",
   247  						Port:     2080,
   248  						Protocol: protocol.HTTP,
   249  					},
   250  				},
   251  				Attributes: model.ServiceAttributes{
   252  					Name:      "adsupdate",
   253  					Namespace: "default",
   254  				},
   255  			},
   256  			{
   257  				Hostname:       "adsstatic.example.com",
   258  				DefaultAddress: "10.11.0.2",
   259  				Ports: []*model.Port{
   260  					{
   261  						Name:     "http-main",
   262  						Port:     2080,
   263  						Protocol: protocol.HTTP,
   264  					},
   265  				},
   266  				Attributes: model.ServiceAttributes{
   267  					Name:      "adsstatic",
   268  					Namespace: "default",
   269  				},
   270  			},
   271  		},
   272  	})
   273  
   274  	const updateCluster = "outbound|2080||adsupdate.example.com"
   275  	const staticCluster = "outbound|2080||adsstatic.example.com"
   276  	ads := s.ConnectDeltaADS()
   277  	// Send initial request
   278  	res := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{TypeUrl: v3.ClusterType})
   279  	// we must get the cluster back
   280  	if resn := xdstest.ExtractResource(res.Resources); !resn.Contains(updateCluster) || !resn.Contains(staticCluster) {
   281  		t.Fatalf("unexpected resources: %v", resn)
   282  	}
   283  
   284  	// A push should get a response
   285  	s.Discovery.ConfigUpdate(&model.PushRequest{Full: true})
   286  	ads.ExpectResponse()
   287  
   288  	// Close the connection
   289  	ads.Cleanup()
   290  
   291  	// Service is removed while connection is closed
   292  	s.MemRegistry.RemoveService("adsupdate.example.com")
   293  	s.Discovery.ConfigUpdate(&model.PushRequest{
   294  		Full:           true,
   295  		ConfigsUpdated: sets.New(model.ConfigKey{Kind: kind.ServiceEntry, Name: "adsupdate.example.com", Namespace: "default"}),
   296  	})
   297  	s.EnsureSynced(t)
   298  
   299  	ads = s.ConnectDeltaADS()
   300  	// Sometimes we get an EDS request first before CDS
   301  	ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{
   302  		TypeUrl:                v3.EndpointType,
   303  		ResponseNonce:          "",
   304  		ResourceNamesSubscribe: []string{"outbound|80||local.default.svc.cluster.local"},
   305  	})
   306  
   307  	// Now send initial CDS request
   308  	res = ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{
   309  		TypeUrl: v3.ClusterType,
   310  		InitialResourceVersions: map[string]string{
   311  			// This time we include the version map, since it is a reconnect
   312  			staticCluster: "",
   313  			updateCluster: "",
   314  		},
   315  	})
   316  
   317  	// Expect that we send an EDS response even though there was no request
   318  	resp := ads.ExpectResponse()
   319  	if resp.TypeUrl != v3.EndpointType {
   320  		t.Fatalf("unexpected response type %v. Expected dependent EDS response", resp.TypeUrl)
   321  	}
   322  	// we must NOT get the cluster back
   323  	if resn := xdstest.ExtractResource(res.Resources); resn.Contains(updateCluster) || !resn.Contains(staticCluster) {
   324  		t.Fatalf("unexpected resources: %v", resn)
   325  	}
   326  	// It should be removed
   327  	if resn := sets.New(res.RemovedResources...); !resn.Contains(updateCluster) {
   328  		t.Fatalf("unexpected remove resources: %v", resn)
   329  	}
   330  }
   331  
   332  func TestDeltaWDS(t *testing.T) {
   333  	test.SetForTest(t, &features.EnableAmbient, true)
   334  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
   335  	wlA := &model.WorkloadInfo{
   336  		Workload: &workloadapi.Workload{
   337  			Uid:       fmt.Sprintf("Kubernetes//Pod/%s/%s", "test", "a"),
   338  			Namespace: "test",
   339  			Name:      "a",
   340  		},
   341  	}
   342  	wlB := &model.WorkloadInfo{
   343  		Workload: &workloadapi.Workload{
   344  			Uid:       fmt.Sprintf("Kubernetes//Pod/%s/%s", "test", "b"),
   345  			Namespace: "test",
   346  			Name:      "n",
   347  		},
   348  	}
   349  	wlC := &model.WorkloadInfo{
   350  		Workload: &workloadapi.Workload{
   351  			Uid:       fmt.Sprintf("Kubernetes//Pod/%s/%s", "test", "c"),
   352  			Namespace: "test",
   353  			Name:      "c",
   354  		},
   355  	}
   356  	svcA := &model.ServiceInfo{
   357  		Service: &workloadapi.Service{
   358  			Name:      "a",
   359  			Namespace: "default",
   360  			Hostname:  "a.default.svc.cluster.local",
   361  		},
   362  	}
   363  	svcB := &model.ServiceInfo{
   364  		Service: &workloadapi.Service{
   365  			Name:      "b",
   366  			Namespace: "default",
   367  			Hostname:  "b.default.svc.cluster.local",
   368  		},
   369  	}
   370  	svcC := &model.ServiceInfo{
   371  		Service: &workloadapi.Service{
   372  			Name:      "c",
   373  			Namespace: "default",
   374  			Hostname:  "c.default.svc.cluster.local",
   375  		},
   376  	}
   377  	s.MemRegistry.AddWorkloadInfo(wlA, wlB, wlC)
   378  	s.MemRegistry.AddServiceInfo(svcA, svcB, svcC)
   379  
   380  	// Wait until the above debounce, to ensure we can precisely check XDS responses without spurious pushes
   381  	s.EnsureSynced(t)
   382  
   383  	ads := s.ConnectDeltaADS().WithType(v3.AddressType).WithID("ztunnel~1.1.1.1~test.default~default.svc.cluster.local")
   384  	ads.Request(&discovery.DeltaDiscoveryRequest{
   385  		ResourceNamesSubscribe: []string{"*"},
   386  	})
   387  	resp := ads.ExpectResponse()
   388  	if len(resp.Resources) != 6 {
   389  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   390  	}
   391  	if len(resp.RemovedResources) != 0 {
   392  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   393  	}
   394  
   395  	// simulate a svc update
   396  	s.XdsUpdater.ConfigUpdate(&model.PushRequest{
   397  		ConfigsUpdated: sets.New(model.ConfigKey{
   398  			Kind: kind.Address, Name: svcA.ResourceName(), Namespace: svcA.Namespace,
   399  		}),
   400  	})
   401  
   402  	resp = ads.ExpectResponse()
   403  	if len(resp.Resources) != 1 || resp.Resources[0].Name != svcA.ResourceName() {
   404  		t.Fatalf("received unexpected address resource %v", resp.Resources)
   405  	}
   406  	if len(resp.RemovedResources) != 0 {
   407  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   408  	}
   409  
   410  	// simulate a svc delete
   411  	s.MemRegistry.RemoveServiceInfo(svcA)
   412  	s.XdsUpdater.ConfigUpdate(&model.PushRequest{
   413  		ConfigsUpdated: sets.New(model.ConfigKey{
   414  			Kind: kind.Address, Name: svcA.ResourceName(), Namespace: svcA.Namespace,
   415  		}),
   416  	})
   417  
   418  	resp = ads.ExpectResponse()
   419  	if len(resp.Resources) != 0 {
   420  		t.Fatalf("received unexpected address resource %v", resp.Resources)
   421  	}
   422  	if len(resp.RemovedResources) != 1 || resp.RemovedResources[0] != svcA.ResourceName() {
   423  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   424  	}
   425  
   426  	// delete workload
   427  	s.MemRegistry.RemoveWorkloadInfo(wlA)
   428  	// a full push and a pod delete event
   429  	// This is a merged push request
   430  	s.XdsUpdater.ConfigUpdate(&model.PushRequest{
   431  		Full: true,
   432  	})
   433  
   434  	resp = ads.ExpectResponse()
   435  	if len(resp.RemovedResources) != 1 || resp.RemovedResources[0] != wlA.ResourceName() {
   436  		t.Fatalf("received unexpected removed eds resource %v", resp.RemovedResources)
   437  	}
   438  	if len(resp.Resources) != 4 {
   439  		t.Fatalf("received unexpected eds resource %v", resp.Resources)
   440  	}
   441  }