google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/transport/transport_ack_nack_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 package transport_test 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "testing" 25 26 "github.com/google/go-cmp/cmp" 27 "github.com/google/go-cmp/cmp/cmpopts" 28 "github.com/google/uuid" 29 "google.golang.org/grpc/codes" 30 "google.golang.org/grpc/internal/testutils" 31 "google.golang.org/grpc/internal/testutils/xds/e2e" 32 xdstestutils "google.golang.org/grpc/xds/internal/testutils" 33 "google.golang.org/grpc/xds/internal/xdsclient/transport" 34 "google.golang.org/grpc/xds/internal/xdsclient/xdsresource/version" 35 "google.golang.org/protobuf/proto" 36 "google.golang.org/protobuf/testing/protocmp" 37 "google.golang.org/protobuf/types/known/anypb" 38 "google.golang.org/protobuf/types/known/wrapperspb" 39 40 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 41 v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 42 v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 43 v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 44 statuspb "google.golang.org/genproto/googleapis/rpc/status" 45 ) 46 47 var ( 48 errWantNack = errors.New("unsupported field 'use_original_dst' is present and set to true") 49 50 // A simple update handler for listener resources which validates only the 51 // `use_original_dst` field. 52 dataModelValidator = func(update transport.ResourceUpdate) error { 53 for _, r := range update.Resources { 54 inner := &v3discoverypb.Resource{} 55 if err := proto.Unmarshal(r.GetValue(), inner); err != nil { 56 return fmt.Errorf("failed to unmarshal DiscoveryResponse: %v", err) 57 } 58 lis := &v3listenerpb.Listener{} 59 if err := proto.Unmarshal(r.GetValue(), lis); err != nil { 60 return fmt.Errorf("failed to unmarshal DiscoveryResponse: %v", err) 61 } 62 if useOrigDst := lis.GetUseOriginalDst(); useOrigDst != nil && useOrigDst.GetValue() { 63 return errWantNack 64 } 65 } 66 return nil 67 } 68 ) 69 70 // TestSimpleAckAndNack tests simple ACK and NACK scenarios. 71 // 1. When the data model layer likes a received response, the test verifies 72 // that an ACK is sent matching the version and nonce from the response. 73 // 2. When a subsequent response is disliked by the data model layer, the test 74 // verifies that a NACK is sent matching the previously ACKed version and 75 // current nonce from the response. 76 // 3. When a subsequent response is liked by the data model layer, the test 77 // verifies that an ACK is sent matching the version and nonce from the 78 // current response. 79 func (s) TestSimpleAckAndNack(t *testing.T) { 80 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 81 defer cancel() 82 83 // Create an xDS management server listening on a local port. Configure the 84 // request and response handlers to push on channels which are inspected by 85 // the test goroutine to verify ack version and nonce. 86 streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1) 87 streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1) 88 mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{ 89 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 90 select { 91 case streamRequestCh <- req: 92 case <-ctx.Done(): 93 } 94 return nil 95 }, 96 OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) { 97 select { 98 case streamResponseCh <- resp: 99 case <-ctx.Done(): 100 } 101 }, 102 }) 103 if err != nil { 104 t.Fatalf("Failed to start xDS management server: %v", err) 105 } 106 defer mgmtServer.Stop() 107 t.Logf("Started xDS management server on %s", mgmtServer.Address) 108 109 // Configure the management server with appropriate resources. 110 apiListener := &v3listenerpb.ApiListener{ 111 ApiListener: func() *anypb.Any { 112 return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 113 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{ 114 Rds: &v3httppb.Rds{ 115 ConfigSource: &v3corepb.ConfigSource{ 116 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 117 }, 118 RouteConfigName: "route-configuration-name", 119 }, 120 }, 121 }) 122 }(), 123 } 124 const resourceName = "resource name 1" 125 listenerResource := &v3listenerpb.Listener{ 126 Name: resourceName, 127 ApiListener: apiListener, 128 } 129 nodeID := uuid.New().String() 130 mgmtServer.Update(ctx, e2e.UpdateOptions{ 131 NodeID: nodeID, 132 Listeners: []*v3listenerpb.Listener{listenerResource}, 133 SkipValidation: true, 134 }) 135 136 // Create a new transport. 137 tr, err := transport.New(transport.Options{ 138 ServerCfg: *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 139 OnRecvHandler: dataModelValidator, 140 OnErrorHandler: func(err error) {}, 141 OnSendHandler: func(*transport.ResourceSendInfo) {}, 142 NodeProto: &v3corepb.Node{Id: nodeID}, 143 }) 144 if err != nil { 145 t.Fatalf("Failed to create xDS transport: %v", err) 146 } 147 defer tr.Close() 148 149 // Send a discovery request through the transport. 150 tr.SendRequest(version.V3ListenerURL, []string{resourceName}) 151 152 // Verify that the initial discovery request matches expectation. 153 var gotReq *v3discoverypb.DiscoveryRequest 154 select { 155 case gotReq = <-streamRequestCh: 156 case <-ctx.Done(): 157 t.Fatalf("Timeout waiting for discovery request on the stream") 158 } 159 wantReq := &v3discoverypb.DiscoveryRequest{ 160 VersionInfo: "", 161 Node: &v3corepb.Node{Id: nodeID}, 162 ResourceNames: []string{resourceName}, 163 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 164 ResponseNonce: "", 165 } 166 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 167 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 168 } 169 170 // Capture the version and nonce from the response. 171 var gotResp *v3discoverypb.DiscoveryResponse 172 select { 173 case gotResp = <-streamResponseCh: 174 case <-ctx.Done(): 175 t.Fatalf("Timeout waiting for discovery response on the stream") 176 } 177 178 // Verify that the ACK contains the appropriate version and nonce. 179 wantReq.VersionInfo = gotResp.GetVersionInfo() 180 wantReq.ResponseNonce = gotResp.GetNonce() 181 select { 182 case gotReq = <-streamRequestCh: 183 case <-ctx.Done(): 184 t.Fatalf("Timeout waiting for the discovery request ACK on the stream") 185 } 186 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 187 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 188 } 189 190 // Update the management server's copy of the resource to include a field 191 // which will cause the resource to be NACKed. 192 badListener := proto.Clone(listenerResource).(*v3listenerpb.Listener) 193 badListener.UseOriginalDst = &wrapperspb.BoolValue{Value: true} 194 mgmtServer.Update(ctx, e2e.UpdateOptions{ 195 NodeID: nodeID, 196 Listeners: []*v3listenerpb.Listener{badListener}, 197 SkipValidation: true, 198 }) 199 200 select { 201 case gotResp = <-streamResponseCh: 202 case <-ctx.Done(): 203 t.Fatalf("Timeout waiting for discovery response on the stream") 204 } 205 206 // Verify that the NACK contains the appropriate version, nonce and error. 207 // We expect the version to not change as this is a NACK. 208 wantReq.ResponseNonce = gotResp.GetNonce() 209 wantReq.ErrorDetail = &statuspb.Status{ 210 Code: int32(codes.InvalidArgument), 211 Message: errWantNack.Error(), 212 } 213 select { 214 case gotReq = <-streamRequestCh: 215 case <-ctx.Done(): 216 t.Fatalf("Timeout waiting for the discovery request ACK on the stream") 217 } 218 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 219 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 220 } 221 222 // Update the management server to send a good resource again. 223 mgmtServer.Update(ctx, e2e.UpdateOptions{ 224 NodeID: nodeID, 225 Listeners: []*v3listenerpb.Listener{listenerResource}, 226 SkipValidation: true, 227 }) 228 229 // The envoy-go-control-plane management server keeps resending the same 230 // resource as long as we keep NACK'ing it. So, we will see the bad resource 231 // sent to us a few times here, before receiving the good resource. 232 for { 233 select { 234 case gotResp = <-streamResponseCh: 235 case <-ctx.Done(): 236 t.Fatalf("Timeout waiting for discovery response on the stream") 237 } 238 239 // Verify that the ACK contains the appropriate version and nonce. 240 wantReq.VersionInfo = gotResp.GetVersionInfo() 241 wantReq.ResponseNonce = gotResp.GetNonce() 242 wantReq.ErrorDetail = nil 243 select { 244 case gotReq = <-streamRequestCh: 245 case <-ctx.Done(): 246 t.Fatalf("Timeout waiting for the discovery request ACK on the stream") 247 } 248 diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)) 249 if diff == "" { 250 break 251 } 252 t.Logf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 253 } 254 } 255 256 // TestInvalidFirstResponse tests the case where the first response is invalid. 257 // The test verifies that the NACK contains an empty version string. 258 func (s) TestInvalidFirstResponse(t *testing.T) { 259 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 260 defer cancel() 261 262 // Create an xDS management server listening on a local port. Configure the 263 // request and response handlers to push on channels which are inspected by 264 // the test goroutine to verify ack version and nonce. 265 streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1) 266 streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1) 267 mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{ 268 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 269 select { 270 case streamRequestCh <- req: 271 case <-ctx.Done(): 272 } 273 return nil 274 }, 275 OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) { 276 select { 277 case streamResponseCh <- resp: 278 case <-ctx.Done(): 279 } 280 }, 281 }) 282 if err != nil { 283 t.Fatalf("Failed to start xDS management server: %v", err) 284 } 285 defer mgmtServer.Stop() 286 t.Logf("Started xDS management server on %s", mgmtServer.Address) 287 288 // Configure the management server with appropriate resources. 289 apiListener := &v3listenerpb.ApiListener{ 290 ApiListener: func() *anypb.Any { 291 return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 292 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{ 293 Rds: &v3httppb.Rds{ 294 ConfigSource: &v3corepb.ConfigSource{ 295 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 296 }, 297 RouteConfigName: "route-configuration-name", 298 }, 299 }, 300 }) 301 }(), 302 } 303 const resourceName = "resource name 1" 304 listenerResource := &v3listenerpb.Listener{ 305 Name: resourceName, 306 ApiListener: apiListener, 307 UseOriginalDst: &wrapperspb.BoolValue{Value: true}, // This will cause the resource to be NACKed. 308 } 309 nodeID := uuid.New().String() 310 mgmtServer.Update(ctx, e2e.UpdateOptions{ 311 NodeID: nodeID, 312 Listeners: []*v3listenerpb.Listener{listenerResource}, 313 SkipValidation: true, 314 }) 315 316 // Create a new transport. 317 tr, err := transport.New(transport.Options{ 318 ServerCfg: *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 319 NodeProto: &v3corepb.Node{Id: nodeID}, 320 OnRecvHandler: dataModelValidator, 321 OnErrorHandler: func(err error) {}, 322 OnSendHandler: func(*transport.ResourceSendInfo) {}, 323 }) 324 if err != nil { 325 t.Fatalf("Failed to create xDS transport: %v", err) 326 } 327 defer tr.Close() 328 329 // Send a discovery request through the transport. 330 tr.SendRequest(version.V3ListenerURL, []string{resourceName}) 331 332 // Verify that the initial discovery request matches expectation. 333 var gotReq *v3discoverypb.DiscoveryRequest 334 select { 335 case gotReq = <-streamRequestCh: 336 case <-ctx.Done(): 337 t.Fatalf("Timeout waiting for discovery request on the stream") 338 } 339 wantReq := &v3discoverypb.DiscoveryRequest{ 340 Node: &v3corepb.Node{Id: nodeID}, 341 ResourceNames: []string{resourceName}, 342 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 343 } 344 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 345 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 346 } 347 348 var gotResp *v3discoverypb.DiscoveryResponse 349 select { 350 case gotResp = <-streamResponseCh: 351 case <-ctx.Done(): 352 t.Fatalf("Timeout waiting for discovery response on the stream") 353 } 354 355 // NACK should contain the appropriate error, nonce, but empty version. 356 wantReq.VersionInfo = "" 357 wantReq.ResponseNonce = gotResp.GetNonce() 358 wantReq.ErrorDetail = &statuspb.Status{ 359 Code: int32(codes.InvalidArgument), 360 Message: errWantNack.Error(), 361 } 362 select { 363 case gotReq = <-streamRequestCh: 364 case <-ctx.Done(): 365 t.Fatalf("Timeout waiting for the discovery request ACK on the stream") 366 } 367 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 368 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 369 } 370 } 371 372 // TestResourceIsNotRequestedAnymore tests the scenario where the xDS client is 373 // no longer interested in a resource. The following sequence of events are 374 // tested: 375 // 1. A resource is requested and a good response is received. The test verifies 376 // that an ACK is sent for this resource. 377 // 2. The previously requested resource is no longer requested. The test 378 // verifies that a request with no resource names is sent out. 379 // 3. The same resource is requested again. The test verifies that the request 380 // is sent with the previously ACKed version. 381 func (s) TestResourceIsNotRequestedAnymore(t *testing.T) { 382 ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) 383 defer cancel() 384 385 // Create an xDS management server listening on a local port. Configure the 386 // request and response handlers to push on channels which are inspected by 387 // the test goroutine to verify ack version and nonce. 388 streamRequestCh := make(chan *v3discoverypb.DiscoveryRequest, 1) 389 streamResponseCh := make(chan *v3discoverypb.DiscoveryResponse, 1) 390 mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{ 391 OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { 392 select { 393 case streamRequestCh <- req: 394 case <-ctx.Done(): 395 } 396 return nil 397 }, 398 OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) { 399 select { 400 case streamResponseCh <- resp: 401 case <-ctx.Done(): 402 } 403 }, 404 }) 405 if err != nil { 406 t.Fatalf("Failed to start xDS management server: %v", err) 407 } 408 defer mgmtServer.Stop() 409 t.Logf("Started xDS management server on %s", mgmtServer.Address) 410 411 // Configure the management server with appropriate resources. 412 apiListener := &v3listenerpb.ApiListener{ 413 ApiListener: func() *anypb.Any { 414 return testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{ 415 RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{ 416 Rds: &v3httppb.Rds{ 417 ConfigSource: &v3corepb.ConfigSource{ 418 ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}}, 419 }, 420 RouteConfigName: "route-configuration-name", 421 }, 422 }, 423 }) 424 }(), 425 } 426 const resourceName = "resource name 1" 427 listenerResource := &v3listenerpb.Listener{ 428 Name: resourceName, 429 ApiListener: apiListener, 430 } 431 nodeID := uuid.New().String() 432 mgmtServer.Update(ctx, e2e.UpdateOptions{ 433 NodeID: nodeID, 434 Listeners: []*v3listenerpb.Listener{listenerResource}, 435 SkipValidation: true, 436 }) 437 438 // Create a new transport. 439 tr, err := transport.New(transport.Options{ 440 ServerCfg: *xdstestutils.ServerConfigForAddress(t, mgmtServer.Address), 441 NodeProto: &v3corepb.Node{Id: nodeID}, 442 OnRecvHandler: dataModelValidator, 443 OnErrorHandler: func(err error) {}, 444 OnSendHandler: func(*transport.ResourceSendInfo) {}, 445 }) 446 if err != nil { 447 t.Fatalf("Failed to create xDS transport: %v", err) 448 } 449 defer tr.Close() 450 451 // Send a discovery request through the transport. 452 tr.SendRequest(version.V3ListenerURL, []string{resourceName}) 453 454 // Verify that the initial discovery request matches expectation. 455 var gotReq *v3discoverypb.DiscoveryRequest 456 select { 457 case gotReq = <-streamRequestCh: 458 case <-ctx.Done(): 459 t.Fatalf("Timeout waiting for discovery request on the stream") 460 } 461 wantReq := &v3discoverypb.DiscoveryRequest{ 462 VersionInfo: "", 463 Node: &v3corepb.Node{Id: nodeID}, 464 ResourceNames: []string{resourceName}, 465 TypeUrl: "type.googleapis.com/envoy.config.listener.v3.Listener", 466 ResponseNonce: "", 467 } 468 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 469 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 470 } 471 472 // Capture the version and nonce from the response. 473 var gotResp *v3discoverypb.DiscoveryResponse 474 select { 475 case gotResp = <-streamResponseCh: 476 case <-ctx.Done(): 477 t.Fatalf("Timeout waiting for discovery response on the stream") 478 } 479 480 // Verify that the ACK contains the appropriate version and nonce. 481 wantReq.VersionInfo = gotResp.GetVersionInfo() 482 wantReq.ResponseNonce = gotResp.GetNonce() 483 select { 484 case gotReq = <-streamRequestCh: 485 case <-ctx.Done(): 486 t.Fatalf("Timeout waiting for the discovery request ACK on the stream") 487 } 488 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 489 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 490 } 491 492 // Send a discovery request with no resource names. 493 tr.SendRequest(version.V3ListenerURL, []string{}) 494 495 // Verify that the discovery request matches expectation. 496 select { 497 case gotReq = <-streamRequestCh: 498 case <-ctx.Done(): 499 t.Fatalf("Timeout waiting for discovery request on the stream") 500 } 501 wantReq.ResourceNames = nil 502 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 503 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 504 } 505 506 // Send a discovery request for the same resource requested earlier. 507 tr.SendRequest(version.V3ListenerURL, []string{resourceName}) 508 509 // Verify that the discovery request contains the version from the 510 // previously received response. 511 select { 512 case gotReq = <-streamRequestCh: 513 case <-ctx.Done(): 514 t.Fatalf("Timeout waiting for discovery request on the stream") 515 } 516 wantReq.ResourceNames = []string{resourceName} 517 if diff := cmp.Diff(gotReq, wantReq, protocmp.Transform(), cmpopts.SortSlices(strSort)); diff != "" { 518 t.Fatalf("Unexpected diff in received discovery request, diff (-got, +want):\n%s", diff) 519 } 520 }