| /* |
| * Copyright 2023 gRPC authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package e2e_test |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "strings" |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/uuid" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/balancer" |
| "google.golang.org/grpc/balancer/roundrobin" |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/connectivity" |
| "google.golang.org/grpc/credentials/insecure" |
| "google.golang.org/grpc/internal" |
| "google.golang.org/grpc/internal/balancer/stub" |
| iserviceconfig "google.golang.org/grpc/internal/serviceconfig" |
| "google.golang.org/grpc/internal/stubserver" |
| "google.golang.org/grpc/internal/testutils" |
| "google.golang.org/grpc/internal/testutils/xds/e2e" |
| "google.golang.org/grpc/internal/xds/balancer/clusterimpl" |
| "google.golang.org/grpc/internal/xds/balancer/outlierdetection" |
| "google.golang.org/grpc/internal/xds/balancer/priority" |
| "google.golang.org/grpc/internal/xds/balancer/wrrlocality" |
| "google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version" |
| "google.golang.org/grpc/resolver" |
| "google.golang.org/grpc/serviceconfig" |
| "google.golang.org/grpc/status" |
| "google.golang.org/protobuf/types/known/durationpb" |
| "google.golang.org/protobuf/types/known/wrapperspb" |
| |
| v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" |
| v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" |
| v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" |
| v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" |
| v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" |
| v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" |
| testgrpc "google.golang.org/grpc/interop/grpc_testing" |
| testpb "google.golang.org/grpc/interop/grpc_testing" |
| |
| _ "google.golang.org/grpc/internal/xds/balancer/cdsbalancer" // Register the "cds_experimental" LB policy. |
| _ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter |
| _ "google.golang.org/grpc/internal/xds/resolver" // Register the xds resolver |
| ) |
| |
| // setupAndDial performs common setup across all tests |
| // |
| // - creates an xDS client with the passed in bootstrap contents |
| // - creates a xds resolver to be used |
| // - creates a ClientConn to talk to the test backends |
| // |
| // Returns a function to close the ClientConn and the xDS client. |
| func setupAndDial(t *testing.T, bootstrapContents []byte) (*grpc.ClientConn, func()) { |
| t.Helper() |
| // Create an xDS resolver with the above bootstrap configuration. |
| r, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents) |
| if err != nil { |
| t.Fatalf("xDS resolver creation failed: %v", err) |
| } |
| // Create a ClientConn and make a successful RPC. |
| cc, err := grpc.NewClient("xds:///"+serviceName, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r)) |
| if err != nil { |
| t.Fatalf("grpc.NewClient() failed: %v", err) |
| } |
| cc.Connect() |
| return cc, func() { |
| cc.Close() |
| } |
| } |
| |
| // TestErrorFromParentLB_ConnectionError tests the case where a connection error |
| // is sent by the CDS LB Policy. The CDS LB policy sends a connection error when |
| // the ADS stream to the management server breaks. The test verifies that there |
| // is no perceivable effect because of this connection error, and that RPCs |
| // continue to work (because the LB policies are expected to use previously |
| // received xDS resources). |
| func (s) TestErrorFromParentLB_ConnectionError(t *testing.T) { |
| // Create a listener to be used by the management server. The test will |
| // close this listener to simulate ADS stream breakage. |
| lis, err := testutils.LocalTCPListener() |
| if err != nil { |
| t.Fatalf("testutils.LocalTCPListener() failed: %v", err) |
| } |
| |
| // Start an xDS management server with the above restartable listener, and |
| // push a channel when the stream is closed. |
| streamClosedCh := make(chan struct{}, 1) |
| managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ |
| Listener: lis, |
| OnStreamClosed: func(int64, *v3corepb.Node) { |
| select { |
| case streamClosedCh <- struct{}{}: |
| default: |
| } |
| }, |
| }) |
| |
| // Create bootstrap configuration pointing to the above management server. |
| nodeID := uuid.New().String() |
| bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) |
| |
| server := stubserver.StartTestService(t, nil) |
| defer server.Stop() |
| |
| resources := e2e.DefaultClientResources(e2e.ResourceParams{ |
| NodeID: nodeID, |
| DialTarget: serviceName, |
| Host: "localhost", |
| Port: testutils.ParsePort(t, server.Address), |
| }) |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| defer cancel() |
| if err := managementServer.Update(ctx, resources); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create xDS client, xdsResolver, and dial the test backends. |
| cc, cleanup := setupAndDial(t, bootstrapContents) |
| defer cleanup() |
| |
| client := testgrpc.NewTestServiceClient(cc) |
| if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil { |
| t.Fatalf("EmptyCall() failed: %v", err) |
| } |
| |
| // Close the listener and ensure that the ADS stream breaks. |
| lis.Close() |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout when waiting for ADS stream to close") |
| default: |
| } |
| |
| // Ensure that RPCs continue to succeed for the next second. |
| for end := time.Now().Add(time.Second); time.Now().Before(end); <-time.After(defaultTestShortTimeout) { |
| if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil { |
| t.Fatalf("EmptyCall() failed: %v", err) |
| } |
| } |
| } |
| |
| // TestErrorFromParentLB_ResourceNotFound tests the case where a |
| // resource-not-found error is received by the CDS LB policy for a cluster. The |
| // resource-not-found error is received when the cluster resource associated |
| // with these LB policies is removed by the management server. The test verifies |
| // that the associated EDS is canceled and RPCs fail. It also ensures that when |
| // the Cluster resource is added back, the EDS resource is re-requested and RPCs |
| // being to succeed. |
| func (s) TestErrorFromParentLB_ResourceNotFound(t *testing.T) { |
| // Start an xDS management server that uses a couple of channels to |
| // notify the test about the following events: |
| // - an EDS requested with the expected resource name is requested |
| // - EDS resource is unrequested, i.e, an EDS request with no resource name |
| // is received, which indicates that we are no longer interested in that |
| // resource. |
| edsResourceRequestedCh := make(chan struct{}, 1) |
| edsResourceCanceledCh := make(chan struct{}, 1) |
| managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ |
| OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { |
| if req.GetTypeUrl() == version.V3EndpointsURL { |
| switch len(req.GetResourceNames()) { |
| case 0: |
| select { |
| case edsResourceCanceledCh <- struct{}{}: |
| default: |
| } |
| case 1: |
| if req.GetResourceNames()[0] == edsServiceName { |
| select { |
| case edsResourceRequestedCh <- struct{}{}: |
| default: |
| } |
| } |
| default: |
| t.Errorf("Unexpected number of resources, %d, in an EDS request", len(req.GetResourceNames())) |
| } |
| } |
| return nil |
| }, |
| }) |
| |
| // Create bootstrap configuration pointing to the above management server. |
| nodeID := uuid.New().String() |
| bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) |
| |
| server := stubserver.StartTestService(t, nil) |
| defer server.Stop() |
| |
| // Configure cluster and endpoints resources in the management server. |
| resources := e2e.UpdateOptions{ |
| NodeID: nodeID, |
| Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, |
| Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, |
| Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone)}, |
| Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})}, |
| SkipValidation: true, |
| } |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| defer cancel() |
| if err := managementServer.Update(ctx, resources); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create xDS client, xdsResolver, and dial the test backends. |
| cc, cleanup := setupAndDial(t, bootstrapContents) |
| defer cleanup() |
| |
| // Wait for the EDS resource to be requested. |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout when waiting for EDS resource to be requested") |
| case <-edsResourceRequestedCh: |
| } |
| |
| // Ensure that a successful RPC can be made. |
| client := testgrpc.NewTestServiceClient(cc) |
| if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil { |
| t.Fatalf("EmptyCall() failed: %v", err) |
| } |
| |
| oldCluster := resources.Clusters |
| // Delete the cluster resource from the management server. |
| resources.Clusters = nil |
| if err := managementServer.Update(ctx, resources); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Wait for the EDS resource to be not requested anymore. |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout when waiting for EDS resource to not be requested") |
| case <-edsResourceCanceledCh: |
| } |
| |
| // Ensure that RPCs start to fail with expected error. |
| wantErr := fmt.Sprintf("resource %q of type %q has been removed", clusterName, "ClusterResource") |
| for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { |
| sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) |
| defer sCancel() |
| _, err := client.EmptyCall(sCtx, &testpb.Empty{}) |
| if status.Code(err) == codes.Unavailable && strings.Contains(err.Error(), wantErr) { |
| break |
| } |
| if err != nil { |
| t.Logf("EmptyCall failed: %v", err) |
| } |
| } |
| if ctx.Err() != nil { |
| t.Fatalf("RPCs did not fail after removal of Cluster resource") |
| } |
| |
| testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure) |
| |
| // Add the cluster resource back to the management server. |
| resources.Clusters = oldCluster |
| if err := managementServer.Update(ctx, resources); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Wait for the EDS resource to be requested again. |
| select { |
| case <-ctx.Done(): |
| t.Fatal("Timeout when waiting for EDS resource to be requested") |
| case <-edsResourceRequestedCh: |
| } |
| |
| // Ensure that a successful RPC can be made. |
| for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { |
| sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) |
| defer sCancel() |
| if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); err != nil { |
| t.Logf("EmptyCall failed: %v", err) |
| continue |
| } |
| break |
| } |
| if ctx.Err() != nil { |
| t.Fatalf("RPCs did not fail after removal of Cluster resource") |
| } |
| } |
| |
| // Test verifies that when the received Cluster resource contains outlier |
| // detection configuration, the LB config pushed to the priority policy contains |
| // the appropriate configuration for the outlier detection LB policy. |
| func (s) TestOutlierDetectionConfigPropagationToChildPolicy(t *testing.T) { |
| // Unregister the priority balancer builder for the duration of this test, |
| // and register a policy under the same name that makes the LB config |
| // pushed to it available to the test. |
| priorityBuilder := balancer.Get(priority.Name) |
| internal.BalancerUnregister(priorityBuilder.Name()) |
| lbCfgCh := make(chan serviceconfig.LoadBalancingConfig, 1) |
| stub.Register(priority.Name, stub.BalancerFuncs{ |
| Init: func(bd *stub.BalancerData) { |
| bd.ChildBalancer = priorityBuilder.Build(bd.ClientConn, bd.BuildOptions) |
| }, |
| ParseConfig: func(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) { |
| return priorityBuilder.(balancer.ConfigParser).ParseConfig(lbCfg) |
| }, |
| UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error { |
| select { |
| case lbCfgCh <- ccs.BalancerConfig: |
| default: |
| } |
| return bd.ChildBalancer.UpdateClientConnState(ccs) |
| }, |
| Close: func(bd *stub.BalancerData) { |
| bd.ChildBalancer.Close() |
| }, |
| }) |
| defer balancer.Register(priorityBuilder) |
| |
| managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{}) |
| |
| // Create bootstrap configuration pointing to the above management server. |
| nodeID := uuid.New().String() |
| bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) |
| |
| server := stubserver.StartTestService(t, nil) |
| defer server.Stop() |
| |
| // Configure cluster and endpoints resources in the management server. |
| cluster := e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone) |
| cluster.OutlierDetection = &v3clusterpb.OutlierDetection{ |
| Interval: durationpb.New(10 * time.Second), |
| BaseEjectionTime: durationpb.New(30 * time.Second), |
| MaxEjectionTime: durationpb.New(300 * time.Second), |
| MaxEjectionPercent: wrapperspb.UInt32(10), |
| SuccessRateStdevFactor: wrapperspb.UInt32(2000), |
| EnforcingSuccessRate: wrapperspb.UInt32(50), |
| SuccessRateMinimumHosts: wrapperspb.UInt32(10), |
| SuccessRateRequestVolume: wrapperspb.UInt32(50), |
| } |
| resources := e2e.UpdateOptions{ |
| NodeID: nodeID, |
| Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, |
| Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, |
| Clusters: []*v3clusterpb.Cluster{cluster}, |
| Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})}, |
| SkipValidation: true, |
| } |
| ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) |
| defer cancel() |
| if err := managementServer.Update(ctx, resources); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Create xDS client, xdsResolver, and dial the test backends. |
| _, cleanup := setupAndDial(t, bootstrapContents) |
| defer cleanup() |
| |
| // The priority configuration generated should have Outlier Detection as a |
| // direct child due to Outlier Detection being turned on. |
| wantCfg := &priority.LBConfig{ |
| Children: map[string]*priority.Child{ |
| "priority-0-0": { |
| Config: &iserviceconfig.BalancerConfig{ |
| Name: outlierdetection.Name, |
| Config: &outlierdetection.LBConfig{ |
| Interval: iserviceconfig.Duration(10 * time.Second), // default interval |
| BaseEjectionTime: iserviceconfig.Duration(30 * time.Second), |
| MaxEjectionTime: iserviceconfig.Duration(300 * time.Second), |
| MaxEjectionPercent: 10, |
| SuccessRateEjection: &outlierdetection.SuccessRateEjection{ |
| StdevFactor: 2000, |
| EnforcementPercentage: 50, |
| MinimumHosts: 10, |
| RequestVolume: 50, |
| }, |
| ChildPolicy: &iserviceconfig.BalancerConfig{ |
| Name: clusterimpl.Name, |
| Config: &clusterimpl.LBConfig{ |
| Cluster: clusterName, |
| ChildPolicy: &iserviceconfig.BalancerConfig{ |
| Name: wrrlocality.Name, |
| Config: &wrrlocality.LBConfig{ |
| ChildPolicy: &iserviceconfig.BalancerConfig{ |
| Name: roundrobin.Name, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| IgnoreReresolutionRequests: true, |
| }, |
| }, |
| Priorities: []string{"priority-0-0"}, |
| } |
| |
| select { |
| case lbCfg := <-lbCfgCh: |
| gotCfg := lbCfg.(*priority.LBConfig) |
| if diff := cmp.Diff(wantCfg, gotCfg); diff != "" { |
| t.Fatalf("Child policy received unexpected diff in config (-want +got):\n%s", diff) |
| } |
| case <-ctx.Done(): |
| t.Fatalf("Timeout when waiting for child policy to receive its configuration") |
| } |
| } |