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