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