/* * * Copyright 2022 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package xdsclient_test import ( "context" "encoding/json" "fmt" "strings" "testing" "time" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "google.golang.org/grpc/internal/grpcsync" "google.golang.org/grpc/internal/testutils" "google.golang.org/grpc/internal/testutils/xds/e2e" "google.golang.org/grpc/internal/xds/bootstrap" "google.golang.org/grpc/internal/xds/xdsclient" "google.golang.org/grpc/internal/xds/xdsclient/xdsresource" v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" _ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter. _ "google.golang.org/grpc/xds" // To ensure internal.NewXDSResolverWithConfigForTesting is set. ) type noopListenerWatcher struct{} func (noopListenerWatcher) ResourceChanged(_ *xdsresource.ListenerUpdate, onDone func()) { onDone() } func (noopListenerWatcher) ResourceError(_ error, onDone func()) { onDone() } func (noopListenerWatcher) AmbientError(_ error, onDone func()) { onDone() } type listenerUpdateErrTuple struct { update *xdsresource.ListenerUpdate err error } type listenerWatcher struct { updateCh *testutils.Channel } func newListenerWatcher() *listenerWatcher { return &listenerWatcher{updateCh: testutils.NewChannel()} } func (lw *listenerWatcher) ResourceChanged(update *xdsresource.ListenerUpdate, onDone func()) { lw.updateCh.Send(listenerUpdateErrTuple{update: update}) onDone() } func (lw *listenerWatcher) ResourceError(err error, onDone func()) { // When used with a go-control-plane management server that continuously // resends resources which are NACKed by the xDS client, using a `Replace()` // here and in AmbientError() simplifies tests which will have // access to the most recently received error. lw.updateCh.Replace(listenerUpdateErrTuple{err: err}) onDone() } func (lw *listenerWatcher) AmbientError(err error, onDone func()) { lw.updateCh.Replace(listenerUpdateErrTuple{err: err}) onDone() } type listenerWatcherMultiple struct { updateCh *testutils.Channel } // TODO: delete this once `newListenerWatcher` is modified to handle multiple // updates (https://github.com/grpc/grpc-go/issues/7864). func newListenerWatcherMultiple(size int) *listenerWatcherMultiple { return &listenerWatcherMultiple{updateCh: testutils.NewChannelWithSize(size)} } func (lw *listenerWatcherMultiple) ResourceChanged(update *xdsresource.ListenerUpdate, onDone func()) { lw.updateCh.Send(listenerUpdateErrTuple{update: update}) onDone() } func (lw *listenerWatcherMultiple) ResourceError(err error, onDone func()) { lw.updateCh.Send(listenerUpdateErrTuple{err: err}) onDone() } func (lw *listenerWatcherMultiple) AmbientError(err error, onDone func()) { lw.updateCh.Send(listenerUpdateErrTuple{err: err}) onDone() } // badListenerResource returns a listener resource for the given name which does // not contain the `RouteSpecifier` field in the HTTPConnectionManager, and // hence is expected to be NACKed by the client. func badListenerResource(t *testing.T, name string) *v3listenerpb.Listener { hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})}, }) return &v3listenerpb.Listener{ Name: name, ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, FilterChains: []*v3listenerpb.FilterChain{{ Name: "filter-chain-name", Filters: []*v3listenerpb.Filter{{ Name: wellknown.HTTPConnectionManager, ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, }}, }}, } } // verifyNoListenerUpdate verifies that no listener update is received on the // provided update channel, and returns an error if an update is received. // // A very short deadline is used while waiting for the update, as this function // is intended to be used when an update is not expected. func verifyNoListenerUpdate(ctx context.Context, updateCh *testutils.Channel) error { sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) defer sCancel() if u, err := updateCh.Receive(sCtx); err != context.DeadlineExceeded { return fmt.Errorf("unexpected ListenerUpdate: %v", u) } return nil } // verifyListenerUpdate waits for an update to be received on the provided // update channel and verifies that it matches the expected update. // // Returns an error if no update is received before the context deadline expires // or the received update does not match the expected one. func verifyListenerUpdate(ctx context.Context, updateCh *testutils.Channel, wantUpdate listenerUpdateErrTuple) error { u, err := updateCh.Receive(ctx) if err != nil { return fmt.Errorf("timeout when waiting for a listener resource from the management server: %v", err) } got := u.(listenerUpdateErrTuple) if wantUpdate.err != nil { if got.err == nil || !strings.Contains(got.err.Error(), wantUpdate.err.Error()) { return fmt.Errorf("update received with error: %v, want %q", got.err, wantUpdate.err) } } cmpOpts := []cmp.Option{ cmpopts.EquateEmpty(), cmpopts.IgnoreFields(xdsresource.HTTPFilter{}, "Filter", "Config"), cmpopts.IgnoreFields(xdsresource.ListenerUpdate{}, "Raw"), } if diff := cmp.Diff(wantUpdate.update, got.update, cmpOpts...); diff != "" { return fmt.Errorf("received unexpected diff in the listener resource update: (-want, got):\n%s", diff) } return nil } func verifyErrorType(ctx context.Context, updateCh *testutils.Channel, wantErrType xdsresource.ErrorType, wantNodeID string) error { u, err := updateCh.Receive(ctx) if err != nil { return fmt.Errorf("timeout when waiting for a listener error from the management server: %v", err) } gotErr := u.(listenerUpdateErrTuple).err if got, want := xdsresource.ErrType(gotErr), wantErrType; got != want { return fmt.Errorf("update received with error %v of type: %v, want %v", gotErr, got, want) } if !strings.Contains(gotErr.Error(), wantNodeID) { return fmt.Errorf("update received with error: %v, want error with node ID: %q", gotErr, wantNodeID) } return nil } // TestLDSWatch covers the case where a single watcher exists for a single // listener resource. The test verifies the following scenarios: // 1. An update from the management server containing the resource being // watched should result in the invocation of the watch callback. // 2. An update from the management server containing a resource *not* being // watched should not result in the invocation of the watch callback. // 3. After the watch is cancelled, an update from the management server // containing the resource that was being watched should not result in the // invocation of the watch callback. // // The test is run for old and new style names. func (s) TestLDSWatch(t *testing.T) { tests := []struct { desc string resourceName string watchedResource *v3listenerpb.Listener // The resource being watched. updatedWatchedResource *v3listenerpb.Listener // The watched resource after an update. notWatchedResource *v3listenerpb.Listener // A resource which is not being watched. wantUpdate listenerUpdateErrTuple }{ { desc: "old style resource", resourceName: ldsName, watchedResource: e2e.DefaultClientListener(ldsName, rdsName), updatedWatchedResource: e2e.DefaultClientListener(ldsName, "new-rds-resource"), notWatchedResource: e2e.DefaultClientListener("unsubscribed-lds-resource", rdsName), wantUpdate: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, }, { desc: "new style resource", resourceName: ldsNameNewStyle, watchedResource: e2e.DefaultClientListener(ldsNameNewStyle, rdsNameNewStyle), updatedWatchedResource: e2e.DefaultClientListener(ldsNameNewStyle, "new-rds-resource"), notWatchedResource: e2e.DefaultClientListener("unsubscribed-lds-resource", rdsNameNewStyle), wantUpdate: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsNameNewStyle, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp resource names used in this test do not specify an // authority. These will end up looking up an entry with the // empty key in the authorities map. Having an entry with an // empty key and empty configuration, results in these // resources also using the top-level configuration. "": []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register a watch for a listener resource and have the watch // callback push the received update on to a channel. lw := newListenerWatcher() ldsCancel := xdsresource.WatchListener(client, test.resourceName, lw) // Configure the management server to return a single listener // resource, corresponding to the one we registered a watch for. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{test.watchedResource}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update. if err := verifyListenerUpdate(ctx, lw.updateCh, test.wantUpdate); err != nil { t.Fatal(err) } // Configure the management server to return an additional listener // resource, one that we are not interested in. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{test.watchedResource, test.notWatchedResource}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } if err := verifyNoListenerUpdate(ctx, lw.updateCh); err != nil { t.Fatal(err) } // Cancel the watch and update the resource corresponding to the original // watch. Ensure that the cancelled watch callback is not invoked. ldsCancel() resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{test.updatedWatchedResource, test.notWatchedResource}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } if err := verifyNoListenerUpdate(ctx, lw.updateCh); err != nil { t.Fatal(err) } }) } } // TestLDSWatch_TwoWatchesForSameResourceName covers the case where two watchers // exist for a single listener resource. The test verifies the following // scenarios: // 1. An update from the management server containing the resource being // watched should result in the invocation of both watch callbacks. // 2. After one of the watches is cancelled, a redundant update from the // management server should not result in the invocation of either of the // watch callbacks. // 3. An update from the management server containing the resource being // watched should result in the invocation of the un-cancelled watch // callback. // // The test is run for old and new style names. func (s) TestLDSWatch_TwoWatchesForSameResourceName(t *testing.T) { tests := []struct { desc string resourceName string watchedResource *v3listenerpb.Listener // The resource being watched. updatedWatchedResource *v3listenerpb.Listener // The watched resource after an update. wantUpdateV1 listenerUpdateErrTuple wantUpdateV2 listenerUpdateErrTuple }{ { desc: "old style resource", resourceName: ldsName, watchedResource: e2e.DefaultClientListener(ldsName, rdsName), updatedWatchedResource: e2e.DefaultClientListener(ldsName, "new-rds-resource"), wantUpdateV1: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, wantUpdateV2: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: "new-rds-resource", HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, }, { desc: "new style resource", resourceName: ldsNameNewStyle, watchedResource: e2e.DefaultClientListener(ldsNameNewStyle, rdsNameNewStyle), updatedWatchedResource: e2e.DefaultClientListener(ldsNameNewStyle, "new-rds-resource"), wantUpdateV1: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsNameNewStyle, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, wantUpdateV2: listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: "new-rds-resource", HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp resource names used in this test do not specify an // authority. These will end up looking up an entry with the // empty key in the authorities map. Having an entry with an // empty key and empty configuration, results in these // resources also using the top-level configuration. "": []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register two watches for the same listener resource and have the // callbacks push the received updates on to a channel. lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, test.resourceName, lw1) defer ldsCancel1() lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, test.resourceName, lw2) // Configure the management server to return a single listener // resource, corresponding to the one we registered watches for. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{test.watchedResource}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update. if err := verifyListenerUpdate(ctx, lw1.updateCh, test.wantUpdateV1); err != nil { t.Fatal(err) } if err := verifyListenerUpdate(ctx, lw2.updateCh, test.wantUpdateV1); err != nil { t.Fatal(err) } // Cancel the second watch and force the management server to push a // redundant update for the resource being watched. Neither of the // two watch callbacks should be invoked. ldsCancel2() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } if err := verifyNoListenerUpdate(ctx, lw1.updateCh); err != nil { t.Fatal(err) } if err := verifyNoListenerUpdate(ctx, lw2.updateCh); err != nil { t.Fatal(err) } // Update to the resource being watched. The un-cancelled callback // should be invoked while the cancelled one should not be. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{test.updatedWatchedResource}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } if err := verifyListenerUpdate(ctx, lw1.updateCh, test.wantUpdateV2); err != nil { t.Fatal(err) } if err := verifyNoListenerUpdate(ctx, lw2.updateCh); err != nil { t.Fatal(err) } }) } } // TestLDSWatch_ThreeWatchesForDifferentResourceNames covers the case with three // watchers (two watchers for one resource, and the third watcher for another // resource), exist across two listener resources. The test verifies that an // update from the management server containing both resources results in the // invocation of all watch callbacks. // // The test is run with both old and new style names. func (s) TestLDSWatch_ThreeWatchesForDifferentResourceNames(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() authority := makeAuthorityName(t.Name()) bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp style resource names used in this test use a slash removed // version of t.Name as their authority, and the empty config // results in the top-level xds server configuration being used for // this authority. authority: []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register two watches for the same listener resource and have the // callbacks push the received updates on to a channel. lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, ldsName, lw1) defer ldsCancel1() lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, ldsName, lw2) defer ldsCancel2() // Register the third watch for a different listener resource. ldsNameNewStyle := makeNewStyleLDSName(authority) lw3 := newListenerWatcher() ldsCancel3 := xdsresource.WatchListener(client, ldsNameNewStyle, lw3) defer ldsCancel3() // Configure the management server to return two listener resources, // corresponding to the registered watches. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{ e2e.DefaultClientListener(ldsName, rdsName), e2e.DefaultClientListener(ldsNameNewStyle, rdsName), }, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update for the all watchers. The two // resources returned differ only in the resource name. Therefore the // expected update is the same for all the watchers. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate); err != nil { t.Fatal(err) } if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } if err := verifyListenerUpdate(ctx, lw3.updateCh, wantUpdate); err != nil { t.Fatal(err) } } // TestLDSWatch_ResourceCaching covers the case where a watch is registered for // a resource which is already present in the cache. The test verifies that the // watch callback is invoked with the contents from the cache, instead of a // request being sent to the management server. func (s) TestLDSWatch_ResourceCaching(t *testing.T) { firstRequestReceived := false firstAckReceived := grpcsync.NewEvent() secondRequestReceived := grpcsync.NewEvent() mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { // The first request has an empty version string. if !firstRequestReceived && req.GetVersionInfo() == "" { firstRequestReceived = true return nil } // The first ack has a non-empty version string. if !firstAckReceived.HasFired() && req.GetVersionInfo() != "" { firstAckReceived.Fire() return nil } // Any requests after the first request and ack, are not expected. secondRequestReceived.Fire() return nil }, }) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register a watch for a listener resource and have the watch // callback push the received update on to a channel. lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, ldsName, lw1) defer ldsCancel1() // Configure the management server to return a single listener // resource, corresponding to the one we registered a watch for. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate); err != nil { t.Fatal(err) } select { case <-ctx.Done(): t.Fatal("timeout when waiting for receipt of ACK at the management server") case <-firstAckReceived.Done(): } // Register another watch for the same resource. This should get the update // from the cache. lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, ldsName, lw2) defer ldsCancel2() if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } // No request should get sent out as part of this watch. sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) defer sCancel() select { case <-sCtx.Done(): case <-secondRequestReceived.Done(): t.Fatal("xdsClient sent out request instead of using update from cache") } } // TestLDSWatch_ExpiryTimerFiresBeforeResponse tests the case where the client // does not receive an LDS response for the request that it sends. The test // verifies that the watch callback is invoked with an error once the // watchExpiryTimer fires. func (s) TestLDSWatch_ExpiryTimerFiresBeforeResponse(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client talking to the above management server. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), WatchExpiryTimeout: defaultTestWatchExpiryTimeout, }) if err != nil { t.Fatalf("Failed to create an xDS client: %v", err) } defer close() // Register a watch for a resource which is expected to fail with an error // after the watch expiry timer fires. lw := newListenerWatcher() ldsCancel := xdsresource.WatchListener(client, ldsName, lw) defer ldsCancel() // Wait for the watch expiry timer to fire. <-time.After(defaultTestWatchExpiryTimeout) // Verify that an empty update with the expected error is received. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() wantErr := xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, "") if err := verifyListenerUpdate(ctx, lw.updateCh, listenerUpdateErrTuple{err: wantErr}); err != nil { t.Fatal(err) } } // TestLDSWatch_ValidResponseCancelsExpiryTimerBehavior tests the case where the // client receives a valid LDS response for the request that it sends. The test // verifies that the behavior associated with the expiry timer (i.e, callback // invocation with error) does not take place. func (s) TestLDSWatch_ValidResponseCancelsExpiryTimerBehavior(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client talking to the above management server. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), WatchExpiryTimeout: defaultTestWatchExpiryTimeout, }) if err != nil { t.Fatalf("Failed to create an xDS client: %v", err) } defer close() // Register a watch for a listener resource and have the watch // callback push the received update on to a channel. lw := newListenerWatcher() ldsCancel := xdsresource.WatchListener(client, ldsName, lw) defer ldsCancel() // Configure the management server to return a single listener // resource, corresponding to the one we registered a watch for. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw.updateCh, wantUpdate); err != nil { t.Fatal(err) } // Wait for the watch expiry timer to fire, and verify that the callback is // not invoked. <-time.After(defaultTestWatchExpiryTimeout) if err := verifyNoListenerUpdate(ctx, lw.updateCh); err != nil { t.Fatal(err) } } // TestLDSWatch_ResourceRemoved covers the cases where a resource being watched // is removed from the management server. The test verifies the following // scenarios: // 1. Removing a resource should trigger the watch callback with a resource // removed error. It should not trigger the watch callback for an unrelated // resource. // 2. An update to another resource should result in the invocation of the watch // callback associated with that resource. It should not result in the // invocation of the watch callback associated with the deleted resource. // // The test is run with both old and new style names. func (s) TestLDSWatch_ResourceRemoved(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() authority := makeAuthorityName(t.Name()) bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp style resource names used in this test use a slash removed // version of t.Name as their authority, and the empty config // results in the top-level xds server configuration being used for // this authority. authority: []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register two watches for two listener resources and have the // callbacks push the received updates on to a channel. resourceName1 := ldsName lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, resourceName1, lw1) defer ldsCancel1() resourceName2 := makeNewStyleLDSName(authority) lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, resourceName2, lw2) defer ldsCancel2() // Configure the management server to return two listener resources, // corresponding to the registered watches. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{ e2e.DefaultClientListener(resourceName1, rdsName), e2e.DefaultClientListener(resourceName2, rdsName), }, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update for both watchers. The two // resources returned differ only in the resource name. Therefore the // expected update is the same for both watchers. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate); err != nil { t.Fatal(err) } if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } // Remove the first listener resource on the management server. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(resourceName2, rdsName)}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // The first watcher should receive a resource removed error, while the // second watcher should not see an update. if err := verifyListenerUpdate(ctx, lw1.updateCh, listenerUpdateErrTuple{ err: xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, ""), }); err != nil { t.Fatal(err) } if err := verifyNoListenerUpdate(ctx, lw2.updateCh); err != nil { t.Fatal(err) } // Update the second listener resource on the management server. The first // watcher should not see an update, while the second watcher should. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(resourceName2, "new-rds-resource")}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } if err := verifyNoListenerUpdate(ctx, lw1.updateCh); err != nil { t.Fatal(err) } wantUpdate = listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: "new-rds-resource", HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } } // TestLDSWatch_NewWatcherForRemovedResource covers the case where a new // watcher registers for a resource that has been removed. The test verifies // the following scenarios: // 1. When a resource is deleted by the management server, any active // watchers of that resource should be notified with a "resource removed" // error through their watch callback. // 2. If a new watcher attempts to register for a resource that has already // been deleted, its watch callback should be immediately invoked with a // "resource removed" error. func (s) TestLDSWatch_NewWatcherForRemovedResource(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register watch for the listener resource and have the // callbacks push the received updates on to a channel. lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, ldsName, lw1) defer ldsCancel1() // Configure the management server to return listener resource, // corresponding to the registered watch. resource := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resource); err != nil { t.Fatalf("Failed to update management server with resource: %v, err: %v", resource, err) } // Verify the contents of the received update for existing watch. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate); err != nil { t.Fatal(err) } // Remove the listener resource on the management server. resource = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resource); err != nil { t.Fatalf("Failed to update management server with resource: %v, err: %v", resource, err) } // The existing watcher should receive a resource removed error. updateError := listenerUpdateErrTuple{err: xdsresource.NewError(xdsresource.ErrorTypeResourceNotFound, "")} if err := verifyListenerUpdate(ctx, lw1.updateCh, updateError); err != nil { t.Fatal(err) } // New watchers attempting to register for a deleted resource should also // receive a "resource removed" error. lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, ldsName, lw2) defer ldsCancel2() if err := verifyListenerUpdate(ctx, lw2.updateCh, updateError); err != nil { t.Fatal(err) } } // TestLDSWatch_NACKError covers the case where an update from the management // server is NACKed by the xdsclient. The test verifies that the error is // propagated to the existing watcher. After NACK, if a new watcher registers // for the resource, error is propagated to the new watcher as well. func (s) TestLDSWatch_NACKError(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register a watch for a listener resource and have the watch // callback push the received update on to a channel. lw := newListenerWatcher() ldsCancel := xdsresource.WatchListener(client, ldsName, lw) defer ldsCancel() // Configure the management server to return a single listener resource // which is expected to be NACKed by the client. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{badListenerResource(t, ldsName)}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify that the expected error is propagated to the existing watcher. if err := verifyErrorType(ctx, lw.updateCh, xdsresource.ErrorTypeUnknown, nodeID); err != nil { t.Fatal(err) } // Verify that the expected error is propagated to the new watcher as well. lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, ldsName, lw2) defer ldsCancel2() if err := verifyErrorType(ctx, lw2.updateCh, xdsresource.ErrorTypeUnknown, nodeID); err != nil { t.Fatal(err) } } // Tests the scenario where a watch registered for a resource results in a good // update followed by a bad update. This results in the resource cache // containing both the old good update and the latest NACK error. The test // verifies that a when a new watch is registered for the same resource, the new // watcher receives the good update followed by the NACK error. func (s) TestLDSWatch_ResourceCaching_NACKError(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() bc := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register a watch for a listener resource and have the watch // callback push the received update on to a channel. lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, ldsName, lw1) defer ldsCancel1() // Configure the management server to return a single listener // resource, corresponding to the one we registered a watch for. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), 1000*defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate); err != nil { t.Fatal(err) } // Configure the management server to return a single listener resource // which is expected to be NACKed by the client. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{badListenerResource(t, ldsName)}, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify that the expected error is propagated to the existing watcher. if err := verifyErrorType(ctx, lw1.updateCh, xdsresource.ErrorTypeUnknown, nodeID); err != nil { t.Fatal(err) } // Register another watch for the same resource. This should get the update // and error from the cache. lw2 := newListenerWatcherMultiple(2) ldsCancel2 := xdsresource.WatchListener(client, ldsName, lw2) defer ldsCancel2() if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } // Verify that the expected error is propagated to the existing watcher. if err := verifyErrorType(ctx, lw2.updateCh, xdsresource.ErrorTypeUnknown, nodeID); err != nil { t.Fatal(err) } } // TestLDSWatch_PartialValid covers the case where a response from the // management server contains both valid and invalid resources and is expected // to be NACKed by the xdsclient. The test verifies that watchers corresponding // to the valid resource receive the update, while watchers corresponding to the // invalid resource receive an error. func (s) TestLDSWatch_PartialValid(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() authority := makeAuthorityName(t.Name()) bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp style resource names used in this test use a slash removed // version of t.Name as their authority, and the empty config // results in the top-level xds server configuration being used for // this authority. authority: []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register two watches for listener resources. The first watch is expected // to receive an error because the received resource is NACKed. The second // watch is expected to get a good update. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() badResourceName := ldsName lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, badResourceName, lw1) defer ldsCancel1() goodResourceName := makeNewStyleLDSName(authority) lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, goodResourceName, lw2) defer ldsCancel2() // Configure the management server with two listener resources. One of these // is a bad resource causing the update to be NACKed. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{ badListenerResource(t, badResourceName), e2e.DefaultClientListener(goodResourceName, rdsName), }, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify that the expected error is propagated to the watcher which // requested for the bad resource. // Verify that the expected error is propagated to the existing watcher. if err := verifyErrorType(ctx, lw1.updateCh, xdsresource.ErrorTypeUnknown, nodeID); err != nil { t.Fatal(err) } // Verify that the watcher watching the good resource receives a good // update. wantUpdate := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate); err != nil { t.Fatal(err) } } // TestLDSWatch_PartialResponse covers the case where a response from the // management server does not contain all requested resources. LDS responses are // supposed to contain all requested resources, and the absence of one usually // indicates that the management server does not know about it. In cases where // the server has never responded with this resource before, the xDS client is // expected to wait for the watch timeout to expire before concluding that the // resource does not exist on the server func (s) TestLDSWatch_PartialResponse(t *testing.T) { mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) nodeID := uuid.New().String() authority := makeAuthorityName(t.Name()) bc, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ Servers: []byte(fmt.Sprintf(`[{ "server_uri": %q, "channel_creds": [{"type": "insecure"}] }]`, mgmtServer.Address)), Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), Authorities: map[string]json.RawMessage{ // Xdstp style resource names used in this test use a slash removed // version of t.Name as their authority, and the empty config // results in the top-level xds server configuration being used for // this authority. authority: []byte(`{}`), }, }) if err != nil { t.Fatalf("Failed to create bootstrap configuration: %v", err) } // Create an xDS client with the above bootstrap contents. config, err := bootstrap.NewConfigFromContents(bc) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bc), err) } pool := xdsclient.NewPool(config) client, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), }) if err != nil { t.Fatalf("Failed to create xDS client: %v", err) } defer close() // Register two watches for two listener resources and have the // callbacks push the received updates on to a channel. resourceName1 := ldsName lw1 := newListenerWatcher() ldsCancel1 := xdsresource.WatchListener(client, resourceName1, lw1) defer ldsCancel1() resourceName2 := makeNewStyleLDSName(authority) lw2 := newListenerWatcher() ldsCancel2 := xdsresource.WatchListener(client, resourceName2, lw2) defer ldsCancel2() // Configure the management server to return only one of the two listener // resources, corresponding to the registered watches. resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{ e2e.DefaultClientListener(resourceName1, rdsName), }, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update for first watcher. wantUpdate1 := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw1.updateCh, wantUpdate1); err != nil { t.Fatal(err) } // Verify that the second watcher does not get an update with an error. if err := verifyNoListenerUpdate(ctx, lw2.updateCh); err != nil { t.Fatal(err) } // Configure the management server to return two listener resources, // corresponding to the registered watches. resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{ e2e.DefaultClientListener(resourceName1, rdsName), e2e.DefaultClientListener(resourceName2, rdsName), }, SkipValidation: true, } if err := mgmtServer.Update(ctx, resources); err != nil { t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) } // Verify the contents of the received update for the second watcher. wantUpdate2 := listenerUpdateErrTuple{ update: &xdsresource.ListenerUpdate{ APIListener: &xdsresource.HTTPConnectionManagerConfig{ RouteConfigName: rdsName, HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, }, }, } if err := verifyListenerUpdate(ctx, lw2.updateCh, wantUpdate2); err != nil { t.Fatal(err) } // Verify that the first watcher gets no update, as the first resource did // not change. if err := verifyNoListenerUpdate(ctx, lw1.updateCh); err != nil { t.Fatal(err) } }