google.golang.org/grpc@v1.72.2/xds/internal/xdsclient/channel_test.go (about) 1 /* 2 * 3 * Copyright 2024 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package xdsclient 19 20 import ( 21 "context" 22 "fmt" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/envoyproxy/go-control-plane/pkg/wellknown" 28 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 30 "github.com/google/uuid" 31 "google.golang.org/grpc/internal/testutils" 32 "google.golang.org/grpc/internal/testutils/xds/e2e" 33 "google.golang.org/grpc/internal/testutils/xds/fakeserver" 34 "google.golang.org/grpc/internal/xds/bootstrap" 35 xdsinternal "google.golang.org/grpc/xds/internal" 36 "google.golang.org/grpc/xds/internal/httpfilter" 37 "google.golang.org/grpc/xds/internal/httpfilter/router" 38 "google.golang.org/grpc/xds/internal/xdsclient/transport" 39 "google.golang.org/grpc/xds/internal/xdsclient/transport/ads" 40 "google.golang.org/grpc/xds/internal/xdsclient/transport/grpctransport" 41 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 42 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 43 "google.golang.org/protobuf/testing/protocmp" 44 "google.golang.org/protobuf/types/known/anypb" 45 "google.golang.org/protobuf/types/known/durationpb" 46 47 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 48 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 49 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 50 v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" 51 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 52 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 53 ) 54 55 // Lookup the listener resource type from the resource type map. This is used to 56 // parse listener resources used in this test. 57 var listenerType = xdsinternal.ResourceTypeMapForTesting[version.V3ListenerURL].(xdsresource.Type) 58 59 // xdsChannelForTest creates an xdsChannel to the specified serverURI for 60 // testing purposes. 61 func xdsChannelForTest(t *testing.T, serverURI, nodeID string, watchExpiryTimeout time.Duration) *xdsChannel { 62 t.Helper() 63 64 // Create server configuration for the above management server. 65 serverCfg, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{URI: serverURI}) 66 if err != nil { 67 t.Fatalf("Failed to create server config for testing: %v", err) 68 } 69 70 // Create a grpc transport to the above management server. 71 tr, err := (&grpctransport.Builder{}).Build(transport.BuildOptions{ServerConfig: serverCfg}) 72 if err != nil { 73 t.Fatalf("Failed to create a transport for server config %s: %v", serverCfg, err) 74 } 75 76 // Create bootstrap configuration with the top-level xds servers 77 // field containing the server configuration for the above 78 // management server. 79 contents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{ 80 Servers: []byte(fmt.Sprintf(`[{ 81 "server_uri": %q, 82 "channel_creds": [{"type": "insecure"}] 83 }]`, serverURI)), 84 Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)), 85 }) 86 if err != nil { 87 t.Fatalf("Failed to create bootstrap contents: %v", err) 88 } 89 bootstrapCfg, err := bootstrap.NewConfigFromContents(contents) 90 if err != nil { 91 t.Fatalf("Failed to create bootstrap configuration: %v", err) 92 } 93 94 // Create an xdsChannel that uses everything set up above. 95 xc, err := newXDSChannel(xdsChannelOpts{ 96 transport: tr, 97 serverConfig: serverCfg, 98 bootstrapConfig: bootstrapCfg, 99 resourceTypeGetter: func(typeURL string) xdsresource.Type { 100 if typeURL != "type.googleapis.com/envoy.config.listener.v3.Listener" { 101 return nil 102 } 103 return listenerType 104 }, 105 eventHandler: newTestEventHandler(), 106 watchExpiryTimeout: watchExpiryTimeout, 107 }) 108 if err != nil { 109 t.Fatalf("Failed to create xdsChannel: %v", err) 110 } 111 t.Cleanup(func() { xc.close() }) 112 return xc 113 } 114 115 // verifyUpdateAndMetadata verifies that the event handler received the expected 116 // updates and metadata. It checks that the received resource type matches the 117 // expected type, and that the received updates and metadata match the expected 118 // values. The function ignores the timestamp fields in the metadata, as those 119 // are expected to be different. 120 func verifyUpdateAndMetadata(ctx context.Context, t *testing.T, eh *testEventHandler, wantUpdates map[string]ads.DataAndErrTuple, wantMD xdsresource.UpdateMetadata) { 121 t.Helper() 122 123 gotTyp, gotUpdates, gotMD, err := eh.waitForUpdate(ctx) 124 if err != nil { 125 t.Fatalf("Timeout when waiting for update callback to be invoked on the event handler") 126 } 127 128 if gotTyp != listenerType { 129 t.Fatalf("Got resource type %v, want %v", gotTyp, listenerType) 130 } 131 opts := cmp.Options{ 132 protocmp.Transform(), 133 cmpopts.EquateEmpty(), 134 cmpopts.EquateErrors(), 135 cmpopts.IgnoreFields(xdsresource.UpdateMetadata{}, "Timestamp"), 136 cmpopts.IgnoreFields(xdsresource.UpdateErrorMetadata{}, "Timestamp"), 137 } 138 if diff := cmp.Diff(wantUpdates, gotUpdates, opts); diff != "" { 139 t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %+v\n got: %+v", diff, wantUpdates, gotUpdates) 140 } 141 if diff := cmp.Diff(wantMD, gotMD, opts); diff != "" { 142 t.Fatalf("Got unexpected diff in update (-want +got):\n%s\n want: %v\n got: %v", diff, wantMD, gotMD) 143 } 144 } 145 146 // Tests different failure cases when creating a new xdsChannel. It checks that 147 // the xdsChannel creation fails when any of the required options (transport, 148 // serverConfig, bootstrapConfig, or resourceTypeGetter) are missing or nil. 149 func (s) TestChannel_New_FailureCases(t *testing.T) { 150 type fakeTransport struct { 151 transport.Transport 152 } 153 154 tests := []struct { 155 name string 156 opts xdsChannelOpts 157 wantErrStr string 158 }{ 159 { 160 name: "emptyTransport", 161 opts: xdsChannelOpts{}, 162 wantErrStr: "transport is nil", 163 }, 164 { 165 name: "emptyServerConfig", 166 opts: xdsChannelOpts{transport: &fakeTransport{}}, 167 wantErrStr: "serverConfig is nil", 168 }, 169 { 170 name: "emptyBootstrapConfig", 171 opts: xdsChannelOpts{ 172 transport: &fakeTransport{}, 173 serverConfig: &bootstrap.ServerConfig{}, 174 }, 175 wantErrStr: "bootstrapConfig is nil", 176 }, 177 { 178 name: "emptyResourceTypeGetter", 179 opts: xdsChannelOpts{ 180 transport: &fakeTransport{}, 181 serverConfig: &bootstrap.ServerConfig{}, 182 bootstrapConfig: &bootstrap.Config{}, 183 }, 184 wantErrStr: "resourceTypeGetter is nil", 185 }, 186 { 187 name: "emptyEventHandler", 188 opts: xdsChannelOpts{ 189 transport: &fakeTransport{}, 190 serverConfig: &bootstrap.ServerConfig{}, 191 bootstrapConfig: &bootstrap.Config{}, 192 resourceTypeGetter: func(string) xdsresource.Type { return nil }, 193 }, 194 wantErrStr: "eventHandler is nil", 195 }, 196 } 197 198 for _, test := range tests { 199 t.Run(test.name, func(t *testing.T) { 200 if _, err := newXDSChannel(test.opts); err == nil || !strings.Contains(err.Error(), test.wantErrStr) { 201 t.Fatalf("newXDSChannel() = %v, want %q", err, test.wantErrStr) 202 } 203 }) 204 } 205 } 206 207 // Tests different scenarios of the xdsChannel receiving a response from the 208 // management server. In all scenarios, the xdsChannel is expected to pass the 209 // received responses as-is to the resource parsing functionality specified by 210 // the resourceTypeGetter. 211 func (s) TestChannel_ADS_HandleResponseFromManagementServer(t *testing.T) { 212 const ( 213 listenerName1 = "listener-name-1" 214 listenerName2 = "listener-name-2" 215 routeName = "route-name" 216 clusterName = "cluster-name" 217 ) 218 var ( 219 badlyMarshaledResource = &anypb.Any{ 220 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 221 Value: []byte{1, 2, 3, 4}, 222 } 223 apiListener = &v3listenerpb.ApiListener{ 224 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 225 RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{ 226 RouteConfig: &v3routepb.RouteConfiguration{ 227 Name: routeName, 228 VirtualHosts: []*v3routepb.VirtualHost{{ 229 Domains: []string{"*"}, 230 Routes: []*v3routepb.Route{{ 231 Match: &v3routepb.RouteMatch{ 232 PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}, 233 }, 234 Action: &v3routepb.Route_Route{ 235 Route: &v3routepb.RouteAction{ 236 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}, 237 }}}}}}}, 238 }, 239 HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter}, 240 CommonHttpProtocolOptions: &v3corepb.HttpProtocolOptions{ 241 MaxStreamDuration: durationpb.New(time.Second), 242 }, 243 }), 244 } 245 listener1 = testutils.MarshalAny(t, &v3listenerpb.Listener{ 246 Name: listenerName1, 247 ApiListener: apiListener, 248 }) 249 listener2 = testutils.MarshalAny(t, &v3listenerpb.Listener{ 250 Name: listenerName2, 251 ApiListener: apiListener, 252 }) 253 ) 254 255 tests := []struct { 256 desc string 257 resourceNamesToRequest []string 258 managementServerResponse *v3discoverypb.DiscoveryResponse 259 wantUpdates map[string]ads.DataAndErrTuple 260 wantMD xdsresource.UpdateMetadata 261 wantErr error 262 }{ 263 { 264 desc: "one bad resource - deserialization failure", 265 resourceNamesToRequest: []string{listenerName1}, 266 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 267 VersionInfo: "0", 268 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 269 Resources: []*anypb.Any{badlyMarshaledResource}, 270 }, 271 wantUpdates: nil, // No updates expected as the response runs into unmarshaling errors. 272 wantMD: xdsresource.UpdateMetadata{ 273 Status: xdsresource.ServiceStatusNACKed, 274 Version: "0", 275 ErrState: &xdsresource.UpdateErrorMetadata{ 276 Version: "0", 277 Err: cmpopts.AnyError, 278 }, 279 }, 280 wantErr: cmpopts.AnyError, 281 }, 282 { 283 desc: "one bad resource - validation failure", 284 resourceNamesToRequest: []string{listenerName1}, 285 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 286 VersionInfo: "0", 287 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 288 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{ 289 Name: listenerName1, 290 ApiListener: &v3listenerpb.ApiListener{ 291 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 292 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 293 }), 294 }, 295 })}, 296 }, 297 wantUpdates: map[string]ads.DataAndErrTuple{ 298 listenerName1: { 299 Err: cmpopts.AnyError, 300 }, 301 }, 302 wantMD: xdsresource.UpdateMetadata{ 303 Status: xdsresource.ServiceStatusNACKed, 304 Version: "0", 305 ErrState: &xdsresource.UpdateErrorMetadata{ 306 Version: "0", 307 Err: cmpopts.AnyError, 308 }, 309 }, 310 }, 311 { 312 desc: "two bad resources", 313 resourceNamesToRequest: []string{listenerName1, listenerName2}, 314 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 315 VersionInfo: "0", 316 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 317 Resources: []*anypb.Any{ 318 badlyMarshaledResource, 319 testutils.MarshalAny(t, &v3listenerpb.Listener{ 320 Name: listenerName2, 321 ApiListener: &v3listenerpb.ApiListener{ 322 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 323 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 324 }), 325 }, 326 }), 327 }, 328 }, 329 wantUpdates: map[string]ads.DataAndErrTuple{ 330 listenerName2: { 331 Err: cmpopts.AnyError, 332 }, 333 }, 334 wantMD: xdsresource.UpdateMetadata{ 335 Status: xdsresource.ServiceStatusNACKed, 336 Version: "0", 337 ErrState: &xdsresource.UpdateErrorMetadata{ 338 Version: "0", 339 Err: cmpopts.AnyError, 340 }, 341 }, 342 }, 343 { 344 desc: "one good resource", 345 resourceNamesToRequest: []string{listenerName1}, 346 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 347 VersionInfo: "0", 348 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 349 Resources: []*anypb.Any{listener1}, 350 }, 351 wantUpdates: map[string]ads.DataAndErrTuple{ 352 listenerName1: { 353 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 354 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 355 VirtualHosts: []*xdsresource.VirtualHost{{ 356 Domains: []string{"*"}, 357 Routes: []*xdsresource.Route{{ 358 Prefix: newStringP("/"), 359 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 360 ActionType: xdsresource.RouteActionRoute}, 361 }, 362 }}}, 363 MaxStreamDuration: time.Second, 364 Raw: listener1, 365 HTTPFilters: makeRouterFilterList(t), 366 }}, 367 }, 368 }, 369 wantMD: xdsresource.UpdateMetadata{ 370 Status: xdsresource.ServiceStatusACKed, 371 Version: "0", 372 }, 373 }, 374 { 375 desc: "one good and one bad - deserialization failure", 376 resourceNamesToRequest: []string{listenerName1, listenerName2}, 377 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 378 VersionInfo: "0", 379 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 380 Resources: []*anypb.Any{ 381 badlyMarshaledResource, 382 listener2, 383 }, 384 }, 385 wantUpdates: map[string]ads.DataAndErrTuple{ 386 listenerName2: { 387 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 388 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 389 VirtualHosts: []*xdsresource.VirtualHost{{ 390 Domains: []string{"*"}, 391 Routes: []*xdsresource.Route{{ 392 Prefix: newStringP("/"), 393 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 394 ActionType: xdsresource.RouteActionRoute}, 395 }, 396 }}}, 397 MaxStreamDuration: time.Second, 398 Raw: listener2, 399 HTTPFilters: makeRouterFilterList(t), 400 }}, 401 }, 402 }, 403 wantMD: xdsresource.UpdateMetadata{ 404 Status: xdsresource.ServiceStatusNACKed, 405 Version: "0", 406 ErrState: &xdsresource.UpdateErrorMetadata{ 407 Version: "0", 408 Err: cmpopts.AnyError, 409 }, 410 }, 411 }, 412 { 413 desc: "one good and one bad - validation failure", 414 resourceNamesToRequest: []string{listenerName1, listenerName2}, 415 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 416 VersionInfo: "0", 417 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 418 Resources: []*anypb.Any{ 419 testutils.MarshalAny(t, &v3listenerpb.Listener{ 420 Name: listenerName1, 421 ApiListener: &v3listenerpb.ApiListener{ 422 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 423 RouteSpecifier: &v3httppb.HttpConnectionManager_ScopedRoutes{}, 424 }), 425 }, 426 }), 427 listener2, 428 }, 429 }, 430 wantUpdates: map[string]ads.DataAndErrTuple{ 431 listenerName1: {Err: cmpopts.AnyError}, 432 listenerName2: { 433 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 434 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 435 VirtualHosts: []*xdsresource.VirtualHost{{ 436 Domains: []string{"*"}, 437 Routes: []*xdsresource.Route{{ 438 Prefix: newStringP("/"), 439 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 440 ActionType: xdsresource.RouteActionRoute}, 441 }, 442 }}}, 443 MaxStreamDuration: time.Second, 444 Raw: listener2, 445 HTTPFilters: makeRouterFilterList(t), 446 }}, 447 }, 448 }, 449 wantMD: xdsresource.UpdateMetadata{ 450 Status: xdsresource.ServiceStatusNACKed, 451 Version: "0", 452 ErrState: &xdsresource.UpdateErrorMetadata{ 453 Version: "0", 454 Err: cmpopts.AnyError, 455 }, 456 }, 457 }, 458 { 459 desc: "two good resources", 460 resourceNamesToRequest: []string{listenerName1, listenerName2}, 461 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 462 VersionInfo: "0", 463 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 464 Resources: []*anypb.Any{listener1, listener2}, 465 }, 466 wantUpdates: map[string]ads.DataAndErrTuple{ 467 listenerName1: { 468 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 469 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 470 VirtualHosts: []*xdsresource.VirtualHost{{ 471 Domains: []string{"*"}, 472 Routes: []*xdsresource.Route{{ 473 Prefix: newStringP("/"), 474 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 475 ActionType: xdsresource.RouteActionRoute}, 476 }, 477 }}}, 478 MaxStreamDuration: time.Second, 479 Raw: listener1, 480 HTTPFilters: makeRouterFilterList(t), 481 }}, 482 }, 483 listenerName2: { 484 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 485 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 486 VirtualHosts: []*xdsresource.VirtualHost{{ 487 Domains: []string{"*"}, 488 Routes: []*xdsresource.Route{{ 489 Prefix: newStringP("/"), 490 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 491 ActionType: xdsresource.RouteActionRoute}, 492 }, 493 }}}, 494 MaxStreamDuration: time.Second, 495 Raw: listener2, 496 HTTPFilters: makeRouterFilterList(t), 497 }}, 498 }, 499 }, 500 wantMD: xdsresource.UpdateMetadata{ 501 Status: xdsresource.ServiceStatusACKed, 502 Version: "0", 503 }, 504 }, 505 { 506 desc: "two resources when we requested one", 507 resourceNamesToRequest: []string{listenerName1}, 508 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 509 VersionInfo: "0", 510 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 511 Resources: []*anypb.Any{listener1, listener2}, 512 }, 513 wantUpdates: map[string]ads.DataAndErrTuple{ 514 listenerName1: { 515 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 516 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 517 VirtualHosts: []*xdsresource.VirtualHost{{ 518 Domains: []string{"*"}, 519 Routes: []*xdsresource.Route{{ 520 Prefix: newStringP("/"), 521 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 522 ActionType: xdsresource.RouteActionRoute}, 523 }, 524 }}}, 525 MaxStreamDuration: time.Second, 526 Raw: listener1, 527 HTTPFilters: makeRouterFilterList(t), 528 }}, 529 }, 530 listenerName2: { 531 Resource: &xdsresource.ListenerResourceData{Resource: xdsresource.ListenerUpdate{ 532 InlineRouteConfig: &xdsresource.RouteConfigUpdate{ 533 VirtualHosts: []*xdsresource.VirtualHost{{ 534 Domains: []string{"*"}, 535 Routes: []*xdsresource.Route{{ 536 Prefix: newStringP("/"), 537 WeightedClusters: map[string]xdsresource.WeightedCluster{clusterName: {Weight: 1}}, 538 ActionType: xdsresource.RouteActionRoute}, 539 }, 540 }}}, 541 MaxStreamDuration: time.Second, 542 Raw: listener2, 543 HTTPFilters: makeRouterFilterList(t), 544 }}, 545 }, 546 }, 547 wantMD: xdsresource.UpdateMetadata{ 548 Status: xdsresource.ServiceStatusACKed, 549 Version: "0", 550 }, 551 }, 552 } 553 554 for _, test := range tests { 555 t.Run(test.desc, func(t *testing.T) { 556 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 557 defer cancel() 558 559 // Start a fake xDS management server and configure the response it 560 // would send to its client. 561 mgmtServer, cleanup, err := fakeserver.StartServer(nil) 562 if err != nil { 563 t.Fatalf("Failed to start fake xDS server: %v", err) 564 } 565 defer cleanup() 566 t.Logf("Started xDS management server on %s", mgmtServer.Address) 567 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 568 569 // Create an xdsChannel for the test with a long watch expiry timer 570 // to ensure that watches don't expire for the duration of the test. 571 nodeID := uuid.New().String() 572 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout) 573 defer xc.close() 574 575 // Subscribe to the resources specified in the test table. 576 for _, name := range test.resourceNamesToRequest { 577 xc.subscribe(listenerType, name) 578 } 579 580 // Wait for an update callback on the event handler and verify the 581 // contents of the update and the metadata. 582 verifyUpdateAndMetadata(ctx, t, xc.eventHandler.(*testEventHandler), test.wantUpdates, test.wantMD) 583 }) 584 } 585 } 586 587 // Tests that the xdsChannel correctly handles the expiry of a watch for a 588 // resource by ensuring that the watch expiry callback is invoked on the event 589 // handler with the expected resource type and name. 590 func (s) TestChannel_ADS_HandleResponseWatchExpiry(t *testing.T) { 591 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 592 defer cancel() 593 594 // Start an xDS management server, but do not configure any resources on it. 595 // This will result in the watch for a resource to timeout. 596 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 597 598 // Create an xdsChannel for the test with a short watch expiry timer to 599 // ensure that the test does not run very long, as it needs to wait for the 600 // watch to expire. 601 nodeID := uuid.New().String() 602 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestShortTimeout) 603 defer xc.close() 604 605 // Subscribe to a listener resource. 606 const listenerName = "listener-name" 607 xc.subscribe(listenerType, listenerName) 608 609 // Wait for the watch expiry callback on the authority to be invoked and 610 // verify that the watch expired for the expected resource name and type. 611 eventHandler := xc.eventHandler.(*testEventHandler) 612 gotTyp, gotName, err := eventHandler.waitForResourceDoesNotExist(ctx) 613 if err != nil { 614 t.Fatal("Timeout when waiting for the watch expiry callback to be invoked on the xDS client") 615 } 616 617 if gotTyp != listenerType { 618 t.Fatalf("Got type %v, want %v", gotTyp, listenerType) 619 } 620 if gotName != listenerName { 621 t.Fatalf("Got name %v, want %v", gotName, listenerName) 622 } 623 } 624 625 // Tests that the xdsChannel correctly handles stream failures by ensuring that 626 // the stream failure callback is invoked on the event handler. 627 func (s) TestChannel_ADS_StreamFailure(t *testing.T) { 628 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 629 defer cancel() 630 631 // Start an xDS management server with a restartable listener to simulate 632 // connection failures. 633 l, err := testutils.LocalTCPListener() 634 if err != nil { 635 t.Fatalf("net.Listen() failed: %v", err) 636 } 637 lis := testutils.NewRestartableListener(l) 638 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{Listener: lis}) 639 640 // Configure a listener resource on the management server. 641 const listenerResourceName = "test-listener-resource" 642 const routeConfigurationName = "test-route-configuration-resource" 643 nodeID := uuid.New().String() 644 resources := e2e.UpdateOptions{ 645 NodeID: nodeID, 646 Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(listenerResourceName, routeConfigurationName)}, 647 SkipValidation: true, 648 } 649 if err := mgmtServer.Update(ctx, resources); err != nil { 650 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 651 } 652 653 // Create an xdsChannel for the test with a long watch expiry timer 654 // to ensure that watches don't expire for the duration of the test. 655 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout) 656 defer xc.close() 657 658 // Subscribe to the resource created above. 659 xc.subscribe(listenerType, listenerResourceName) 660 661 // Wait for an update callback on the event handler and verify the 662 // contents of the update and the metadata. 663 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 664 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{ 665 ConfigSource: &v3corepb.ConfigSource{ 666 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 667 }, 668 RouteConfigName: routeConfigurationName, 669 }}, 670 HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})}, 671 }) 672 listenerResource, err := anypb.New(&v3listenerpb.Listener{ 673 Name: listenerResourceName, 674 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 675 FilterChains: []*v3listenerpb.FilterChain{{ 676 Name: "filter-chain-name", 677 Filters: []*v3listenerpb.Filter{{ 678 Name: wellknown.HTTPConnectionManager, 679 ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm}, 680 }}, 681 }}, 682 }) 683 if err != nil { 684 t.Fatalf("Failed to create listener resource: %v", err) 685 } 686 687 wantUpdates := map[string]ads.DataAndErrTuple{ 688 listenerResourceName: { 689 Resource: &xdsresource.ListenerResourceData{ 690 Resource: xdsresource.ListenerUpdate{ 691 RouteConfigName: routeConfigurationName, 692 HTTPFilters: makeRouterFilterList(t), 693 Raw: listenerResource, 694 }, 695 }, 696 }, 697 } 698 wantMD := xdsresource.UpdateMetadata{ 699 Status: xdsresource.ServiceStatusACKed, 700 Version: "1", 701 } 702 703 eventHandler := xc.eventHandler.(*testEventHandler) 704 verifyUpdateAndMetadata(ctx, t, eventHandler, wantUpdates, wantMD) 705 706 lis.Stop() 707 if err := eventHandler.waitForStreamFailure(ctx); err != nil { 708 t.Fatalf("Timeout when waiting for the stream failure callback to be invoked on the xDS client: %v", err) 709 } 710 } 711 712 // Tests the behavior of the xdsChannel when a resource is unsubscribed. 713 // Verifies that when a previously subscribed resource is unsubscribed, a 714 // request is sent without the previously subscribed resource name. 715 func (s) TestChannel_ADS_ResourceUnsubscribe(t *testing.T) { 716 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 717 defer cancel() 718 719 // Start an xDS management server that uses a channel to inform the test 720 // about the specific LDS resource names being requested. 721 ldsResourcesCh := make(chan []string, 1) 722 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ 723 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 724 t.Logf("Received request for resources: %v of type %s", req.GetResourceNames(), req.GetTypeUrl()) 725 726 if req.TypeUrl != version.V3ListenerURL { 727 return fmt.Errorf("unexpected resource type URL: %q", req.TypeUrl) 728 } 729 730 // Make the most recently requested names available to the test. 731 ldsResourcesCh <- req.GetResourceNames() 732 return nil 733 }, 734 }) 735 736 // Configure two listener resources on the management server. 737 const listenerResourceName1 = "test-listener-resource-1" 738 const routeConfigurationName1 = "test-route-configuration-resource-1" 739 const listenerResourceName2 = "test-listener-resource-2" 740 const routeConfigurationName2 = "test-route-configuration-resource-2" 741 nodeID := uuid.New().String() 742 resources := e2e.UpdateOptions{ 743 NodeID: nodeID, 744 Listeners: []*v3listenerpb.Listener{ 745 e2e.DefaultClientListener(listenerResourceName1, routeConfigurationName1), 746 e2e.DefaultClientListener(listenerResourceName2, routeConfigurationName2), 747 }, 748 SkipValidation: true, 749 } 750 if err := mgmtServer.Update(ctx, resources); err != nil { 751 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 752 } 753 754 // Create an xdsChannel for the test with a long watch expiry timer 755 // to ensure that watches don't expire for the duration of the test. 756 xc := xdsChannelForTest(t, mgmtServer.Address, nodeID, 2*defaultTestTimeout) 757 defer xc.close() 758 759 // Subscribe to the resources created above and verify that a request is 760 // sent for the same. 761 xc.subscribe(listenerType, listenerResourceName1) 762 xc.subscribe(listenerType, listenerResourceName2) 763 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil { 764 t.Fatal(err) 765 } 766 767 // Wait for the above resources to be ACKed. 768 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName1, listenerResourceName2}); err != nil { 769 t.Fatal(err) 770 } 771 772 // Unsubscribe to one of the resources created above, and ensure that the 773 // other resource is still being requested. 774 xc.unsubscribe(listenerType, listenerResourceName1) 775 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{listenerResourceName2}); err != nil { 776 t.Fatal(err) 777 } 778 779 // Since the version on the management server for the above resource is not 780 // changed, we will not receive an update from it for the one resource that 781 // we are still requesting. 782 783 // Unsubscribe to the remaining resource, and ensure that no more resources 784 // are being requested. 785 xc.unsubscribe(listenerType, listenerResourceName2) 786 if err := waitForResourceNames(ctx, t, ldsResourcesCh, []string{}); err != nil { 787 t.Fatal(err) 788 } 789 } 790 791 // Tests the load reporting functionality of the xdsChannel. It creates an 792 // xdsChannel, starts load reporting, and verifies that an LRS streaming RPC is 793 // created. It then makes another call to the load reporting API and ensures 794 // that a new LRS stream is not created. Finally, it cancels the load reporting 795 // calls and ensures that the stream is closed when the last call is canceled. 796 // 797 // Note that this test does not actually report any load. That is already tested 798 // by an e2e style test in the xdsclient package. 799 func (s) TestChannel_LRS_ReportLoad(t *testing.T) { 800 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 801 defer cancel() 802 803 // Create a management server that serves LRS. 804 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{SupportLoadReportingService: true}) 805 806 // Create an xdsChannel for the test. Node id and watch expiry timer don't 807 // matter for LRS. 808 xc := xdsChannelForTest(t, mgmtServer.Address, "", defaultTestTimeout) 809 defer xc.close() 810 811 // Start load reporting and verify that an LRS streaming RPC is created. 812 _, stopLRS1 := xc.reportLoad() 813 lrsServer := mgmtServer.LRSServer 814 if _, err := lrsServer.LRSStreamOpenChan.Receive(ctx); err != nil { 815 t.Fatalf("Timeout when waiting for an LRS streaming RPC to be created: %v", err) 816 } 817 818 // Make another call to the load reporting API, and ensure that a new LRS 819 // stream is not created. 820 _, stopLRS2 := xc.reportLoad() 821 sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout) 822 defer sCancel() 823 if _, err := lrsServer.LRSStreamOpenChan.Receive(sCtx); err != context.DeadlineExceeded { 824 t.Fatal("New LRS streaming RPC created when expected to use an existing one") 825 } 826 827 // Cancel the first load reporting call, and ensure that the stream does not 828 // close (because we have another call open). 829 stopLRS1() 830 sCtx, sCancel = context.WithTimeout(context.Background(), defaultTestShortTimeout) 831 defer sCancel() 832 if _, err := lrsServer.LRSStreamCloseChan.Receive(sCtx); err != context.DeadlineExceeded { 833 t.Fatal("LRS stream closed when expected to stay open") 834 } 835 836 // Cancel the second load reporting call, and ensure the stream is closed. 837 stopLRS2() 838 if _, err := lrsServer.LRSStreamCloseChan.Receive(ctx); err != nil { 839 t.Fatal("Timeout waiting for LRS stream to close") 840 } 841 } 842 843 // waitForResourceNames waits for the wantNames to be received on namesCh. 844 // Returns a non-nil error if the context expires before that. 845 func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) error { 846 t.Helper() 847 848 var lastRequestedNames []string 849 for ; ; <-time.After(defaultTestShortTimeout) { 850 select { 851 case <-ctx.Done(): 852 return fmt.Errorf("timeout waiting for resources %v to be requested from the management server. Last requested resources: %v", wantNames, lastRequestedNames) 853 case gotNames := <-namesCh: 854 if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(s1, s2 string) bool { return s1 < s2 })) { 855 return nil 856 } 857 lastRequestedNames = gotNames 858 } 859 } 860 } 861 862 // newTestEventHandler creates a new testEventHandler instance with the 863 // necessary channels for testing the xdsChannel. 864 func newTestEventHandler() *testEventHandler { 865 return &testEventHandler{ 866 typeCh: make(chan xdsresource.Type, 1), 867 updateCh: make(chan map[string]ads.DataAndErrTuple, 1), 868 mdCh: make(chan xdsresource.UpdateMetadata, 1), 869 nameCh: make(chan string, 1), 870 connErrCh: make(chan error, 1), 871 } 872 } 873 874 // testEventHandler is a struct that implements the xdsChannelEventhandler 875 // interface. It is used to receive events from an xdsChannel, and has multiple 876 // channels on which it makes these events available to the test. 877 type testEventHandler struct { 878 typeCh chan xdsresource.Type // Resource type of an update or resource-does-not-exist error. 879 updateCh chan map[string]ads.DataAndErrTuple // Resource updates. 880 mdCh chan xdsresource.UpdateMetadata // Metadata from an update. 881 nameCh chan string // Name of the non-existent resource. 882 connErrCh chan error // Connectivity error. 883 884 } 885 886 func (ta *testEventHandler) adsStreamFailure(err error) { 887 ta.connErrCh <- err 888 } 889 890 func (ta *testEventHandler) waitForStreamFailure(ctx context.Context) error { 891 select { 892 case <-ctx.Done(): 893 return ctx.Err() 894 case <-ta.connErrCh: 895 } 896 return nil 897 } 898 899 func (ta *testEventHandler) adsResourceUpdate(typ xdsresource.Type, updates map[string]ads.DataAndErrTuple, md xdsresource.UpdateMetadata, onDone func()) { 900 ta.typeCh <- typ 901 ta.updateCh <- updates 902 ta.mdCh <- md 903 onDone() 904 } 905 906 // waitForUpdate waits for the next resource update event from the xdsChannel. 907 // It returns the resource type, the resource updates, and the update metadata. 908 // If the context is canceled, it returns an error. 909 func (ta *testEventHandler) waitForUpdate(ctx context.Context) (xdsresource.Type, map[string]ads.DataAndErrTuple, xdsresource.UpdateMetadata, error) { 910 var typ xdsresource.Type 911 var updates map[string]ads.DataAndErrTuple 912 var md xdsresource.UpdateMetadata 913 914 select { 915 case typ = <-ta.typeCh: 916 case <-ctx.Done(): 917 return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err() 918 } 919 920 select { 921 case updates = <-ta.updateCh: 922 case <-ctx.Done(): 923 return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err() 924 } 925 926 select { 927 case md = <-ta.mdCh: 928 case <-ctx.Done(): 929 return nil, nil, xdsresource.UpdateMetadata{}, ctx.Err() 930 } 931 return typ, updates, md, nil 932 } 933 934 func (ta *testEventHandler) adsResourceDoesNotExist(typ xdsresource.Type, name string) { 935 ta.typeCh <- typ 936 ta.nameCh <- name 937 } 938 939 // waitForResourceDoesNotExist waits for the next resource-does-not-exist event 940 // from the xdsChannel. It returns the resource type and the resource name. If 941 // the context is canceled, it returns an error. 942 func (ta *testEventHandler) waitForResourceDoesNotExist(ctx context.Context) (xdsresource.Type, string, error) { 943 var typ xdsresource.Type 944 var name string 945 946 select { 947 case typ = <-ta.typeCh: 948 case <-ctx.Done(): 949 return nil, "", ctx.Err() 950 } 951 952 select { 953 case name = <-ta.nameCh: 954 case <-ctx.Done(): 955 return nil, "", ctx.Err() 956 } 957 return typ, name, nil 958 } 959 960 func newStringP(s string) *string { 961 return &s 962 } 963 964 func makeRouterFilter(t *testing.T) xdsresource.HTTPFilter { 965 routerBuilder := httpfilter.Get(router.TypeURL) 966 routerConfig, _ := routerBuilder.ParseFilterConfig(testutils.MarshalAny(t, &v3routerpb.Router{})) 967 return xdsresource.HTTPFilter{Name: "router", Filter: routerBuilder, Config: routerConfig} 968 } 969 970 func makeRouterFilterList(t *testing.T) []xdsresource.HTTPFilter { 971 return []xdsresource.HTTPFilter{makeRouterFilter(t)} 972 }