google.golang.org/grpc@v1.74.2/xds/internal/clients/xdsclient/test/dump_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 "slices" 25 "strings" 26 "testing" 27 "time" 28 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/uuid" 31 "google.golang.org/grpc/credentials/insecure" 32 "google.golang.org/grpc/xds/internal/clients" 33 "google.golang.org/grpc/xds/internal/clients/grpctransport" 34 "google.golang.org/grpc/xds/internal/clients/internal/pretty" 35 "google.golang.org/grpc/xds/internal/clients/internal/testutils" 36 "google.golang.org/grpc/xds/internal/clients/internal/testutils/e2e" 37 "google.golang.org/grpc/xds/internal/clients/xdsclient" 38 "google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource" 39 "google.golang.org/protobuf/proto" 40 "google.golang.org/protobuf/testing/protocmp" 41 "google.golang.org/protobuf/types/known/anypb" 42 43 v3adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3" 44 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 45 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 46 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 47 v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 48 ) 49 50 func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig { 51 return &v3statuspb.ClientConfig_GenericXdsConfig{ 52 TypeUrl: typeURL, 53 Name: name, 54 VersionInfo: version, 55 ClientStatus: status, 56 XdsConfig: config, 57 ErrorState: failure, 58 } 59 } 60 61 func checkResourceDump(ctx context.Context, want *v3statuspb.ClientStatusResponse, client *xdsclient.XDSClient) error { 62 var cmpOpts = cmp.Options{ 63 protocmp.Transform(), 64 protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"), 65 protocmp.IgnoreFields((*v3statuspb.ClientConfig)(nil), "client_scope"), 66 protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"), 67 } 68 69 var lastErr error 70 for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { 71 b, err := client.DumpResources() 72 if err != nil { 73 lastErr = err 74 continue 75 } 76 got := &v3statuspb.ClientStatusResponse{} 77 if err := proto.Unmarshal(b, got); err != nil { 78 lastErr = err 79 continue 80 } 81 // Sort the client configs based on the `client_scope` field. 82 slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int { 83 return strings.Compare(a.ClientScope, b.ClientScope) 84 }) 85 // Sort the resource configs based on the type_url and name fields. 86 for _, cc := range got.GetConfig() { 87 slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int { 88 if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 { 89 return strings.Compare(a.Name, b.Name) 90 } 91 return strings.Compare(a.TypeUrl, b.TypeUrl) 92 }) 93 } 94 diff := cmp.Diff(want, got, cmpOpts) 95 if diff == "" { 96 return nil 97 } 98 lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want)) 99 } 100 return fmt.Errorf("timeout when waiting for resource dump to reach expected state: %v", lastErr) 101 } 102 103 // Tests the scenario where there are multiple xDS clients talking to the same 104 // management server, and requesting the same set of resources. Verifies that 105 // under all circumstances, both xDS clients receive the same configuration from 106 // the server. 107 func (s) TestDumpResources_ManyToOne(t *testing.T) { 108 // Initialize the xDS resources to be used in this test. 109 ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"} 110 rdsTargets := []string{"route-config-0", "route-config-1"} 111 listeners := make([]*v3listenerpb.Listener, len(ldsTargets)) 112 listenerAnys := make([]*anypb.Any, len(ldsTargets)) 113 for i := range ldsTargets { 114 listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i]) 115 listenerAnys[i] = testutils.MarshalAny(t, listeners[i]) 116 } 117 118 // Spin up an xDS management server on a local port. 119 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 120 121 nodeID := uuid.New().String() 122 123 resourceTypes := map[string]xdsclient.ResourceType{xdsresource.V3ListenerURL: listenerType} 124 si := clients.ServerIdentifier{ 125 ServerURI: mgmtServer.Address, 126 Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}, 127 } 128 129 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 130 xdsClientConfig := xdsclient.Config{ 131 Servers: []xdsclient.ServerConfig{{ServerIdentifier: si}}, 132 Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, 133 TransportBuilder: grpctransport.NewBuilder(configs), 134 ResourceTypes: resourceTypes, 135 // Xdstp resource names used in this test do not specify an 136 // authority. These will end up looking up an entry with the 137 // empty key in the authorities map. Having an entry with an 138 // empty key and empty configuration, results in these 139 // resources also using the top-level configuration. 140 Authorities: map[string]xdsclient.Authority{ 141 "": {XDSServers: []xdsclient.ServerConfig{}}, 142 }, 143 } 144 145 // Create two xDS clients with the above config. 146 client1, err := xdsclient.New(xdsClientConfig) 147 if err != nil { 148 t.Fatalf("Failed to create xDS client: %v", err) 149 } 150 defer client1.Close() 151 client2, err := xdsclient.New(xdsClientConfig) 152 if err != nil { 153 t.Fatalf("Failed to create xDS client: %v", err) 154 } 155 defer client2.Close() 156 157 // Dump resources and expect empty configs. 158 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 159 defer cancel() 160 wantNode := &v3corepb.Node{ 161 Id: nodeID, 162 UserAgentName: "user-agent", 163 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, 164 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 165 } 166 wantResp := &v3statuspb.ClientStatusResponse{ 167 Config: []*v3statuspb.ClientConfig{ 168 { 169 Node: wantNode, 170 }, 171 }, 172 } 173 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 174 t.Fatal(err) 175 } 176 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 177 t.Fatal(err) 178 } 179 180 // Register watches, dump resources and expect configs in requested state. 181 for _, xdsC := range []*xdsclient.XDSClient{client1, client2} { 182 for _, target := range ldsTargets { 183 xdsC.WatchResource(xdsresource.V3ListenerURL, target, noopListenerWatcher{}) 184 } 185 } 186 wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{ 187 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 188 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 189 } 190 wantResp = &v3statuspb.ClientStatusResponse{ 191 Config: []*v3statuspb.ClientConfig{ 192 { 193 Node: wantNode, 194 GenericXdsConfigs: wantConfigs, 195 }, 196 }, 197 } 198 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 199 t.Fatal(err) 200 } 201 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 202 t.Fatal(err) 203 } 204 205 // Configure the resources on the management server. 206 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{ 207 NodeID: nodeID, 208 Listeners: listeners, 209 SkipValidation: true, 210 }); err != nil { 211 t.Fatal(err) 212 } 213 214 // Dump resources and expect ACK configs. 215 wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{ 216 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil), 217 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil), 218 } 219 wantResp = &v3statuspb.ClientStatusResponse{ 220 Config: []*v3statuspb.ClientConfig{ 221 { 222 Node: wantNode, 223 GenericXdsConfigs: wantConfigs, 224 }, 225 }, 226 } 227 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 228 t.Fatal(err) 229 } 230 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 231 t.Fatal(err) 232 } 233 234 // Update the first resource of each type in the management server to a 235 // value which is expected to be NACK'ed by the xDS client. 236 listeners[0] = func() *v3listenerpb.Listener { 237 hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{}) 238 return &v3listenerpb.Listener{ 239 Name: ldsTargets[0], 240 ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm}, 241 } 242 }() 243 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{ 244 NodeID: nodeID, 245 Listeners: listeners, 246 SkipValidation: true, 247 }); err != nil { 248 t.Fatal(err) 249 } 250 251 wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{ 252 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_NACKED, listenerAnys[0], &v3adminpb.UpdateFailureState{VersionInfo: "2"}), 253 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "2", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil), 254 } 255 wantResp = &v3statuspb.ClientStatusResponse{ 256 Config: []*v3statuspb.ClientConfig{ 257 { 258 Node: wantNode, 259 GenericXdsConfigs: wantConfigs, 260 }, 261 }, 262 } 263 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 264 t.Fatal(err) 265 } 266 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 267 t.Fatal(err) 268 } 269 } 270 271 // Tests the scenario where there are multiple xDS client talking to different 272 // management server, and requesting different set of resources. 273 func (s) TestDumpResources_ManyToMany(t *testing.T) { 274 // Initialize the xDS resources to be used in this test: 275 // - The first xDS client watches old style resource names, and thereby 276 // requests these resources from the top-level xDS server. 277 // - The second xDS client watches new style resource names with a non-empty 278 // authority, and thereby requests these resources from the server 279 // configuration for that authority. 280 authority := strings.Join(strings.Split(t.Name(), "/"), "") 281 ldsTargets := []string{ 282 "lds.target.good:0000", 283 fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/lds.targer.good:1111", authority), 284 } 285 rdsTargets := []string{ 286 "route-config-0", 287 fmt.Sprintf("xdstp://%s/envoy.config.route.v3.RouteConfiguration/route-config-1", authority), 288 } 289 listeners := make([]*v3listenerpb.Listener, len(ldsTargets)) 290 listenerAnys := make([]*anypb.Any, len(ldsTargets)) 291 for i := range ldsTargets { 292 listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i]) 293 listenerAnys[i] = testutils.MarshalAny(t, listeners[i]) 294 } 295 296 // Start two management servers. 297 mgmtServer1 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 298 mgmtServer2 := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 299 300 // The first of the above management servers will be the top-level xDS 301 // server in the bootstrap configuration, and the second will be the xDS 302 // server corresponding to the test authority. 303 nodeID := uuid.New().String() 304 305 resourceTypes := map[string]xdsclient.ResourceType{} 306 listenerType := listenerType 307 resourceTypes[xdsresource.V3ListenerURL] = listenerType 308 si1 := clients.ServerIdentifier{ 309 ServerURI: mgmtServer1.Address, 310 Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}, 311 } 312 si2 := clients.ServerIdentifier{ 313 ServerURI: mgmtServer2.Address, 314 Extensions: grpctransport.ServerIdentifierExtension{ConfigName: "insecure"}, 315 } 316 317 configs := map[string]grpctransport.Config{"insecure": {Credentials: insecure.NewBundle()}} 318 xdsClientConfig := xdsclient.Config{ 319 Servers: []xdsclient.ServerConfig{{ServerIdentifier: si1}}, 320 Node: clients.Node{ID: nodeID, UserAgentName: "user-agent", UserAgentVersion: "0.0.0.0"}, 321 TransportBuilder: grpctransport.NewBuilder(configs), 322 ResourceTypes: resourceTypes, 323 // Xdstp style resource names used in this test use a slash removed 324 // version of t.Name as their authority, and the empty config 325 // results in the top-level xds server configuration being used for 326 // this authority. 327 Authorities: map[string]xdsclient.Authority{ 328 authority: {XDSServers: []xdsclient.ServerConfig{{ServerIdentifier: si2}}}, 329 }, 330 } 331 332 // Create two xDS clients with the above config. 333 client1, err := xdsclient.New(xdsClientConfig) 334 if err != nil { 335 t.Fatalf("Failed to create xDS client: %v", err) 336 } 337 defer client1.Close() 338 client2, err := xdsclient.New(xdsClientConfig) 339 if err != nil { 340 t.Fatalf("Failed to create xDS client: %v", err) 341 } 342 defer client2.Close() 343 344 // Check the resource dump before configuring resources on the management server. 345 // Dump resources and expect empty configs. 346 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 347 defer cancel() 348 wantNode := &v3corepb.Node{ 349 Id: nodeID, 350 UserAgentName: "user-agent", 351 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: "0.0.0.0"}, 352 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 353 } 354 wantResp := &v3statuspb.ClientStatusResponse{ 355 Config: []*v3statuspb.ClientConfig{ 356 { 357 Node: wantNode, 358 }, 359 }, 360 } 361 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 362 t.Fatal(err) 363 } 364 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 365 t.Fatal(err) 366 } 367 368 // Register watches, the first xDS client watches old style resource names, 369 // while the second xDS client watches new style resource names. 370 client1.WatchResource(xdsresource.V3ListenerURL, ldsTargets[0], noopListenerWatcher{}) 371 client2.WatchResource(xdsresource.V3ListenerURL, ldsTargets[1], noopListenerWatcher{}) 372 373 // Check the resource dump. Both clients should have all resources in 374 // REQUESTED state. 375 wantConfigs1 := []*v3statuspb.ClientConfig_GenericXdsConfig{ 376 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 377 } 378 wantConfigs2 := []*v3statuspb.ClientConfig_GenericXdsConfig{ 379 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 380 } 381 wantResp = &v3statuspb.ClientStatusResponse{ 382 Config: []*v3statuspb.ClientConfig{ 383 { 384 Node: wantNode, 385 GenericXdsConfigs: wantConfigs1, 386 }, 387 }, 388 } 389 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 390 t.Fatal(err) 391 } 392 wantResp = &v3statuspb.ClientStatusResponse{ 393 Config: []*v3statuspb.ClientConfig{ 394 { 395 Node: wantNode, 396 GenericXdsConfigs: wantConfigs2, 397 }, 398 }, 399 } 400 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 401 t.Fatal(err) 402 } 403 404 // Configure resources on the first management server. 405 if err := mgmtServer1.Update(ctx, e2e.UpdateOptions{ 406 NodeID: nodeID, 407 Listeners: listeners[:1], 408 SkipValidation: true, 409 }); err != nil { 410 t.Fatal(err) 411 } 412 413 // Check the resource dump. One client should have resources in ACKED state, 414 // while the other should still have resources in REQUESTED state. 415 wantConfigs1 = []*v3statuspb.ClientConfig_GenericXdsConfig{ 416 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil), 417 } 418 wantResp = &v3statuspb.ClientStatusResponse{ 419 Config: []*v3statuspb.ClientConfig{ 420 { 421 Node: wantNode, 422 GenericXdsConfigs: wantConfigs1, 423 }, 424 }, 425 } 426 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 427 t.Fatal(err) 428 } 429 wantResp = &v3statuspb.ClientStatusResponse{ 430 Config: []*v3statuspb.ClientConfig{ 431 { 432 Node: wantNode, 433 GenericXdsConfigs: wantConfigs2, 434 }, 435 }, 436 } 437 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 438 t.Fatal(err) 439 } 440 441 // Configure resources on the second management server. 442 if err := mgmtServer2.Update(ctx, e2e.UpdateOptions{ 443 NodeID: nodeID, 444 Listeners: listeners[1:], 445 SkipValidation: true, 446 }); err != nil { 447 t.Fatal(err) 448 } 449 450 // Check the resource dump. Both clients should have appropriate resources 451 // in REQUESTED state. 452 wantConfigs2 = []*v3statuspb.ClientConfig_GenericXdsConfig{ 453 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil), 454 } 455 wantResp = &v3statuspb.ClientStatusResponse{ 456 Config: []*v3statuspb.ClientConfig{ 457 { 458 Node: wantNode, 459 GenericXdsConfigs: wantConfigs1, 460 }, 461 }, 462 } 463 if err := checkResourceDump(ctx, wantResp, client1); err != nil { 464 t.Fatal(err) 465 } 466 wantResp = &v3statuspb.ClientStatusResponse{ 467 Config: []*v3statuspb.ClientConfig{ 468 { 469 Node: wantNode, 470 GenericXdsConfigs: wantConfigs2, 471 }, 472 }, 473 } 474 if err := checkResourceDump(ctx, wantResp, client2); err != nil { 475 t.Fatal(err) 476 } 477 }