google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/tests/rds_watchers_test.go (about) 1 /* 2 * 3 * Copyright 2022 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 19 package xdsclient_test 20 21 import ( 22 "context" 23 "fmt" 24 "strings" 25 "testing" 26 "time" 27 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/grpcsync" 32 "google.golang.org/grpc/internal/testutils" 33 "google.golang.org/grpc/internal/testutils/xds/e2e" 34 xdstestutils "google.golang.org/grpc/xds/internal/testutils" 35 "google.golang.org/grpc/xds/internal/xdsclient" 36 "google.golang.org/grpc/xds/internal/xdsclient/bootstrap" 37 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 38 "google.golang.org/protobuf/types/known/wrapperspb" 39 40 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 41 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 42 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 43 ) 44 45 type noopRouteConfigWatcher struct{} 46 47 func (noopRouteConfigWatcher) OnUpdate(update *xdsresource.RouteConfigResourceData) {} 48 func (noopRouteConfigWatcher) OnError(err error) {} 49 func (noopRouteConfigWatcher) OnResourceDoesNotExist() {} 50 51 type routeConfigUpdateErrTuple struct { 52 update xdsresource.RouteConfigUpdate 53 err error 54 } 55 56 type routeConfigWatcher struct { 57 updateCh *testutils.Channel 58 } 59 60 func newRouteConfigWatcher() *routeConfigWatcher { 61 return &routeConfigWatcher{updateCh: testutils.NewChannel()} 62 } 63 64 func (rw *routeConfigWatcher) OnUpdate(update *xdsresource.RouteConfigResourceData) { 65 rw.updateCh.Send(routeConfigUpdateErrTuple{update: update.Resource}) 66 } 67 68 func (rw *routeConfigWatcher) OnError(err error) { 69 // When used with a go-control-plane management server that continuously 70 // resends resources which are NACKed by the xDS client, using a `Replace()` 71 // here and in OnResourceDoesNotExist() simplifies tests which will have 72 // access to the most recently received error. 73 rw.updateCh.Replace(routeConfigUpdateErrTuple{err: err}) 74 } 75 76 func (rw *routeConfigWatcher) OnResourceDoesNotExist() { 77 rw.updateCh.Replace(routeConfigUpdateErrTuple{err: xdsresource.NewErrorf(xdsresource.ErrorTypeResourceNotFound, "RouteConfiguration not found in received response")}) 78 } 79 80 // badRouteConfigResource returns a RouteConfiguration resource for the given 81 // routeName which contains a retry config with num_retries set to `0`. This is 82 // expected to be NACK'ed by the xDS client. 83 func badRouteConfigResource(routeName, ldsTarget, clusterName string) *v3routepb.RouteConfiguration { 84 return &v3routepb.RouteConfiguration{ 85 Name: routeName, 86 VirtualHosts: []*v3routepb.VirtualHost{{ 87 Domains: []string{ldsTarget}, 88 Routes: []*v3routepb.Route{{ 89 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 90 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 91 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: clusterName}, 92 }}}}, 93 RetryPolicy: &v3routepb.RetryPolicy{ 94 NumRetries: &wrapperspb.UInt32Value{Value: 0}, 95 }, 96 }}, 97 } 98 } 99 100 // xdsClient is expected to produce an error containing this string when an 101 // update is received containing a route configuration resource created using 102 // `badRouteConfigResource`. 103 const wantRouteConfigNACKErr = "received route is invalid: retry_policy.num_retries = 0; must be >= 1" 104 105 // verifyRouteConfigUpdate waits for an update to be received on the provided 106 // update channel and verifies that it matches the expected update. 107 // 108 // Returns an error if no update is received before the context deadline expires 109 // or the received update does not match the expected one. 110 func verifyRouteConfigUpdate(ctx context.Context, updateCh *testutils.Channel, wantUpdate routeConfigUpdateErrTuple) error { 111 u, err := updateCh.Receive(ctx) 112 if err != nil { 113 return fmt.Errorf("timeout when waiting for a route configuration resource from the management server: %v", err) 114 } 115 got := u.(routeConfigUpdateErrTuple) 116 if wantUpdate.err != nil { 117 if gotType, wantType := xdsresource.ErrType(got.err), xdsresource.ErrType(wantUpdate.err); gotType != wantType { 118 return fmt.Errorf("received update with error type %v, want %v", gotType, wantType) 119 } 120 } 121 cmpOpts := []cmp.Option{cmpopts.EquateEmpty(), cmpopts.IgnoreFields(xdsresource.RouteConfigUpdate{}, "Raw")} 122 if diff := cmp.Diff(wantUpdate.update, got.update, cmpOpts...); diff != "" { 123 return fmt.Errorf("received unepected diff in the route configuration resource update: (-want, got):\n%s", diff) 124 } 125 return nil 126 } 127 128 // verifyNoRouteConfigUpdate verifies that no route configuration update is 129 // received on the provided update channel, and returns an error if an update is 130 // received. 131 // 132 // A very short deadline is used while waiting for the update, as this function 133 // is intended to be used when an update is not expected. 134 func verifyNoRouteConfigUpdate(ctx context.Context, updateCh *testutils.Channel) error { 135 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 136 defer sCancel() 137 if u, err := updateCh.Receive(sCtx); err != context.DeadlineExceeded { 138 return fmt.Errorf("unexpected RouteConfigUpdate: %v", u) 139 } 140 return nil 141 } 142 143 // TestRDSWatch covers the case where a single watcher exists for a single route 144 // configuration resource. The test verifies the following scenarios: 145 // 1. An update from the management server containing the resource being 146 // watched should result in the invocation of the watch callback. 147 // 2. An update from the management server containing a resource *not* being 148 // watched should not result in the invocation of the watch callback. 149 // 3. After the watch is cancelled, an update from the management server 150 // containing the resource that was being watched should not result in the 151 // invocation of the watch callback. 152 // 153 // The test is run for old and new style names. 154 func (s) TestRDSWatch(t *testing.T) { 155 tests := []struct { 156 desc string 157 resourceName string 158 watchedResource *v3routepb.RouteConfiguration // The resource being watched. 159 updatedWatchedResource *v3routepb.RouteConfiguration // The watched resource after an update. 160 notWatchedResource *v3routepb.RouteConfiguration // A resource which is not being watched. 161 wantUpdate routeConfigUpdateErrTuple 162 }{ 163 { 164 desc: "old style resource", 165 resourceName: rdsName, 166 watchedResource: e2e.DefaultRouteConfig(rdsName, ldsName, cdsName), 167 updatedWatchedResource: e2e.DefaultRouteConfig(rdsName, ldsName, "new-cds-resource"), 168 notWatchedResource: e2e.DefaultRouteConfig("unsubscribed-rds-resource", ldsName, cdsName), 169 wantUpdate: routeConfigUpdateErrTuple{ 170 update: xdsresource.RouteConfigUpdate{ 171 VirtualHosts: []*xdsresource.VirtualHost{ 172 { 173 Domains: []string{ldsName}, 174 Routes: []*xdsresource.Route{ 175 { 176 Prefix: newStringP("/"), 177 ActionType: xdsresource.RouteActionRoute, 178 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 179 }, 180 }, 181 }, 182 }, 183 }, 184 }, 185 }, 186 { 187 desc: "new style resource", 188 resourceName: rdsNameNewStyle, 189 watchedResource: e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, cdsNameNewStyle), 190 updatedWatchedResource: e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, "new-cds-resource"), 191 notWatchedResource: e2e.DefaultRouteConfig("unsubscribed-rds-resource", ldsNameNewStyle, cdsNameNewStyle), 192 wantUpdate: routeConfigUpdateErrTuple{ 193 update: xdsresource.RouteConfigUpdate{ 194 VirtualHosts: []*xdsresource.VirtualHost{ 195 { 196 Domains: []string{ldsNameNewStyle}, 197 Routes: []*xdsresource.Route{ 198 { 199 Prefix: newStringP("/"), 200 ActionType: xdsresource.RouteActionRoute, 201 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsNameNewStyle: {Weight: 100}}, 202 }, 203 }, 204 }, 205 }, 206 }, 207 }, 208 }, 209 } 210 211 for _, test := range tests { 212 t.Run(test.desc, func(t *testing.T) { 213 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) 214 defer cleanup() 215 216 // Create an xDS client with the above bootstrap contents. 217 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 218 if err != nil { 219 t.Fatalf("Failed to create xDS client: %v", err) 220 } 221 defer close() 222 223 // Register a watch for a route configuration resource and have the 224 // watch callback push the received update on to a channel. 225 rw := newRouteConfigWatcher() 226 rdsCancel := xdsresource.WatchRouteConfig(client, test.resourceName, rw) 227 228 // Configure the management server to return a single route 229 // configuration resource, corresponding to the one being watched. 230 resources := e2e.UpdateOptions{ 231 NodeID: nodeID, 232 Routes: []*v3routepb.RouteConfiguration{test.watchedResource}, 233 SkipValidation: true, 234 } 235 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 236 defer cancel() 237 if err := mgmtServer.Update(ctx, resources); err != nil { 238 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 239 } 240 241 // Verify the contents of the received update. 242 if err := verifyRouteConfigUpdate(ctx, rw.updateCh, test.wantUpdate); err != nil { 243 t.Fatal(err) 244 } 245 246 // Configure the management server to return an additional route 247 // configuration resource, one that we are not interested in. 248 resources = e2e.UpdateOptions{ 249 NodeID: nodeID, 250 Routes: []*v3routepb.RouteConfiguration{test.watchedResource, test.notWatchedResource}, 251 SkipValidation: true, 252 } 253 if err := mgmtServer.Update(ctx, resources); err != nil { 254 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 255 } 256 if err := verifyNoRouteConfigUpdate(ctx, rw.updateCh); err != nil { 257 t.Fatal(err) 258 } 259 260 // Cancel the watch and update the resource corresponding to the original 261 // watch. Ensure that the cancelled watch callback is not invoked. 262 rdsCancel() 263 resources = e2e.UpdateOptions{ 264 NodeID: nodeID, 265 Routes: []*v3routepb.RouteConfiguration{test.updatedWatchedResource, test.notWatchedResource}, 266 SkipValidation: true, 267 } 268 if err := mgmtServer.Update(ctx, resources); err != nil { 269 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 270 } 271 if err := verifyNoRouteConfigUpdate(ctx, rw.updateCh); err != nil { 272 t.Fatal(err) 273 } 274 }) 275 } 276 } 277 278 // TestRDSWatch_TwoWatchesForSameResourceName covers the case where two watchers 279 // exist for a single route configuration resource. The test verifies the 280 // following scenarios: 281 // 1. An update from the management server containing the resource being 282 // watched should result in the invocation of both watch callbacks. 283 // 2. After one of the watches is cancelled, a redundant update from the 284 // management server should not result in the invocation of either of the 285 // watch callbacks. 286 // 3. An update from the management server containing the resource being 287 // watched should result in the invocation of the un-cancelled watch 288 // callback. 289 // 290 // The test is run for old and new style names. 291 func (s) TestRDSWatch_TwoWatchesForSameResourceName(t *testing.T) { 292 tests := []struct { 293 desc string 294 resourceName string 295 watchedResource *v3routepb.RouteConfiguration // The resource being watched. 296 updatedWatchedResource *v3routepb.RouteConfiguration // The watched resource after an update. 297 wantUpdateV1 routeConfigUpdateErrTuple 298 wantUpdateV2 routeConfigUpdateErrTuple 299 }{ 300 { 301 desc: "old style resource", 302 resourceName: rdsName, 303 watchedResource: e2e.DefaultRouteConfig(rdsName, ldsName, cdsName), 304 updatedWatchedResource: e2e.DefaultRouteConfig(rdsName, ldsName, "new-cds-resource"), 305 wantUpdateV1: routeConfigUpdateErrTuple{ 306 update: xdsresource.RouteConfigUpdate{ 307 VirtualHosts: []*xdsresource.VirtualHost{ 308 { 309 Domains: []string{ldsName}, 310 Routes: []*xdsresource.Route{ 311 { 312 Prefix: newStringP("/"), 313 ActionType: xdsresource.RouteActionRoute, 314 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 315 }, 316 }, 317 }, 318 }, 319 }, 320 }, 321 wantUpdateV2: routeConfigUpdateErrTuple{ 322 update: xdsresource.RouteConfigUpdate{ 323 VirtualHosts: []*xdsresource.VirtualHost{ 324 { 325 Domains: []string{ldsName}, 326 Routes: []*xdsresource.Route{ 327 { 328 Prefix: newStringP("/"), 329 ActionType: xdsresource.RouteActionRoute, 330 WeightedClusters: map[string]xdsresource.WeightedCluster{"new-cds-resource": {Weight: 100}}, 331 }, 332 }, 333 }, 334 }, 335 }, 336 }, 337 }, 338 { 339 desc: "new style resource", 340 resourceName: rdsNameNewStyle, 341 watchedResource: e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, cdsNameNewStyle), 342 updatedWatchedResource: e2e.DefaultRouteConfig(rdsNameNewStyle, ldsNameNewStyle, "new-cds-resource"), 343 wantUpdateV1: routeConfigUpdateErrTuple{ 344 update: xdsresource.RouteConfigUpdate{ 345 VirtualHosts: []*xdsresource.VirtualHost{ 346 { 347 Domains: []string{ldsNameNewStyle}, 348 Routes: []*xdsresource.Route{ 349 { 350 Prefix: newStringP("/"), 351 ActionType: xdsresource.RouteActionRoute, 352 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsNameNewStyle: {Weight: 100}}, 353 }, 354 }, 355 }, 356 }, 357 }, 358 }, 359 wantUpdateV2: routeConfigUpdateErrTuple{ 360 update: xdsresource.RouteConfigUpdate{ 361 VirtualHosts: []*xdsresource.VirtualHost{ 362 { 363 Domains: []string{ldsNameNewStyle}, 364 Routes: []*xdsresource.Route{ 365 { 366 Prefix: newStringP("/"), 367 ActionType: xdsresource.RouteActionRoute, 368 WeightedClusters: map[string]xdsresource.WeightedCluster{"new-cds-resource": {Weight: 100}}, 369 }, 370 }, 371 }, 372 }, 373 }, 374 }, 375 }, 376 } 377 378 for _, test := range tests { 379 t.Run(test.desc, func(t *testing.T) { 380 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) 381 defer cleanup() 382 383 // Create an xDS client with the above bootstrap contents. 384 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 385 if err != nil { 386 t.Fatalf("Failed to create xDS client: %v", err) 387 } 388 defer close() 389 390 // Register two watches for the same route configuration resource 391 // and have the callbacks push the received updates on to a channel. 392 rw1 := newRouteConfigWatcher() 393 rdsCancel1 := xdsresource.WatchRouteConfig(client, test.resourceName, rw1) 394 defer rdsCancel1() 395 rw2 := newRouteConfigWatcher() 396 rdsCancel2 := xdsresource.WatchRouteConfig(client, test.resourceName, rw2) 397 398 // Configure the management server to return a single route 399 // configuration resource, corresponding to the one being watched. 400 resources := e2e.UpdateOptions{ 401 NodeID: nodeID, 402 Routes: []*v3routepb.RouteConfiguration{test.watchedResource}, 403 SkipValidation: true, 404 } 405 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 406 defer cancel() 407 if err := mgmtServer.Update(ctx, resources); err != nil { 408 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 409 } 410 411 // Verify the contents of the received update. 412 if err := verifyRouteConfigUpdate(ctx, rw1.updateCh, test.wantUpdateV1); err != nil { 413 t.Fatal(err) 414 } 415 if err := verifyRouteConfigUpdate(ctx, rw2.updateCh, test.wantUpdateV1); err != nil { 416 t.Fatal(err) 417 } 418 419 // Cancel the second watch and force the management server to push a 420 // redundant update for the resource being watched. Neither of the 421 // two watch callbacks should be invoked. 422 rdsCancel2() 423 if err := mgmtServer.Update(ctx, resources); err != nil { 424 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 425 } 426 if err := verifyNoRouteConfigUpdate(ctx, rw1.updateCh); err != nil { 427 t.Fatal(err) 428 } 429 if err := verifyNoRouteConfigUpdate(ctx, rw2.updateCh); err != nil { 430 t.Fatal(err) 431 } 432 433 // Update to the resource being watched. The un-cancelled callback 434 // should be invoked while the cancelled one should not be. 435 resources = e2e.UpdateOptions{ 436 NodeID: nodeID, 437 Routes: []*v3routepb.RouteConfiguration{test.updatedWatchedResource}, 438 SkipValidation: true, 439 } 440 if err := mgmtServer.Update(ctx, resources); err != nil { 441 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 442 } 443 if err := verifyRouteConfigUpdate(ctx, rw1.updateCh, test.wantUpdateV2); err != nil { 444 t.Fatal(err) 445 } 446 if err := verifyNoRouteConfigUpdate(ctx, rw2.updateCh); err != nil { 447 t.Fatal(err) 448 } 449 }) 450 } 451 } 452 453 // TestRDSWatch_ThreeWatchesForDifferentResourceNames covers the case with three 454 // watchers (two watchers for one resource, and the third watcher for another 455 // resource), exist across two route configuration resources. The test verifies 456 // that an update from the management server containing both resources results 457 // in the invocation of all watch callbacks. 458 // 459 // The test is run with both old and new style names. 460 func (s) TestRDSWatch_ThreeWatchesForDifferentResourceNames(t *testing.T) { 461 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) 462 defer cleanup() 463 464 // Create an xDS client with the above bootstrap contents. 465 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 466 if err != nil { 467 t.Fatalf("Failed to create xDS client: %v", err) 468 } 469 defer close() 470 471 // Register two watches for the same route configuration resource 472 // and have the callbacks push the received updates on to a channel. 473 rw1 := newRouteConfigWatcher() 474 rdsCancel1 := xdsresource.WatchRouteConfig(client, rdsName, rw1) 475 defer rdsCancel1() 476 rw2 := newRouteConfigWatcher() 477 rdsCancel2 := xdsresource.WatchRouteConfig(client, rdsName, rw2) 478 defer rdsCancel2() 479 480 // Register the third watch for a different route configuration resource. 481 rw3 := newRouteConfigWatcher() 482 rdsCancel3 := xdsresource.WatchRouteConfig(client, rdsNameNewStyle, rw3) 483 defer rdsCancel3() 484 485 // Configure the management server to return two route configuration 486 // resources, corresponding to the registered watches. 487 resources := e2e.UpdateOptions{ 488 NodeID: nodeID, 489 Routes: []*v3routepb.RouteConfiguration{ 490 e2e.DefaultRouteConfig(rdsName, ldsName, cdsName), 491 e2e.DefaultRouteConfig(rdsNameNewStyle, ldsName, cdsName), 492 }, 493 SkipValidation: true, 494 } 495 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 496 defer cancel() 497 if err := mgmtServer.Update(ctx, resources); err != nil { 498 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 499 } 500 501 // Verify the contents of the received update for the all watchers. The two 502 // resources returned differ only in the resource name. Therefore the 503 // expected update is the same for all the watchers. 504 wantUpdate := routeConfigUpdateErrTuple{ 505 update: xdsresource.RouteConfigUpdate{ 506 VirtualHosts: []*xdsresource.VirtualHost{ 507 { 508 Domains: []string{ldsName}, 509 Routes: []*xdsresource.Route{ 510 { 511 Prefix: newStringP("/"), 512 ActionType: xdsresource.RouteActionRoute, 513 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 514 }, 515 }, 516 }, 517 }, 518 }, 519 } 520 if err := verifyRouteConfigUpdate(ctx, rw1.updateCh, wantUpdate); err != nil { 521 t.Fatal(err) 522 } 523 if err := verifyRouteConfigUpdate(ctx, rw2.updateCh, wantUpdate); err != nil { 524 t.Fatal(err) 525 } 526 if err := verifyRouteConfigUpdate(ctx, rw3.updateCh, wantUpdate); err != nil { 527 t.Fatal(err) 528 } 529 } 530 531 // TestRDSWatch_ResourceCaching covers the case where a watch is registered for 532 // a resource which is already present in the cache. The test verifies that the 533 // watch callback is invoked with the contents from the cache, instead of a 534 // request being sent to the management server. 535 func (s) TestRDSWatch_ResourceCaching(t *testing.T) { 536 firstRequestReceived := false 537 firstAckReceived := grpcsync.NewEvent() 538 secondRequestReceived := grpcsync.NewEvent() 539 540 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{ 541 OnStreamRequest: func(id int64, req *v3discoverypb.DiscoveryRequest) error { 542 // The first request has an empty version string. 543 if !firstRequestReceived && req.GetVersionInfo() == "" { 544 firstRequestReceived = true 545 return nil 546 } 547 // The first ack has a non-empty version string. 548 if !firstAckReceived.HasFired() && req.GetVersionInfo() != "" { 549 firstAckReceived.Fire() 550 return nil 551 } 552 // Any requests after the first request and ack, are not expected. 553 secondRequestReceived.Fire() 554 return nil 555 }, 556 }) 557 defer cleanup() 558 559 // Create an xDS client with the above bootstrap contents. 560 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 561 if err != nil { 562 t.Fatalf("Failed to create xDS client: %v", err) 563 } 564 defer close() 565 566 // Register a watch for a route configuration resource and have the watch 567 // callback push the received update on to a channel. 568 rw1 := newRouteConfigWatcher() 569 rdsCancel1 := xdsresource.WatchRouteConfig(client, rdsName, rw1) 570 defer rdsCancel1() 571 572 // Configure the management server to return a single route configuration 573 // resource, corresponding to the one we registered a watch for. 574 resources := e2e.UpdateOptions{ 575 NodeID: nodeID, 576 Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, ldsName, cdsName)}, 577 SkipValidation: true, 578 } 579 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 580 defer cancel() 581 if err := mgmtServer.Update(ctx, resources); err != nil { 582 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 583 } 584 585 // Verify the contents of the received update. 586 wantUpdate := routeConfigUpdateErrTuple{ 587 update: xdsresource.RouteConfigUpdate{ 588 VirtualHosts: []*xdsresource.VirtualHost{ 589 { 590 Domains: []string{ldsName}, 591 Routes: []*xdsresource.Route{ 592 { 593 Prefix: newStringP("/"), 594 ActionType: xdsresource.RouteActionRoute, 595 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 596 }, 597 }, 598 }, 599 }, 600 }, 601 } 602 if err := verifyRouteConfigUpdate(ctx, rw1.updateCh, wantUpdate); err != nil { 603 t.Fatal(err) 604 } 605 select { 606 case <-ctx.Done(): 607 t.Fatal("timeout when waiting for receipt of ACK at the management server") 608 case <-firstAckReceived.Done(): 609 } 610 611 // Register another watch for the same resource. This should get the update 612 // from the cache. 613 rw2 := newRouteConfigWatcher() 614 rdsCancel2 := xdsresource.WatchRouteConfig(client, rdsName, rw2) 615 defer rdsCancel2() 616 if err := verifyRouteConfigUpdate(ctx, rw2.updateCh, wantUpdate); err != nil { 617 t.Fatal(err) 618 } 619 // No request should get sent out as part of this watch. 620 sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) 621 defer sCancel() 622 select { 623 case <-sCtx.Done(): 624 case <-secondRequestReceived.Done(): 625 t.Fatal("xdsClient sent out request instead of using update from cache") 626 } 627 } 628 629 // TestRDSWatch_ExpiryTimerFiresBeforeResponse tests the case where the client 630 // does not receive an RDS response for the request that it sends. The test 631 // verifies that the watch callback is invoked with an error once the 632 // watchExpiryTimer fires. 633 func (s) TestRDSWatch_ExpiryTimerFiresBeforeResponse(t *testing.T) { 634 mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{}) 635 if err != nil { 636 t.Fatalf("Failed to spin up the xDS management server: %v", err) 637 } 638 defer mgmtServer.Stop() 639 640 // Create an xDS client talking to a non-existent management server. 641 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 642 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 643 NodeProto: &v3corepb.Node{}, 644 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 645 if err != nil { 646 t.Fatalf("failed to create xds client: %v", err) 647 } 648 defer close() 649 650 // Register a watch for a resource which is expected to fail with an error 651 // after the watch expiry timer fires. 652 rw := newRouteConfigWatcher() 653 rdsCancel := xdsresource.WatchRouteConfig(client, rdsName, rw) 654 defer rdsCancel() 655 656 // Wait for the watch expiry timer to fire. 657 <-time.After(defaultTestWatchExpiryTimeout) 658 659 // Verify that an empty update with the expected error is received. 660 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 661 defer cancel() 662 wantErr := xdsresource.NewErrorf(xdsresource.ErrorTypeResourceNotFound, "") 663 if err := verifyRouteConfigUpdate(ctx, rw.updateCh, routeConfigUpdateErrTuple{err: wantErr}); err != nil { 664 t.Fatal(err) 665 } 666 } 667 668 // TestRDSWatch_ValidResponseCancelsExpiryTimerBehavior tests the case where the 669 // client receives a valid RDS response for the request that it sends. The test 670 // verifies that the behavior associated with the expiry timer (i.e, callback 671 // invocation with error) does not take place. 672 func (s) TestRDSWatch_ValidResponseCancelsExpiryTimerBehavior(t *testing.T) { 673 mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{}) 674 if err != nil { 675 t.Fatalf("Failed to spin up the xDS management server: %v", err) 676 } 677 defer mgmtServer.Stop() 678 679 // Create an xDS client talking to the above management server. 680 nodeID := uuid.New().String() 681 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 682 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 683 NodeProto: &v3corepb.Node{Id: nodeID}, 684 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 685 if err != nil { 686 t.Fatalf("failed to create xds client: %v", err) 687 } 688 defer close() 689 690 // Register a watch for a route configuration resource and have the watch 691 // callback push the received update on to a channel. 692 rw := newRouteConfigWatcher() 693 rdsCancel := xdsresource.WatchRouteConfig(client, rdsName, rw) 694 defer rdsCancel() 695 696 // Configure the management server to return a single route configuration 697 // resource, corresponding to the one we registered a watch for. 698 resources := e2e.UpdateOptions{ 699 NodeID: nodeID, 700 Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, ldsName, cdsName)}, 701 SkipValidation: true, 702 } 703 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 704 defer cancel() 705 if err := mgmtServer.Update(ctx, resources); err != nil { 706 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 707 } 708 709 // Verify the contents of the received update. 710 wantUpdate := routeConfigUpdateErrTuple{ 711 update: xdsresource.RouteConfigUpdate{ 712 VirtualHosts: []*xdsresource.VirtualHost{ 713 { 714 Domains: []string{ldsName}, 715 Routes: []*xdsresource.Route{ 716 { 717 Prefix: newStringP("/"), 718 ActionType: xdsresource.RouteActionRoute, 719 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 720 }, 721 }, 722 }, 723 }, 724 }, 725 } 726 if err := verifyRouteConfigUpdate(ctx, rw.updateCh, wantUpdate); err != nil { 727 t.Fatal(err) 728 } 729 730 // Wait for the watch expiry timer to fire, and verify that the callback is 731 // not invoked. 732 <-time.After(defaultTestWatchExpiryTimeout) 733 if err := verifyNoRouteConfigUpdate(ctx, rw.updateCh); err != nil { 734 t.Fatal(err) 735 } 736 } 737 738 // TestRDSWatch_NACKError covers the case where an update from the management 739 // server is NACK'ed by the xdsclient. The test verifies that the error is 740 // propagated to the watcher. 741 func (s) TestRDSWatch_NACKError(t *testing.T) { 742 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) 743 defer cleanup() 744 745 // Create an xDS client with the above bootstrap contents. 746 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 747 if err != nil { 748 t.Fatalf("Failed to create xDS client: %v", err) 749 } 750 defer close() 751 752 // Register a watch for a route configuration resource and have the watch 753 // callback push the received update on to a channel. 754 rw := newRouteConfigWatcher() 755 rdsCancel := xdsresource.WatchRouteConfig(client, rdsName, rw) 756 defer rdsCancel() 757 758 // Configure the management server to return a single route configuration 759 // resource which is expected to be NACKed by the client. 760 resources := e2e.UpdateOptions{ 761 NodeID: nodeID, 762 Routes: []*v3routepb.RouteConfiguration{badRouteConfigResource(rdsName, ldsName, cdsName)}, 763 SkipValidation: true, 764 } 765 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 766 defer cancel() 767 if err := mgmtServer.Update(ctx, resources); err != nil { 768 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 769 } 770 771 // Verify that the expected error is propagated to the watcher. 772 u, err := rw.updateCh.Receive(ctx) 773 if err != nil { 774 t.Fatalf("timeout when waiting for a route configuration resource from the management server: %v", err) 775 } 776 gotErr := u.(routeConfigUpdateErrTuple).err 777 if gotErr == nil || !strings.Contains(gotErr.Error(), wantRouteConfigNACKErr) { 778 t.Fatalf("update received with error: %v, want %q", gotErr, wantRouteConfigNACKErr) 779 } 780 } 781 782 // TestRDSWatch_PartialValid covers the case where a response from the 783 // management server contains both valid and invalid resources and is expected 784 // to be NACK'ed by the xdsclient. The test verifies that watchers corresponding 785 // to the valid resource receive the update, while watchers corresponding to the 786 // invalid resource receive an error. 787 func (s) TestRDSWatch_PartialValid(t *testing.T) { 788 mgmtServer, nodeID, bootstrapContents, _, cleanup := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{}) 789 defer cleanup() 790 791 // Create an xDS client with the above bootstrap contents. 792 client, close, err := xdsclient.NewWithBootstrapContentsForTesting(bootstrapContents) 793 if err != nil { 794 t.Fatalf("Failed to create xDS client: %v", err) 795 } 796 defer close() 797 798 // Register two watches for route configuration resources. The first watch 799 // is expected to receive an error because the received resource is NACKed. 800 // The second watch is expected to get a good update. 801 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 802 defer cancel() 803 badResourceName := rdsName 804 rw1 := newRouteConfigWatcher() 805 rdsCancel1 := xdsresource.WatchRouteConfig(client, badResourceName, rw1) 806 defer rdsCancel1() 807 goodResourceName := rdsNameNewStyle 808 rw2 := newRouteConfigWatcher() 809 rdsCancel2 := xdsresource.WatchRouteConfig(client, goodResourceName, rw2) 810 defer rdsCancel2() 811 812 // Configure the management server to return two route configuration 813 // resources, corresponding to the registered watches. 814 resources := e2e.UpdateOptions{ 815 NodeID: nodeID, 816 Routes: []*v3routepb.RouteConfiguration{ 817 badRouteConfigResource(badResourceName, ldsName, cdsName), 818 e2e.DefaultRouteConfig(goodResourceName, ldsName, cdsName), 819 }, 820 SkipValidation: true, 821 } 822 if err := mgmtServer.Update(ctx, resources); err != nil { 823 t.Fatalf("Failed to update management server with resources: %v, err: %v", resources, err) 824 } 825 826 // Verify that the expected error is propagated to the watcher which 827 // requested for the bad resource. 828 u, err := rw1.updateCh.Receive(ctx) 829 if err != nil { 830 t.Fatalf("timeout when waiting for a route configuration resource from the management server: %v", err) 831 } 832 gotErr := u.(routeConfigUpdateErrTuple).err 833 if gotErr == nil || !strings.Contains(gotErr.Error(), wantRouteConfigNACKErr) { 834 t.Fatalf("update received with error: %v, want %q", gotErr, wantRouteConfigNACKErr) 835 } 836 837 // Verify that the watcher watching the good resource receives a good 838 // update. 839 wantUpdate := routeConfigUpdateErrTuple{ 840 update: xdsresource.RouteConfigUpdate{ 841 VirtualHosts: []*xdsresource.VirtualHost{ 842 { 843 Domains: []string{ldsName}, 844 Routes: []*xdsresource.Route{ 845 { 846 Prefix: newStringP("/"), 847 ActionType: xdsresource.RouteActionRoute, 848 WeightedClusters: map[string]xdsresource.WeightedCluster{cdsName: {Weight: 100}}, 849 }, 850 }, 851 }, 852 }, 853 }, 854 } 855 if err := verifyRouteConfigUpdate(ctx, rw2.updateCh, wantUpdate); err != nil { 856 t.Fatal(err) 857 } 858 }