google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/tests/resource_update_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/testutils" 32 "google.golang.org/grpc/internal/testutils/xds/e2e" 33 "google.golang.org/grpc/internal/testutils/xds/fakeserver" 34 "google.golang.org/grpc/xds/internal" 35 xdstestutils "google.golang.org/grpc/xds/internal/testutils" 36 "google.golang.org/grpc/xds/internal/xdsclient" 37 "google.golang.org/grpc/xds/internal/xdsclient/bootstrap" 38 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 39 "google.golang.org/protobuf/proto" 40 "google.golang.org/protobuf/testing/protocmp" 41 "google.golang.org/protobuf/types/known/anypb" 42 "google.golang.org/protobuf/types/known/wrapperspb" 43 44 v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 45 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 46 v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 47 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 48 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 49 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 50 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 51 52 _ "google.golang.org/grpc/xds/internal/httpfilter/router" // Register the router filter. 53 ) 54 55 // startFakeManagementServer starts a fake xDS management server and returns a 56 // cleanup function to close the fake server. 57 func startFakeManagementServer(t *testing.T) (*fakeserver.Server, func()) { 58 t.Helper() 59 fs, sCleanup, err := fakeserver.StartServer(nil) 60 if err != nil { 61 t.Fatalf("Failed to start fake xDS server: %v", err) 62 } 63 return fs, sCleanup 64 } 65 66 func compareUpdateMetadata(ctx context.Context, dumpFunc func() map[string]xdsresource.UpdateWithMD, want map[string]xdsresource.UpdateWithMD) error { 67 var lastErr error 68 for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) { 69 cmpOpts := cmp.Options{ 70 cmpopts.EquateEmpty(), 71 cmp.Comparer(func(a, b time.Time) bool { return true }), 72 cmpopts.EquateErrors(), 73 protocmp.Transform(), 74 } 75 gotUpdateMetadata := dumpFunc() 76 diff := cmp.Diff(want, gotUpdateMetadata, cmpOpts) 77 if diff == "" { 78 return nil 79 } 80 lastErr = fmt.Errorf("unexpected diff in metadata, diff (-want +got):\n%s\n want: %+v\n got: %+v", diff, want, gotUpdateMetadata) 81 } 82 return fmt.Errorf("timeout when waiting for expected update metadata: %v", lastErr) 83 } 84 85 // TestHandleListenerResponseFromManagementServer covers different scenarios 86 // involving receipt of an LDS response from the management server. The test 87 // verifies that the internal state of the xDS client (parsed resource and 88 // metadata) matches expectations. 89 func (s) TestHandleListenerResponseFromManagementServer(t *testing.T) { 90 const ( 91 resourceName1 = "resource-name-1" 92 resourceName2 = "resource-name-2" 93 ) 94 var ( 95 emptyRouterFilter = e2e.RouterHTTPFilter 96 apiListener = &v3listenerpb.ApiListener{ 97 ApiListener: func() *anypb.Any { 98 return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 99 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{ 100 Rds: &v3httppb.Rds{ 101 ConfigSource: &v3corepb.ConfigSource{ 102 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 103 }, 104 RouteConfigName: "route-configuration-name", 105 }, 106 }, 107 HttpFilters: []*v3httppb.HttpFilter{emptyRouterFilter}, 108 }) 109 }(), 110 } 111 resource1 = &v3listenerpb.Listener{ 112 Name: resourceName1, 113 ApiListener: apiListener, 114 } 115 resource2 = &v3listenerpb.Listener{ 116 Name: resourceName2, 117 ApiListener: apiListener, 118 } 119 ) 120 121 tests := []struct { 122 desc string 123 resourceName string 124 managementServerResponse *v3discoverypb.DiscoveryResponse 125 wantUpdate xdsresource.ListenerUpdate 126 wantErr string 127 wantUpdateMetadata map[string]xdsresource.UpdateWithMD 128 }{ 129 { 130 desc: "badly-marshaled-response", 131 resourceName: resourceName1, 132 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 133 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 134 VersionInfo: "1", 135 Resources: []*anypb.Any{{ 136 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 137 Value: []byte{1, 2, 3, 4}, 138 }}, 139 }, 140 wantErr: "Listener not found in received response", 141 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 142 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 143 }, 144 }, 145 { 146 desc: "empty-response", 147 resourceName: resourceName1, 148 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 149 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 150 VersionInfo: "1", 151 }, 152 wantErr: "Listener not found in received response", 153 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 154 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 155 }, 156 }, 157 { 158 desc: "unexpected-type-in-response", 159 resourceName: resourceName1, 160 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 161 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 162 VersionInfo: "1", 163 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3routepb.RouteConfiguration{})}, 164 }, 165 wantErr: "Listener not found in received response", 166 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 167 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 168 }, 169 }, 170 { 171 desc: "one-bad-resource", 172 resourceName: resourceName1, 173 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 174 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 175 VersionInfo: "1", 176 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{ 177 Name: resourceName1, 178 ApiListener: &v3listenerpb.ApiListener{ 179 ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{}), 180 }}), 181 }, 182 }, 183 wantErr: "no RouteSpecifier", 184 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 185 "resource-name-1": {MD: xdsresource.UpdateMetadata{ 186 Status: xdsresource.ServiceStatusNACKed, 187 ErrState: &xdsresource.UpdateErrorMetadata{ 188 Version: "1", 189 Err: cmpopts.AnyError, 190 }, 191 }}, 192 }, 193 }, 194 { 195 desc: "one-good-resource", 196 resourceName: resourceName1, 197 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 198 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 199 VersionInfo: "1", 200 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1)}, 201 }, 202 wantUpdate: xdsresource.ListenerUpdate{ 203 RouteConfigName: "route-configuration-name", 204 HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, 205 }, 206 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 207 "resource-name-1": { 208 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 209 Raw: testutils.MarshalAny(t, resource1), 210 }, 211 }, 212 }, 213 { 214 desc: "two-resources-when-we-requested-one", 215 resourceName: resourceName1, 216 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 217 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 218 VersionInfo: "1", 219 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)}, 220 }, 221 wantUpdate: xdsresource.ListenerUpdate{ 222 RouteConfigName: "route-configuration-name", 223 HTTPFilters: []xdsresource.HTTPFilter{{Name: "router"}}, 224 }, 225 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 226 "resource-name-1": { 227 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 228 Raw: testutils.MarshalAny(t, resource1), 229 }, 230 }, 231 }, 232 } 233 234 for _, test := range tests { 235 t.Run(test.desc, func(t *testing.T) { 236 // Create a fake xDS management server listening on a local port, 237 // and set it up with the response to send. 238 mgmtServer, cleanup := startFakeManagementServer(t) 239 defer cleanup() 240 t.Logf("Started xDS management server on %s", mgmtServer.Address) 241 242 // Create an xDS client talking to the above management server. 243 nodeID := uuid.New().String() 244 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 245 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 246 NodeProto: &v3corepb.Node{Id: nodeID}, 247 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 248 if err != nil { 249 t.Fatalf("failed to create xds client: %v", err) 250 } 251 defer close() 252 t.Logf("Created xDS client to %s", mgmtServer.Address) 253 254 // Register a watch, and push the results on to a channel. 255 lw := newListenerWatcher() 256 cancel := xdsresource.WatchListener(client, test.resourceName, lw) 257 defer cancel() 258 t.Logf("Registered a watch for Listener %q", test.resourceName) 259 260 // Wait for the discovery request to be sent out. 261 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 262 defer cancel() 263 val, err := mgmtServer.XDSRequestChan.Receive(ctx) 264 if err != nil { 265 t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx) 266 } 267 wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{ 268 Node: &v3corepb.Node{Id: nodeID}, 269 ResourceNames: []string{test.resourceName}, 270 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 271 }} 272 gotReq := val.(*fakeserver.Request) 273 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" { 274 t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq) 275 } 276 t.Logf("Discovery request received at management server") 277 278 // Configure the fake management server with a response. 279 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 280 281 // Wait for an update from the xDS client and compare with expected 282 // update. 283 val, err = lw.updateCh.Receive(ctx) 284 if err != nil { 285 t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err) 286 } 287 gotUpdate := val.(listenerUpdateErrTuple).update 288 gotErr := val.(listenerUpdateErrTuple).err 289 if (gotErr != nil) != (test.wantErr != "") { 290 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 291 } 292 if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) { 293 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 294 } 295 cmpOpts := []cmp.Option{ 296 cmpopts.EquateEmpty(), 297 cmpopts.IgnoreFields(xdsresource.HTTPFilter{}, "Filter", "Config"), 298 cmpopts.IgnoreFields(xdsresource.ListenerUpdate{}, "Raw"), 299 } 300 if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" { 301 t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff) 302 } 303 if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD { 304 dump := client.DumpResources() 305 return dump["type.googleapis.com/envoy.config.listener.v3.Listener"] 306 }, test.wantUpdateMetadata); err != nil { 307 t.Fatal(err) 308 } 309 }) 310 } 311 } 312 313 // TestHandleRouteConfigResponseFromManagementServer covers different scenarios 314 // involving receipt of an RDS response from the management server. The test 315 // verifies that the internal state of the xDS client (parsed resource and 316 // metadata) matches expectations. 317 func (s) TestHandleRouteConfigResponseFromManagementServer(t *testing.T) { 318 const ( 319 resourceName1 = "resource-name-1" 320 resourceName2 = "resource-name-2" 321 ) 322 var ( 323 virtualHosts = []*v3routepb.VirtualHost{ 324 { 325 Domains: []string{"lds-target-name"}, 326 Routes: []*v3routepb.Route{ 327 { 328 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: ""}}, 329 Action: &v3routepb.Route_Route{ 330 Route: &v3routepb.RouteAction{ 331 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "cluster-name"}, 332 }, 333 }, 334 }, 335 }, 336 }, 337 } 338 resource1 = &v3routepb.RouteConfiguration{ 339 Name: resourceName1, 340 VirtualHosts: virtualHosts, 341 } 342 resource2 = &v3routepb.RouteConfiguration{ 343 Name: resourceName2, 344 VirtualHosts: virtualHosts, 345 } 346 ) 347 348 tests := []struct { 349 desc string 350 resourceName string 351 managementServerResponse *v3discoverypb.DiscoveryResponse 352 wantUpdate xdsresource.RouteConfigUpdate 353 wantErr string 354 wantUpdateMetadata map[string]xdsresource.UpdateWithMD 355 }{ 356 // The first three tests involve scenarios where the response fails 357 // protobuf deserialization (because it contains an invalid data or type 358 // in the anypb.Any) or the requested resource is not present in the 359 // response. In either case, no resource update makes its way to the 360 // top-level xDS client. An RDS response without a requested resource 361 // does not mean that the resource does not exist in the server. It 362 // could be part of a future update. Therefore, the only failure mode 363 // for this resource is for the watch to timeout. 364 { 365 desc: "badly-marshaled-response", 366 resourceName: resourceName1, 367 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 368 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 369 VersionInfo: "1", 370 Resources: []*anypb.Any{{ 371 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 372 Value: []byte{1, 2, 3, 4}, 373 }}, 374 }, 375 wantErr: "RouteConfiguration not found in received response", 376 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 377 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 378 }, 379 }, 380 { 381 desc: "empty-response", 382 resourceName: resourceName1, 383 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 384 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 385 VersionInfo: "1", 386 }, 387 wantErr: "RouteConfiguration not found in received response", 388 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 389 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 390 }, 391 }, 392 { 393 desc: "unexpected-type-in-response", 394 resourceName: resourceName1, 395 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 396 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 397 VersionInfo: "1", 398 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3clusterpb.Cluster{})}, 399 }, 400 wantErr: "RouteConfiguration not found in received response", 401 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 402 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 403 }, 404 }, 405 { 406 desc: "one-bad-resource", 407 resourceName: resourceName1, 408 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 409 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 410 VersionInfo: "1", 411 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3routepb.RouteConfiguration{ 412 Name: resourceName1, 413 VirtualHosts: []*v3routepb.VirtualHost{{ 414 Domains: []string{"lds-resource-name"}, 415 Routes: []*v3routepb.Route{{ 416 Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}}, 417 Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{ 418 ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "cluster-resource-name"}, 419 }}}}, 420 RetryPolicy: &v3routepb.RetryPolicy{ 421 NumRetries: &wrapperspb.UInt32Value{Value: 0}, 422 }, 423 }}, 424 })}, 425 }, 426 wantErr: "received route is invalid: retry_policy.num_retries = 0; must be >= 1", 427 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 428 "resource-name-1": {MD: xdsresource.UpdateMetadata{ 429 Status: xdsresource.ServiceStatusNACKed, 430 ErrState: &xdsresource.UpdateErrorMetadata{ 431 Version: "1", 432 Err: cmpopts.AnyError, 433 }, 434 }}, 435 }, 436 }, 437 { 438 desc: "one-good-resource", 439 resourceName: resourceName1, 440 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 441 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 442 VersionInfo: "1", 443 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1)}, 444 }, 445 wantUpdate: xdsresource.RouteConfigUpdate{ 446 VirtualHosts: []*xdsresource.VirtualHost{ 447 { 448 Domains: []string{"lds-target-name"}, 449 Routes: []*xdsresource.Route{{Prefix: newStringP(""), 450 WeightedClusters: map[string]xdsresource.WeightedCluster{"cluster-name": {Weight: 1}}, 451 ActionType: xdsresource.RouteActionRoute}}, 452 }, 453 }, 454 }, 455 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 456 "resource-name-1": { 457 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 458 Raw: testutils.MarshalAny(t, resource1), 459 }, 460 }, 461 }, 462 { 463 desc: "two-resources-when-we-requested-one", 464 resourceName: resourceName1, 465 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 466 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 467 VersionInfo: "1", 468 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)}, 469 }, 470 wantUpdate: xdsresource.RouteConfigUpdate{ 471 VirtualHosts: []*xdsresource.VirtualHost{ 472 { 473 Domains: []string{"lds-target-name"}, 474 Routes: []*xdsresource.Route{{Prefix: newStringP(""), 475 WeightedClusters: map[string]xdsresource.WeightedCluster{"cluster-name": {Weight: 1}}, 476 ActionType: xdsresource.RouteActionRoute}}, 477 }, 478 }, 479 }, 480 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 481 "resource-name-1": { 482 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 483 Raw: testutils.MarshalAny(t, resource1), 484 }, 485 }, 486 }, 487 } 488 for _, test := range tests { 489 t.Run(test.desc, func(t *testing.T) { 490 // Create a fake xDS management server listening on a local port, 491 // and set it up with the response to send. 492 mgmtServer, cleanup := startFakeManagementServer(t) 493 defer cleanup() 494 t.Logf("Started xDS management server on %s", mgmtServer.Address) 495 496 // Create an xDS client talking to the above management server. 497 nodeID := uuid.New().String() 498 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 499 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 500 NodeProto: &v3corepb.Node{Id: nodeID}, 501 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 502 if err != nil { 503 t.Fatalf("failed to create xds client: %v", err) 504 } 505 defer close() 506 t.Logf("Created xDS client to %s", mgmtServer.Address) 507 508 // Register a watch, and push the results on to a channel. 509 rw := newRouteConfigWatcher() 510 cancel := xdsresource.WatchRouteConfig(client, test.resourceName, rw) 511 defer cancel() 512 t.Logf("Registered a watch for Route Configuration %q", test.resourceName) 513 514 // Wait for the discovery request to be sent out. 515 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 516 defer cancel() 517 val, err := mgmtServer.XDSRequestChan.Receive(ctx) 518 if err != nil { 519 t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx) 520 } 521 wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{ 522 Node: &v3corepb.Node{Id: nodeID}, 523 ResourceNames: []string{test.resourceName}, 524 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 525 }} 526 gotReq := val.(*fakeserver.Request) 527 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" { 528 t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq) 529 } 530 t.Logf("Discovery request received at management server") 531 532 // Configure the fake management server with a response. 533 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 534 535 // Wait for an update from the xDS client and compare with expected 536 // update. 537 val, err = rw.updateCh.Receive(ctx) 538 if err != nil { 539 t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err) 540 } 541 gotUpdate := val.(routeConfigUpdateErrTuple).update 542 gotErr := val.(routeConfigUpdateErrTuple).err 543 if (gotErr != nil) != (test.wantErr != "") { 544 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 545 } 546 if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) { 547 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 548 } 549 cmpOpts := []cmp.Option{ 550 cmpopts.EquateEmpty(), 551 cmpopts.IgnoreFields(xdsresource.RouteConfigUpdate{}, "Raw"), 552 } 553 if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" { 554 t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff) 555 } 556 if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD { 557 dump := client.DumpResources() 558 return dump["type.googleapis.com/envoy.config.route.v3.RouteConfiguration"] 559 }, test.wantUpdateMetadata); err != nil { 560 t.Fatal(err) 561 } 562 }) 563 } 564 } 565 566 // TestHandleClusterResponseFromManagementServer covers different scenarios 567 // involving receipt of a CDS response from the management server. The test 568 // verifies that the internal state of the xDS client (parsed resource and 569 // metadata) matches expectations. 570 func (s) TestHandleClusterResponseFromManagementServer(t *testing.T) { 571 const ( 572 resourceName1 = "resource-name-1" 573 resourceName2 = "resource-name-2" 574 ) 575 resource1 := e2e.ClusterResourceWithOptions(e2e.ClusterOptions{ 576 ClusterName: resourceName1, 577 ServiceName: "eds-service-name", 578 EnableLRS: true, 579 }) 580 resource2 := proto.Clone(resource1).(*v3clusterpb.Cluster) 581 resource2.Name = resourceName2 582 583 tests := []struct { 584 desc string 585 resourceName string 586 managementServerResponse *v3discoverypb.DiscoveryResponse 587 wantUpdate xdsresource.ClusterUpdate 588 wantErr string 589 wantUpdateMetadata map[string]xdsresource.UpdateWithMD 590 }{ 591 { 592 desc: "badly-marshaled-response", 593 resourceName: resourceName1, 594 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 595 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 596 VersionInfo: "1", 597 Resources: []*anypb.Any{{ 598 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 599 Value: []byte{1, 2, 3, 4}, 600 }}, 601 }, 602 wantErr: "Cluster not found in received response", 603 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 604 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 605 }, 606 }, 607 { 608 desc: "empty-response", 609 resourceName: resourceName1, 610 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 611 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 612 VersionInfo: "1", 613 }, 614 wantErr: "Cluster not found in received response", 615 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 616 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 617 }, 618 }, 619 { 620 desc: "unexpected-type-in-response", 621 resourceName: resourceName1, 622 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 623 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 624 VersionInfo: "1", 625 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3endpointpb.ClusterLoadAssignment{})}, 626 }, 627 wantErr: "Cluster not found in received response", 628 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 629 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 630 }, 631 }, 632 { 633 desc: "one-bad-resource", 634 resourceName: resourceName1, 635 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 636 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 637 VersionInfo: "1", 638 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3clusterpb.Cluster{ 639 Name: resourceName1, 640 ClusterDiscoveryType: &v3clusterpb.Cluster_Type{Type: v3clusterpb.Cluster_EDS}, 641 EdsClusterConfig: &v3clusterpb.Cluster_EdsClusterConfig{ 642 EdsConfig: &v3corepb.ConfigSource{ 643 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{ 644 Ads: &v3corepb.AggregatedConfigSource{}, 645 }, 646 }, 647 ServiceName: "eds-service-name", 648 }, 649 LbPolicy: v3clusterpb.Cluster_MAGLEV, 650 })}, 651 }, 652 wantErr: "unexpected lbPolicy MAGLEV", 653 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 654 "resource-name-1": {MD: xdsresource.UpdateMetadata{ 655 Status: xdsresource.ServiceStatusNACKed, 656 ErrState: &xdsresource.UpdateErrorMetadata{ 657 Version: "1", 658 Err: cmpopts.AnyError, 659 }, 660 }}, 661 }, 662 }, 663 { 664 desc: "one-good-resource", 665 resourceName: resourceName1, 666 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 667 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 668 VersionInfo: "1", 669 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1)}, 670 }, 671 wantUpdate: xdsresource.ClusterUpdate{ 672 ClusterName: "resource-name-1", 673 EDSServiceName: "eds-service-name", 674 LRSServerConfig: xdsresource.ClusterLRSServerSelf, 675 }, 676 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 677 "resource-name-1": { 678 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 679 Raw: testutils.MarshalAny(t, resource1), 680 }, 681 }, 682 }, 683 { 684 desc: "two-resources-when-we-requested-one", 685 resourceName: resourceName1, 686 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 687 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 688 VersionInfo: "1", 689 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)}, 690 }, 691 wantUpdate: xdsresource.ClusterUpdate{ 692 ClusterName: "resource-name-1", 693 EDSServiceName: "eds-service-name", 694 LRSServerConfig: xdsresource.ClusterLRSServerSelf, 695 }, 696 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 697 "resource-name-1": { 698 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 699 Raw: testutils.MarshalAny(t, resource1), 700 }, 701 }, 702 }, 703 } 704 705 for _, test := range tests { 706 t.Run(test.desc, func(t *testing.T) { 707 // Create a fake xDS management server listening on a local port, 708 // and set it up with the response to send. 709 mgmtServer, cleanup := startFakeManagementServer(t) 710 defer cleanup() 711 t.Logf("Started xDS management server on %s", mgmtServer.Address) 712 713 // Create an xDS client talking to the above management server. 714 nodeID := uuid.New().String() 715 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 716 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 717 NodeProto: &v3corepb.Node{Id: nodeID}, 718 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 719 if err != nil { 720 t.Fatalf("failed to create xds client: %v", err) 721 } 722 defer close() 723 t.Logf("Created xDS client to %s", mgmtServer.Address) 724 725 // Register a watch, and push the results on to a channel. 726 cw := newClusterWatcher() 727 cancel := xdsresource.WatchCluster(client, test.resourceName, cw) 728 defer cancel() 729 t.Logf("Registered a watch for Cluster %q", test.resourceName) 730 731 // Wait for the discovery request to be sent out. 732 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 733 defer cancel() 734 val, err := mgmtServer.XDSRequestChan.Receive(ctx) 735 if err != nil { 736 t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx) 737 } 738 wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{ 739 Node: &v3corepb.Node{Id: nodeID}, 740 ResourceNames: []string{test.resourceName}, 741 TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster", 742 }} 743 gotReq := val.(*fakeserver.Request) 744 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" { 745 t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq) 746 } 747 t.Logf("Discovery request received at management server") 748 749 // Configure the fake management server with a response. 750 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 751 752 // Wait for an update from the xDS client and compare with expected 753 // update. 754 val, err = cw.updateCh.Receive(ctx) 755 if err != nil { 756 t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err) 757 } 758 gotUpdate := val.(clusterUpdateErrTuple).update 759 gotErr := val.(clusterUpdateErrTuple).err 760 if (gotErr != nil) != (test.wantErr != "") { 761 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 762 } 763 if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) { 764 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 765 } 766 cmpOpts := []cmp.Option{ 767 cmpopts.EquateEmpty(), 768 cmpopts.IgnoreFields(xdsresource.ClusterUpdate{}, "Raw", "LBPolicy"), 769 } 770 if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" { 771 t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff) 772 } 773 if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD { 774 dump := client.DumpResources() 775 return dump["type.googleapis.com/envoy.config.cluster.v3.Cluster"] 776 }, test.wantUpdateMetadata); err != nil { 777 t.Fatal(err) 778 } 779 }) 780 } 781 } 782 783 // TestHandleEndpointsResponseFromManagementServer covers different scenarios 784 // involving receipt of a CDS response from the management server. The test 785 // verifies that the internal state of the xDS client (parsed resource and 786 // metadata) matches expectations. 787 func (s) TestHandleEndpointsResponseFromManagementServer(t *testing.T) { 788 const ( 789 resourceName1 = "resource-name-1" 790 resourceName2 = "resource-name-2" 791 ) 792 resource1 := &v3endpointpb.ClusterLoadAssignment{ 793 ClusterName: resourceName1, 794 Endpoints: []*v3endpointpb.LocalityLbEndpoints{ 795 { 796 Locality: &v3corepb.Locality{SubZone: "locality-1"}, 797 LbEndpoints: []*v3endpointpb.LbEndpoint{ 798 { 799 HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{ 800 Endpoint: &v3endpointpb.Endpoint{ 801 Address: &v3corepb.Address{ 802 Address: &v3corepb.Address_SocketAddress{ 803 SocketAddress: &v3corepb.SocketAddress{ 804 Protocol: v3corepb.SocketAddress_TCP, 805 Address: "addr1", 806 PortSpecifier: &v3corepb.SocketAddress_PortValue{ 807 PortValue: uint32(314), 808 }, 809 }, 810 }, 811 }, 812 }, 813 }, 814 }, 815 }, 816 LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1}, 817 Priority: 1, 818 }, 819 { 820 Locality: &v3corepb.Locality{SubZone: "locality-2"}, 821 LbEndpoints: []*v3endpointpb.LbEndpoint{ 822 { 823 HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{ 824 Endpoint: &v3endpointpb.Endpoint{ 825 Address: &v3corepb.Address{ 826 Address: &v3corepb.Address_SocketAddress{ 827 SocketAddress: &v3corepb.SocketAddress{ 828 Protocol: v3corepb.SocketAddress_TCP, 829 Address: "addr2", 830 PortSpecifier: &v3corepb.SocketAddress_PortValue{ 831 PortValue: uint32(159), 832 }, 833 }, 834 }, 835 }, 836 }, 837 }, 838 }, 839 }, 840 LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1}, 841 Priority: 0, 842 }, 843 }, 844 } 845 resource2 := proto.Clone(resource1).(*v3endpointpb.ClusterLoadAssignment) 846 resource2.ClusterName = resourceName2 847 848 tests := []struct { 849 desc string 850 resourceName string 851 managementServerResponse *v3discoverypb.DiscoveryResponse 852 wantUpdate xdsresource.EndpointsUpdate 853 wantErr string 854 wantUpdateMetadata map[string]xdsresource.UpdateWithMD 855 }{ 856 // The first three tests involve scenarios where the response fails 857 // protobuf deserialization (because it contains an invalid data or type 858 // in the anypb.Any) or the requested resource is not present in the 859 // response. In either case, no resource update makes its way to the 860 // top-level xDS client. An EDS response without a requested resource 861 // does not mean that the resource does not exist in the server. It 862 // could be part of a future update. Therefore, the only failure mode 863 // for this resource is for the watch to timeout. 864 { 865 desc: "badly-marshaled-response", 866 resourceName: resourceName1, 867 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 868 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 869 VersionInfo: "1", 870 Resources: []*anypb.Any{{ 871 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 872 Value: []byte{1, 2, 3, 4}, 873 }}, 874 }, 875 wantErr: "Endpoints not found in received response", 876 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 877 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 878 }, 879 }, 880 { 881 desc: "empty-response", 882 resourceName: resourceName1, 883 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 884 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 885 VersionInfo: "1", 886 }, 887 wantErr: "Endpoints not found in received response", 888 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 889 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 890 }, 891 }, 892 { 893 desc: "unexpected-type-in-response", 894 resourceName: resourceName1, 895 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 896 TypeUrl: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", 897 VersionInfo: "1", 898 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3listenerpb.Listener{})}, 899 }, 900 wantErr: "Endpoints not found in received response", 901 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 902 "resource-name-1": {MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusNotExist}}, 903 }, 904 }, 905 { 906 desc: "one-bad-resource", 907 resourceName: resourceName1, 908 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 909 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 910 VersionInfo: "1", 911 Resources: []*anypb.Any{testutils.MarshalAny(t, &v3endpointpb.ClusterLoadAssignment{ 912 ClusterName: resourceName1, 913 Endpoints: []*v3endpointpb.LocalityLbEndpoints{ 914 { 915 Locality: &v3corepb.Locality{SubZone: "locality-1"}, 916 LbEndpoints: []*v3endpointpb.LbEndpoint{ 917 { 918 HostIdentifier: &v3endpointpb.LbEndpoint_Endpoint{ 919 Endpoint: &v3endpointpb.Endpoint{ 920 Address: &v3corepb.Address{ 921 Address: &v3corepb.Address_SocketAddress{ 922 SocketAddress: &v3corepb.SocketAddress{ 923 Protocol: v3corepb.SocketAddress_TCP, 924 Address: "addr1", 925 PortSpecifier: &v3corepb.SocketAddress_PortValue{ 926 PortValue: uint32(314), 927 }, 928 }, 929 }, 930 }, 931 }, 932 }, 933 LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 0}, 934 }, 935 }, 936 LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1}, 937 Priority: 1, 938 }, 939 }, 940 }), 941 }, 942 }, 943 wantErr: "EDS response contains an endpoint with zero weight", 944 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 945 "resource-name-1": {MD: xdsresource.UpdateMetadata{ 946 Status: xdsresource.ServiceStatusNACKed, 947 ErrState: &xdsresource.UpdateErrorMetadata{ 948 Version: "1", 949 Err: cmpopts.AnyError, 950 }, 951 }}, 952 }, 953 }, 954 { 955 desc: "one-good-resource", 956 resourceName: resourceName1, 957 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 958 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 959 VersionInfo: "1", 960 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1)}, 961 }, 962 wantUpdate: xdsresource.EndpointsUpdate{ 963 Localities: []xdsresource.Locality{ 964 { 965 Endpoints: []xdsresource.Endpoint{{Address: "addr1:314", Weight: 1}}, 966 ID: internal.LocalityID{SubZone: "locality-1"}, 967 Priority: 1, 968 Weight: 1, 969 }, 970 { 971 Endpoints: []xdsresource.Endpoint{{Address: "addr2:159", Weight: 1}}, 972 ID: internal.LocalityID{SubZone: "locality-2"}, 973 Priority: 0, 974 Weight: 1, 975 }, 976 }, 977 }, 978 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 979 "resource-name-1": { 980 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 981 Raw: testutils.MarshalAny(t, resource1), 982 }, 983 }, 984 }, 985 { 986 desc: "two-resources-when-we-requested-one", 987 resourceName: resourceName1, 988 managementServerResponse: &v3discoverypb.DiscoveryResponse{ 989 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 990 VersionInfo: "1", 991 Resources: []*anypb.Any{testutils.MarshalAny(t, resource1), testutils.MarshalAny(t, resource2)}, 992 }, 993 wantUpdate: xdsresource.EndpointsUpdate{ 994 Localities: []xdsresource.Locality{ 995 { 996 Endpoints: []xdsresource.Endpoint{{Address: "addr1:314", Weight: 1}}, 997 ID: internal.LocalityID{SubZone: "locality-1"}, 998 Priority: 1, 999 Weight: 1, 1000 }, 1001 { 1002 Endpoints: []xdsresource.Endpoint{{Address: "addr2:159", Weight: 1}}, 1003 ID: internal.LocalityID{SubZone: "locality-2"}, 1004 Priority: 0, 1005 Weight: 1, 1006 }, 1007 }, 1008 }, 1009 wantUpdateMetadata: map[string]xdsresource.UpdateWithMD{ 1010 "resource-name-1": { 1011 MD: xdsresource.UpdateMetadata{Status: xdsresource.ServiceStatusACKed, Version: "1"}, 1012 Raw: testutils.MarshalAny(t, resource1), 1013 }, 1014 }, 1015 }, 1016 } 1017 1018 for _, test := range tests { 1019 t.Run(test.desc, func(t *testing.T) { 1020 // Create a fake xDS management server listening on a local port, 1021 // and set it up with the response to send. 1022 mgmtServer, cleanup := startFakeManagementServer(t) 1023 defer cleanup() 1024 t.Logf("Started xDS management server on %s", mgmtServer.Address) 1025 1026 // Create an xDS client talking to the above management server. 1027 nodeID := uuid.New().String() 1028 client, close, err := xdsclient.NewWithConfigForTesting(&bootstrap.Config{ 1029 XDSServer: xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 1030 NodeProto: &v3corepb.Node{Id: nodeID}, 1031 }, defaultTestWatchExpiryTimeout, time.Duration(0)) 1032 if err != nil { 1033 t.Fatalf("failed to create xds client: %v", err) 1034 } 1035 defer close() 1036 t.Logf("Created xDS client to %s", mgmtServer.Address) 1037 1038 // Register a watch, and push the results on to a channel. 1039 ew := newEndpointsWatcher() 1040 cancel := xdsresource.WatchEndpoints(client, test.resourceName, ew) 1041 defer cancel() 1042 t.Logf("Registered a watch for Endpoint %q", test.resourceName) 1043 1044 // Wait for the discovery request to be sent out. 1045 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 1046 defer cancel() 1047 val, err := mgmtServer.XDSRequestChan.Receive(ctx) 1048 if err != nil { 1049 t.Fatalf("Timeout when waiting for discovery request at the management server: %v", ctx) 1050 } 1051 wantReq := &fakeserver.Request{Req: &v3discoverypb.DiscoveryRequest{ 1052 Node: &v3corepb.Node{Id: nodeID}, 1053 ResourceNames: []string{test.resourceName}, 1054 TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", 1055 }} 1056 gotReq := val.(*fakeserver.Request) 1057 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform()); diff != "" { 1058 t.Fatalf("Discovery request received at management server is %+v, want %+v", gotReq, wantReq) 1059 } 1060 t.Logf("Discovery request received at management server") 1061 1062 // Configure the fake management server with a response. 1063 mgmtServer.XDSResponseChan <- &fakeserver.Response{Resp: test.managementServerResponse} 1064 1065 // Wait for an update from the xDS client and compare with expected 1066 // update. 1067 val, err = ew.updateCh.Receive(ctx) 1068 if err != nil { 1069 t.Fatalf("Timeout when waiting for watch callback to invoked after response from management server: %v", err) 1070 } 1071 gotUpdate := val.(endpointsUpdateErrTuple).update 1072 gotErr := val.(endpointsUpdateErrTuple).err 1073 if (gotErr != nil) != (test.wantErr != "") { 1074 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 1075 } 1076 if gotErr != nil && !strings.Contains(gotErr.Error(), test.wantErr) { 1077 t.Fatalf("Got error from handling update: %v, want %v", gotErr, test.wantErr) 1078 } 1079 cmpOpts := []cmp.Option{ 1080 cmpopts.EquateEmpty(), 1081 cmpopts.IgnoreFields(xdsresource.EndpointsUpdate{}, "Raw"), 1082 } 1083 if diff := cmp.Diff(test.wantUpdate, gotUpdate, cmpOpts...); diff != "" { 1084 t.Fatalf("Unexpected diff in metadata, diff (-want +got):\n%s", diff) 1085 } 1086 if err := compareUpdateMetadata(ctx, func() map[string]xdsresource.UpdateWithMD { 1087 dump := client.DumpResources() 1088 return dump["type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment"] 1089 }, test.wantUpdateMetadata); err != nil { 1090 t.Fatal(err) 1091 } 1092 }) 1093 } 1094 }