google.golang.org/grpc@v1.72.2/xds/csds/csds_e2e_test.go (about) 1 /* 2 * 3 * Copyright 2021 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 csds_test 20 21 import ( 22 "context" 23 "fmt" 24 "io" 25 "slices" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/google/go-cmp/cmp" 31 "github.com/google/uuid" 32 "google.golang.org/grpc" 33 "google.golang.org/grpc/credentials/insecure" 34 "google.golang.org/grpc/internal/grpctest" 35 "google.golang.org/grpc/internal/pretty" 36 "google.golang.org/grpc/internal/testutils" 37 "google.golang.org/grpc/internal/testutils/xds/e2e" 38 "google.golang.org/grpc/internal/xds/bootstrap" 39 "google.golang.org/grpc/xds/csds" 40 "google.golang.org/grpc/xds/internal/xdsclient" 41 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource" 42 "google.golang.org/protobuf/encoding/prototext" 43 "google.golang.org/protobuf/testing/protocmp" 44 "google.golang.org/protobuf/types/known/anypb" 45 46 v3adminpb "github.com/envoyproxy/go-control-plane/envoy/admin/v3" 47 v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 48 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 49 v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" 50 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 51 v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 52 v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 53 v3statuspbgrpc "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 54 55 _ "google.golang.org/grpc/xds/internal/httpfilter/router" // Register the router filter 56 ) 57 58 const defaultTestTimeout = 5 * time.Second 59 60 type s struct { 61 grpctest.Tester 62 } 63 64 func Test(t *testing.T) { 65 grpctest.RunSubTests(t, s{}) 66 } 67 68 // The following watcher implementations are no-ops since we don't really care 69 // about the callback received by these watchers in the test. We only care 70 // whether CSDS reports the expected state. 71 72 type nopListenerWatcher struct{} 73 74 func (nopListenerWatcher) OnUpdate(_ *xdsresource.ListenerResourceData, onDone xdsresource.OnDoneFunc) { 75 onDone() 76 } 77 func (nopListenerWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) { 78 onDone() 79 } 80 func (nopListenerWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 81 onDone() 82 } 83 84 type nopRouteConfigWatcher struct{} 85 86 func (nopRouteConfigWatcher) OnUpdate(_ *xdsresource.RouteConfigResourceData, onDone xdsresource.OnDoneFunc) { 87 onDone() 88 } 89 func (nopRouteConfigWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) { 90 onDone() 91 } 92 func (nopRouteConfigWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 93 onDone() 94 } 95 96 type nopClusterWatcher struct{} 97 98 func (nopClusterWatcher) OnUpdate(_ *xdsresource.ClusterResourceData, onDone xdsresource.OnDoneFunc) { 99 onDone() 100 } 101 func (nopClusterWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) { 102 onDone() 103 } 104 func (nopClusterWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 105 onDone() 106 } 107 108 type nopEndpointsWatcher struct{} 109 110 func (nopEndpointsWatcher) OnUpdate(_ *xdsresource.EndpointsResourceData, onDone xdsresource.OnDoneFunc) { 111 onDone() 112 } 113 func (nopEndpointsWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) { 114 onDone() 115 } 116 func (nopEndpointsWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 117 onDone() 118 } 119 120 // This watcher writes the onDone callback on to a channel for the test to 121 // invoke it when it wants to unblock the next read on the ADS stream in the xDS 122 // client. This is particularly useful when a resource is NACKed, because the 123 // go-control-plane management server continuously resends the same resource in 124 // this case, and applying flow control from these watchers ensures that xDS 125 // client does not spend all of its time receiving and NACKing updates from the 126 // management server. This was indeed the case on arm64 (before we had support 127 // for ADS stream level flow control), and was causing CSDS to not receive any 128 // updates from the xDS client. 129 type blockingListenerWatcher struct { 130 testCtxDone <-chan struct{} // Closed when the test is done. 131 onDoneCh chan xdsresource.OnDoneFunc // Channel to write the onDone callback to. 132 } 133 134 func newBlockingListenerWatcher(testCtxDone <-chan struct{}) *blockingListenerWatcher { 135 return &blockingListenerWatcher{ 136 testCtxDone: testCtxDone, 137 onDoneCh: make(chan xdsresource.OnDoneFunc, 1), 138 } 139 } 140 141 func (w *blockingListenerWatcher) OnUpdate(_ *xdsresource.ListenerResourceData, onDone xdsresource.OnDoneFunc) { 142 writeOnDone(w.testCtxDone, w.onDoneCh, onDone) 143 } 144 func (w *blockingListenerWatcher) OnError(_ error, onDone xdsresource.OnDoneFunc) { 145 writeOnDone(w.testCtxDone, w.onDoneCh, onDone) 146 } 147 func (w *blockingListenerWatcher) OnResourceDoesNotExist(onDone xdsresource.OnDoneFunc) { 148 writeOnDone(w.testCtxDone, w.onDoneCh, onDone) 149 } 150 151 // writeOnDone attempts to write the onDone callback on the onDone channel. It 152 // returns when it can successfully write to the channel or when the test is 153 // done, which is signalled by testCtxDone being closed. 154 func writeOnDone(testCtxDone <-chan struct{}, onDoneCh chan xdsresource.OnDoneFunc, onDone xdsresource.OnDoneFunc) { 155 select { 156 case <-testCtxDone: 157 case onDoneCh <- onDone: 158 } 159 } 160 161 // Creates a gRPC server and starts serving a CSDS service implementation on it. 162 // Returns the address of the newly created gRPC server. 163 // 164 // Registers cleanup functions on t to stop the gRPC server and the CSDS 165 // implementation. 166 func startCSDSServer(t *testing.T) string { 167 t.Helper() 168 169 server := grpc.NewServer() 170 t.Cleanup(server.Stop) 171 172 csdss, err := csds.NewClientStatusDiscoveryServer() 173 if err != nil { 174 t.Fatalf("Failed to create CSDS service implementation: %v", err) 175 } 176 v3statuspbgrpc.RegisterClientStatusDiscoveryServiceServer(server, csdss) 177 t.Cleanup(csdss.Close) 178 179 // Create a local listener and pass it to Serve(). 180 lis, err := testutils.LocalTCPListener() 181 if err != nil { 182 t.Fatalf("testutils.LocalTCPListener() failed: %v", err) 183 } 184 go func() { 185 if err := server.Serve(lis); err != nil { 186 t.Errorf("Serve() failed: %v", err) 187 } 188 }() 189 return lis.Addr().String() 190 } 191 192 func startCSDSClientStream(ctx context.Context, t *testing.T, serverAddr string) v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient { 193 conn, err := grpc.NewClient(serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) 194 if err != nil { 195 t.Fatalf("Failed to dial CSDS server %q: %v", serverAddr, err) 196 } 197 198 client := v3statuspbgrpc.NewClientStatusDiscoveryServiceClient(conn) 199 stream, err := client.StreamClientStatus(ctx, grpc.WaitForReady(true)) 200 if err != nil { 201 t.Fatalf("Failed to create a stream for CSDS: %v", err) 202 } 203 t.Cleanup(func() { conn.Close() }) 204 return stream 205 } 206 207 // Tests CSDS functionality. The test performs the following: 208 // - Spins up a management server and creates two xDS clients talking to it. 209 // - Registers a set of watches on the xDS clients, and verifies that the CSDS 210 // response reports resources in REQUESTED state. 211 // - Configures resources on the management server corresponding to the ones 212 // being watched by the clients, and verifies that the CSDS response reports 213 // resources in ACKED state. 214 // 215 // For the above operations, the test also verifies that the client_scope field 216 // in the CSDS response is populated appropriately. 217 func (s) TestCSDS(t *testing.T) { 218 // Spin up a xDS management server on a local port. 219 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) 220 221 // Create a bootstrap contents pointing to the above management server. 222 nodeID := uuid.New().String() 223 bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 224 config, err := bootstrap.NewConfigFromContents(bootstrapContents) 225 if err != nil { 226 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err) 227 } 228 // We use the default xDS client pool here because the CSDS service reports 229 // on the state of the default xDS client which is implicitly managed 230 // within the xdsclient.DefaultPool. 231 xdsclient.DefaultPool.SetFallbackBootstrapConfig(config) 232 defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }() 233 // Create two xDS clients, with different names. These should end up 234 // creating two different xDS clients. 235 const xdsClient1Name = "xds-csds-client-1" 236 xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{ 237 Name: xdsClient1Name, 238 }) 239 if err != nil { 240 t.Fatalf("Failed to create xDS client: %v", err) 241 } 242 defer xdsClose1() 243 const xdsClient2Name = "xds-csds-client-2" 244 xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{ 245 Name: xdsClient2Name, 246 }) 247 if err != nil { 248 t.Fatalf("Failed to create xDS client: %v", err) 249 } 250 defer xdsClose2() 251 252 // Start a CSDS server and create a client stream to it. 253 addr := startCSDSServer(t) 254 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 255 defer cancel() 256 stream := startCSDSClientStream(ctx, t, addr) 257 258 // Verify that the xDS client reports an empty config. 259 wantNode := &v3corepb.Node{ 260 Id: nodeID, 261 UserAgentName: "gRPC Go", 262 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 263 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 264 } 265 wantResp := &v3statuspb.ClientStatusResponse{ 266 Config: []*v3statuspb.ClientConfig{ 267 { 268 Node: wantNode, 269 ClientScope: xdsClient1Name, 270 }, 271 { 272 Node: wantNode, 273 ClientScope: xdsClient2Name, 274 }, 275 }, 276 } 277 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 278 t.Fatal(err) 279 } 280 281 // Initialize the xDS resources to be used in this test. 282 ldsTargets := []string{"lds.target.good:0000", "lds.target.good:1111"} 283 rdsTargets := []string{"route-config-0", "route-config-1"} 284 cdsTargets := []string{"cluster-0", "cluster-1"} 285 edsTargets := []string{"endpoints-0", "endpoints-1"} 286 listeners := make([]*v3listenerpb.Listener, len(ldsTargets)) 287 listenerAnys := make([]*anypb.Any, len(ldsTargets)) 288 for i := range ldsTargets { 289 listeners[i] = e2e.DefaultClientListener(ldsTargets[i], rdsTargets[i]) 290 listenerAnys[i] = testutils.MarshalAny(t, listeners[i]) 291 } 292 routes := make([]*v3routepb.RouteConfiguration, len(rdsTargets)) 293 routeAnys := make([]*anypb.Any, len(rdsTargets)) 294 for i := range rdsTargets { 295 routes[i] = e2e.DefaultRouteConfig(rdsTargets[i], ldsTargets[i], cdsTargets[i]) 296 routeAnys[i] = testutils.MarshalAny(t, routes[i]) 297 } 298 clusters := make([]*v3clusterpb.Cluster, len(cdsTargets)) 299 clusterAnys := make([]*anypb.Any, len(cdsTargets)) 300 for i := range cdsTargets { 301 clusters[i] = e2e.DefaultCluster(cdsTargets[i], edsTargets[i], e2e.SecurityLevelNone) 302 clusterAnys[i] = testutils.MarshalAny(t, clusters[i]) 303 } 304 endpoints := make([]*v3endpointpb.ClusterLoadAssignment, len(edsTargets)) 305 endpointAnys := make([]*anypb.Any, len(edsTargets)) 306 ips := []string{"0.0.0.0", "1.1.1.1"} 307 ports := []uint32{123, 456} 308 for i := range edsTargets { 309 endpoints[i] = e2e.DefaultEndpoint(edsTargets[i], ips[i], ports[i:i+1]) 310 endpointAnys[i] = testutils.MarshalAny(t, endpoints[i]) 311 } 312 313 // Register watches on the xDS clients for two resources of each type. 314 for _, xdsC := range []xdsclient.XDSClient{xdsClient1, xdsClient2} { 315 for _, target := range ldsTargets { 316 xdsresource.WatchListener(xdsC, target, nopListenerWatcher{}) 317 } 318 for _, target := range rdsTargets { 319 xdsresource.WatchRouteConfig(xdsC, target, nopRouteConfigWatcher{}) 320 } 321 for _, target := range cdsTargets { 322 xdsresource.WatchCluster(xdsC, target, nopClusterWatcher{}) 323 } 324 for _, target := range edsTargets { 325 xdsresource.WatchEndpoints(xdsC, target, nopEndpointsWatcher{}) 326 } 327 } 328 329 // Verify that the xDS client reports the resources as being in "Requested" 330 // state, and in version "0". 331 wantConfigs := []*v3statuspb.ClientConfig_GenericXdsConfig{ 332 makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 333 makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 334 makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 335 makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 336 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 337 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 338 makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 339 makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 340 } 341 wantResp = &v3statuspb.ClientStatusResponse{ 342 Config: []*v3statuspb.ClientConfig{ 343 { 344 Node: wantNode, 345 GenericXdsConfigs: wantConfigs, 346 ClientScope: xdsClient1Name, 347 }, 348 { 349 Node: wantNode, 350 GenericXdsConfigs: wantConfigs, 351 ClientScope: xdsClient2Name, 352 }, 353 }, 354 } 355 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 356 t.Fatal(err) 357 } 358 359 // Configure the management server with two resources of each type, 360 // corresponding to the watches registered above. 361 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{ 362 NodeID: nodeID, 363 Listeners: listeners, 364 Routes: routes, 365 Clusters: clusters, 366 Endpoints: endpoints, 367 }); err != nil { 368 t.Fatal(err) 369 } 370 371 // Verify that the xDS client reports the resources as being in "ACKed" 372 // state, and in version "1". 373 wantConfigs = []*v3statuspb.ClientConfig_GenericXdsConfig{ 374 makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[0], nil), 375 makeGenericXdsConfig("type.googleapis.com/envoy.config.cluster.v3.Cluster", cdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, clusterAnys[1], nil), 376 makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[0], nil), 377 makeGenericXdsConfig("type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment", edsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, endpointAnys[1], nil), 378 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[0], nil), 379 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, listenerAnys[1], nil), 380 makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[0], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[0], nil), 381 makeGenericXdsConfig("type.googleapis.com/envoy.config.route.v3.RouteConfiguration", rdsTargets[1], "1", v3adminpb.ClientResourceStatus_ACKED, routeAnys[1], nil), 382 } 383 wantResp = &v3statuspb.ClientStatusResponse{ 384 Config: []*v3statuspb.ClientConfig{ 385 { 386 Node: wantNode, 387 GenericXdsConfigs: wantConfigs, 388 ClientScope: xdsClient1Name, 389 }, 390 { 391 Node: wantNode, 392 GenericXdsConfigs: wantConfigs, 393 ClientScope: xdsClient2Name, 394 }, 395 }, 396 } 397 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 398 t.Fatal(err) 399 } 400 } 401 402 // Tests CSDS functionality. The test performs the following: 403 // - Spins up a management server and creates two xDS clients talking to it. 404 // - Registers one watch on each xDS client, and verifies that the CSDS 405 // response reports resources in REQUESTED state. 406 // - Configures two resources on the management server and verifies that the 407 // CSDS response reports the resources as being in ACKED state. 408 // - Updates one of two resources on the management server such that it is 409 // expected to be NACKed by the client. Verifies that the CSDS response 410 // contains one resource in ACKED state and one in NACKED state. 411 // 412 // For the above operations, the test also verifies that the client_scope field 413 // in the CSDS response is populated appropriately. 414 // 415 // This test does a bunch of similar things to the previous test, but has 416 // reduced complexity because of having to deal with a single resource type. 417 // This makes it possible to test the NACKing a resource (which results in 418 // continuous resending of the resource by the go-control-plane management 419 // server), in an easier and less flaky way. 420 func (s) TestCSDS_NACK(t *testing.T) { 421 // Spin up a xDS management server on a local port. 422 mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) 423 424 // Create a bootstrap contents pointing to the above management server. 425 nodeID := uuid.New().String() 426 bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address) 427 config, err := bootstrap.NewConfigFromContents(bootstrapContents) 428 if err != nil { 429 t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err) 430 } 431 // We use the default xDS client pool here because the CSDS service reports 432 // on the state of the default xDS client which is implicitly managed 433 // within the xdsclient.DefaultPool. 434 xdsclient.DefaultPool.SetFallbackBootstrapConfig(config) 435 defer func() { xdsclient.DefaultPool.UnsetBootstrapConfigForTesting() }() 436 // Create two xDS clients, with different names. These should end up 437 // creating two different xDS clients. 438 const xdsClient1Name = "xds-csds-client-1" 439 xdsClient1, xdsClose1, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{ 440 Name: xdsClient1Name, 441 }) 442 if err != nil { 443 t.Fatalf("Failed to create xDS client: %v", err) 444 } 445 defer xdsClose1() 446 const xdsClient2Name = "xds-csds-client-2" 447 xdsClient2, xdsClose2, err := xdsclient.DefaultPool.NewClientForTesting(xdsclient.OptionsForTesting{ 448 Name: xdsClient2Name, 449 }) 450 if err != nil { 451 t.Fatalf("Failed to create xDS client: %v", err) 452 } 453 defer xdsClose2() 454 455 // Start a CSDS server and create a client stream to it. 456 addr := startCSDSServer(t) 457 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 458 defer cancel() 459 stream := startCSDSClientStream(ctx, t, addr) 460 461 // Verify that the xDS client reports an empty config. 462 wantNode := &v3corepb.Node{ 463 Id: nodeID, 464 UserAgentName: "gRPC Go", 465 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 466 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 467 } 468 wantResp := &v3statuspb.ClientStatusResponse{ 469 Config: []*v3statuspb.ClientConfig{ 470 { 471 Node: wantNode, 472 ClientScope: xdsClient1Name, 473 }, 474 { 475 Node: wantNode, 476 ClientScope: xdsClient2Name, 477 }, 478 }, 479 } 480 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 481 t.Fatal(err) 482 } 483 484 // Initialize the xDS resources to be used in this test. 485 const ldsTarget0, ldsTarget1 = "lds.target.good:0000", "lds.target.good:1111" 486 listener0 := e2e.DefaultClientListener(ldsTarget0, "rds-name") 487 listener1 := e2e.DefaultClientListener(ldsTarget1, "rds-name") 488 listenerAny0 := testutils.MarshalAny(t, listener0) 489 listenerAny1 := testutils.MarshalAny(t, listener1) 490 491 // Register the watchers, one for each xDS client. 492 watcher1 := nopListenerWatcher{} 493 watcher2 := newBlockingListenerWatcher(ctx.Done()) 494 xdsresource.WatchListener(xdsClient1, ldsTarget0, watcher1) 495 xdsresource.WatchListener(xdsClient2, ldsTarget1, watcher2) 496 497 // Verify that the xDS client reports the resources as being in "Requested" 498 // state, and in version "0". 499 wantResp = &v3statuspb.ClientStatusResponse{ 500 Config: []*v3statuspb.ClientConfig{ 501 { 502 Node: wantNode, 503 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 504 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 505 }, 506 ClientScope: xdsClient1Name, 507 }, 508 { 509 Node: wantNode, 510 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 511 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "", v3adminpb.ClientResourceStatus_REQUESTED, nil, nil), 512 }, 513 ClientScope: xdsClient2Name, 514 }, 515 }, 516 } 517 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 518 t.Fatal(err) 519 } 520 521 // Configure the management server with two listener resources corresponding 522 // to the watches registered above. 523 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{ 524 NodeID: nodeID, 525 Listeners: []*v3listenerpb.Listener{listener0, listener1}, 526 SkipValidation: true, 527 }); err != nil { 528 t.Fatal(err) 529 } 530 531 // Verify that the xDS client reports the resources as being in "ACKed" 532 // state, and in version "1". 533 wantResp = &v3statuspb.ClientStatusResponse{ 534 Config: []*v3statuspb.ClientConfig{ 535 { 536 Node: wantNode, 537 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 538 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil), 539 }, 540 ClientScope: xdsClient1Name, 541 }, 542 { 543 Node: wantNode, 544 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 545 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_ACKED, listenerAny1, nil), 546 }, 547 ClientScope: xdsClient2Name, 548 }, 549 }, 550 } 551 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 552 t.Fatal(err) 553 } 554 555 // Unblock reads on the ADS stream by calling the onDone callback sent to 556 // the watcher. 557 select { 558 case <-ctx.Done(): 559 t.Fatal("Timed out waiting for watch callback") 560 case onDone := <-watcher2.onDoneCh: 561 onDone() 562 } 563 564 // Update the second resource with an empty ApiListener field which is 565 // expected to be NACK'ed by the xDS client. 566 listener1.ApiListener = nil 567 if err := mgmtServer.Update(ctx, e2e.UpdateOptions{ 568 NodeID: nodeID, 569 Listeners: []*v3listenerpb.Listener{listener0, listener1}, 570 SkipValidation: true, 571 }); err != nil { 572 t.Fatal(err) 573 } 574 575 // Wait for the update to reach the watchers. 576 select { 577 case <-ctx.Done(): 578 t.Fatal("Timed out waiting for watch callback") 579 case onDone := <-watcher2.onDoneCh: 580 onDone() 581 } 582 583 // Verify that the xDS client reports the first listener resource as being 584 // ACKed and the second listener resource as being NACKed. The version for 585 // the ACKed resource would be "2", while that for the NACKed resource would 586 // be "1". In the NACKed resource, the version which is NACKed is stored in 587 // the ErrorState field. 588 wantResp = &v3statuspb.ClientStatusResponse{ 589 Config: []*v3statuspb.ClientConfig{ 590 { 591 Node: wantNode, 592 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 593 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget0, "2", v3adminpb.ClientResourceStatus_ACKED, listenerAny0, nil), 594 }, 595 ClientScope: xdsClient1Name, 596 }, 597 { 598 Node: wantNode, 599 GenericXdsConfigs: []*v3statuspb.ClientConfig_GenericXdsConfig{ 600 makeGenericXdsConfig("type.googleapis.com/envoy.config.listener.v3.Listener", ldsTarget1, "1", v3adminpb.ClientResourceStatus_NACKED, listenerAny1, &v3adminpb.UpdateFailureState{VersionInfo: "2"}), 601 }, 602 ClientScope: xdsClient2Name, 603 }, 604 }, 605 } 606 if err := checkClientStatusResponse(ctx, stream, wantResp); err != nil { 607 t.Fatal(err) 608 } 609 } 610 611 func makeGenericXdsConfig(typeURL, name, version string, status v3adminpb.ClientResourceStatus, config *anypb.Any, failure *v3adminpb.UpdateFailureState) *v3statuspb.ClientConfig_GenericXdsConfig { 612 return &v3statuspb.ClientConfig_GenericXdsConfig{ 613 TypeUrl: typeURL, 614 Name: name, 615 VersionInfo: version, 616 ClientStatus: status, 617 XdsConfig: config, 618 ErrorState: failure, 619 } 620 } 621 622 // Repeatedly sends CSDS requests and receives CSDS responses on the provided 623 // stream and verifies that the response matches `want`. Returns an error if 624 // sending or receiving on the stream fails, or if the context expires before a 625 // response matching `want` is received. 626 // 627 // Expects client configs in `want` to be sorted on `client_scope` and the 628 // resource dump to be sorted on type_url and resource name. 629 func checkClientStatusResponse(ctx context.Context, stream v3statuspbgrpc.ClientStatusDiscoveryService_StreamClientStatusClient, want *v3statuspb.ClientStatusResponse) error { 630 var cmpOpts = cmp.Options{ 631 protocmp.Transform(), 632 protocmp.IgnoreFields((*v3statuspb.ClientConfig_GenericXdsConfig)(nil), "last_updated"), 633 protocmp.IgnoreFields((*v3adminpb.UpdateFailureState)(nil), "last_update_attempt", "details"), 634 } 635 636 var lastErr error 637 for ; ctx.Err() == nil; <-time.After(100 * time.Millisecond) { 638 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 639 if err != io.EOF { 640 return fmt.Errorf("failed to send ClientStatusRequest: %v", err) 641 } 642 // If the stream has closed, we call Recv() until it returns a non-nil 643 // error to get the actual error on the stream. 644 for { 645 if _, err := stream.Recv(); err != nil { 646 return fmt.Errorf("failed to recv ClientStatusResponse: %v", err) 647 } 648 } 649 } 650 got, err := stream.Recv() 651 if err != nil { 652 return fmt.Errorf("failed to recv ClientStatusResponse: %v", err) 653 } 654 // Sort the client configs based on the `client_scope` field. 655 slices.SortFunc(got.GetConfig(), func(a, b *v3statuspb.ClientConfig) int { 656 return strings.Compare(a.ClientScope, b.ClientScope) 657 }) 658 // Sort the resource configs based on the type_url and name fields. 659 for _, cc := range got.GetConfig() { 660 slices.SortFunc(cc.GetGenericXdsConfigs(), func(a, b *v3statuspb.ClientConfig_GenericXdsConfig) int { 661 if strings.Compare(a.TypeUrl, b.TypeUrl) == 0 { 662 return strings.Compare(a.Name, b.Name) 663 } 664 return strings.Compare(a.TypeUrl, b.TypeUrl) 665 }) 666 } 667 diff := cmp.Diff(want, got, cmpOpts) 668 if diff == "" { 669 return nil 670 } 671 lastErr = fmt.Errorf("received unexpected resource dump, diff (-got, +want):\n%s, got: %s\n want:%s", diff, pretty.ToJSON(got), pretty.ToJSON(want)) 672 } 673 return fmt.Errorf("timeout when waiting for resource dump to reach expected state: ctxErr: %v, otherErr: %v", ctx.Err(), lastErr) 674 } 675 676 func (s) TestCSDSNoXDSClient(t *testing.T) { 677 // Create a bootstrap file in a temporary directory. Since we pass an empty 678 // bootstrap configuration, it will fail xDS client creation because the 679 // `server_uri` field is unset. 680 testutils.CreateBootstrapFileForTesting(t, []byte(``)) 681 682 // Start a CSDS server and create a client stream to it. 683 addr := startCSDSServer(t) 684 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 685 defer cancel() 686 stream := startCSDSClientStream(ctx, t, addr) 687 688 if err := stream.Send(&v3statuspb.ClientStatusRequest{Node: nil}); err != nil { 689 t.Fatalf("Failed to send ClientStatusRequest: %v", err) 690 } 691 r, err := stream.Recv() 692 if err != nil { 693 // io.EOF is not ok. 694 t.Fatalf("Failed to recv ClientStatusResponse: %v", err) 695 } 696 if n := len(r.Config); n != 0 { 697 t.Fatalf("got %d configs, want 0: %v", n, prototext.Format(r)) 698 } 699 }