/* * 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" "fmt" "net" "slices" "strconv" "strings" "sync/atomic" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/uuid" "google.golang.org/grpc" "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/grpcsync" "google.golang.org/grpc/internal/stubserver" "google.golang.org/grpc/internal/testutils/pickfirst" "google.golang.org/grpc/internal/testutils/xds/e2e" "google.golang.org/grpc/internal/testutils/xds/fakeserver" "google.golang.org/grpc/internal/xds/bootstrap" "google.golang.org/grpc/internal/xds/xdsclient" "google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version" "google.golang.org/grpc/peer" "google.golang.org/grpc/resolver" "google.golang.org/grpc/resolver/manual" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" "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" v3lrspb "github.com/envoyproxy/go-control-plane/envoy/service/load_stats/v3" testgrpc "google.golang.org/grpc/interop/grpc_testing" testpb "google.golang.org/grpc/interop/grpc_testing" _ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter ) // makeAggregateClusterResource returns an aggregate cluster resource with the // given name and list of child names. func makeAggregateClusterResource(name string, childNames []string) *v3clusterpb.Cluster { return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{ ClusterName: name, Type: e2e.ClusterTypeAggregate, ChildNames: childNames, }) } // makeLogicalDNSClusterResource returns a LOGICAL_DNS cluster resource with the // given name and given DNS host and port. func makeLogicalDNSClusterResource(name, dnsHost string, dnsPort uint32) *v3clusterpb.Cluster { return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{ ClusterName: name, Type: e2e.ClusterTypeLogicalDNS, DNSHostName: dnsHost, DNSPort: dnsPort, }) } // setupDNS unregisters the DNS resolver and registers a manual resolver for the // same scheme. This allows the test to mock the DNS resolution by supplying the // addresses of the test backends. // // Returns the following: // - a channel onto which the DNS target being resolved is written to by the // mock DNS resolver // - a manual resolver which is used to mock the actual DNS resolution func setupDNS(t *testing.T) (chan resolver.Target, *manual.Resolver) { targetCh := make(chan resolver.Target, 1) mr := manual.NewBuilderWithScheme("dns") mr.BuildCallback = func(target resolver.Target, _ resolver.ClientConn, _ resolver.BuildOptions) { targetCh <- target } dnsResolverBuilder := resolver.Get("dns") resolver.Register(mr) t.Cleanup(func() { resolver.Register(dnsResolverBuilder) }) return targetCh, mr } // TestAggregateCluster_WithTwoEDSClusters tests the case where the top-level // cluster resource is an aggregate cluster. It verifies that RPCs fail when the // management server has not responded to all requested EDS resources, and also // that RPCs are routed to the highest priority cluster once all requested EDS // resources have been sent by the management server. func (s) TestAggregateCluster_WithTwoEDSClusters(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() const clusterName1 = clusterName + "-cluster-1" const clusterName2 = clusterName + "-cluster-2" // gotBothEDSRequests is fired when the management server receives EDS // requests for both clusterName1 and clusterName2. This is used to block // the test until both EDS requests have been received. gotBothEDSRequests := grpcsync.NewEvent() managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { if req.GetTypeUrl() != version.V3EndpointsURL { return nil } if len(req.GetResourceNames()) == 0 { // This happens at the end of the test when the grpc channel is // being shut down and it is no longer interested in xDS // resources. return nil } // Check if we have a request for both EDS resources. If so, fire // the event to unblock the test. names := req.GetResourceNames() sortedNames := slices.Clone(names) slices.Sort(sortedNames) if cmp.Equal(sortedNames, []string{clusterName1, clusterName2}) { gotBothEDSRequests.Fire() } return nil }, AllowResourceSubset: true, }) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend belongs to EDS cluster "cluster-1", while the second backend // belongs to EDS cluster "cluster-2". servers, cleanup2 := startTestServiceBackends(t, 2) defer cleanup2() addrs, ports := backendAddressesAndPorts(t, servers) // Configure an aggregate cluster, two EDS clusters and only one endpoints // resource (corresponding to the first EDS cluster) 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{ makeAggregateClusterResource(clusterName, []string{clusterName1, clusterName2}), e2e.DefaultCluster(clusterName1, "", e2e.SecurityLevelNone), e2e.DefaultCluster(clusterName2, "", e2e.SecurityLevelNone), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(clusterName1, "localhost", []uint32{uint32(ports[0])})}, SkipValidation: true, } 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 both EDS resources to be requested. select { case <-gotBothEDSRequests.Done(): case <-ctx.Done(): t.Fatalf("Timeout when waiting for all EDS resources %v to be requested", []string{clusterName1, clusterName2}) } // Make an RPC with a short deadline. We expect this RPC to not succeed // because the management server has not responded with all EDS resources // requested. client := testgrpc.NewTestServiceClient(cc) sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) defer sCancel() if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); status.Code(err) != codes.DeadlineExceeded { t.Fatalf("EmptyCall() code %s, want %s", status.Code(err), codes.DeadlineExceeded) } // Update the management server with the second EDS resource. resources.Endpoints = append(resources.Endpoints, e2e.DefaultEndpoint(clusterName2, "localhost", []uint32{uint32(ports[1])})) if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Make an RPC and ensure that it gets routed to cluster-1, implicitly // higher priority than cluster-2. peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } } // TestAggregateCluster_WithTwoEDSClusters_PrioritiesChange tests the case where // the top-level cluster resource is an aggregate cluster. It verifies that RPCs // are routed to the highest priority EDS cluster. func (s) TestAggregateCluster_WithTwoEDSClusters_PrioritiesChange(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend belongs to EDS cluster "cluster-1", while the second backend // belongs to EDS cluster "cluster-2". servers, cleanup2 := startTestServiceBackends(t, 2) defer cleanup2() addrs, ports := backendAddressesAndPorts(t, servers) // Configure an aggregate cluster, two EDS clusters and the corresponding // endpoints resources in the management server. const clusterName1 = clusterName + "cluster-1" const clusterName2 = clusterName + "cluster-2" resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{clusterName1, clusterName2}), e2e.DefaultCluster(clusterName1, "", e2e.SecurityLevelNone), e2e.DefaultCluster(clusterName2, "", e2e.SecurityLevelNone), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{ e2e.DefaultEndpoint(clusterName1, "localhost", []uint32{uint32(ports[0])}), e2e.DefaultEndpoint(clusterName2, "localhost", []uint32{uint32(ports[1])}), }, 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() // Make an RPC and ensure that it gets routed to cluster-1, implicitly // higher priority than cluster-2. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } // Swap the priorities of the EDS clusters in the aggregate cluster. resources.Clusters = []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{clusterName2, clusterName1}), e2e.DefaultCluster(clusterName1, "", e2e.SecurityLevelNone), e2e.DefaultCluster(clusterName2, "", e2e.SecurityLevelNone), } if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Wait for RPCs to get routed to cluster-2, which is now implicitly higher // priority than cluster-1, after the priority switch above. for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() == addrs[1].Addr { break } } if ctx.Err() != nil { t.Fatal("Timeout waiting for RPCs to be routed to cluster-2 after priority switch") } } func hostAndPortFromAddress(t *testing.T, addr string) (string, uint32) { t.Helper() host, p, err := net.SplitHostPort(addr) if err != nil { t.Fatalf("Invalid serving address: %v", addr) } port, err := strconv.ParseUint(p, 10, 32) if err != nil { t.Fatalf("Invalid serving port %q: %v", p, err) } return host, uint32(port) } // TestAggregateCluster_WithOneDNSCluster tests the case where the top-level // cluster resource is an aggregate cluster that resolves to a single // LOGICAL_DNS cluster. The test verifies that RPCs can be made to backends that // make up the LOGICAL_DNS cluster. func (s) TestAggregateCluster_WithOneDNSCluster(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start a test service backend. server := stubserver.StartTestService(t, nil) defer server.Stop() host, port := hostAndPortFromAddress(t, server.Address) // Configure an aggregate cluster pointing to a single LOGICAL_DNS cluster. const dnsClusterName = clusterName + "-dns" resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName}), makeLogicalDNSClusterResource(dnsClusterName, host, uint32(port)), }, 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() // Make an RPC and ensure that it gets routed to the first backend since the // child policy for a LOGICAL_DNS cluster is pick_first by default. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != server.Address { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, server.Address) } } // Tests the case where the top-level cluster resource is an aggregate cluster // that resolves to a single LOGICAL_DNS cluster. The specified dns hostname is // expected to fail url parsing. The test verifies that the channel moves to // TRANSIENT_FAILURE. func (s) TestAggregateCluster_WithOneDNSCluster_ParseFailure(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Configure an aggregate cluster pointing to a single LOGICAL_DNS cluster. const dnsClusterName = clusterName + "-dns" resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName}), makeLogicalDNSClusterResource(dnsClusterName, "%gh&%ij", uint32(8080)), }, 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() // Ensure that the ClientConn moves to TransientFailure. for state := cc.GetState(); state != connectivity.TransientFailure; state = cc.GetState() { if !cc.WaitForStateChange(ctx, state) { t.Fatalf("Timed out waiting for state change. got %v; want %v", state, connectivity.TransientFailure) } } } // Tests the case where the top-level cluster resource is an aggregate cluster // that resolves to a single LOGICAL_DNS cluster. The test verifies that RPCs // can be made to backends that make up the LOGICAL_DNS cluster. The hostname of // the LOGICAL_DNS cluster is updated, and the test verifies that RPCs can be // made to backends that the new hostname resolves to. func (s) TestAggregateCluster_WithOneDNSCluster_HostnameChange(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend is used initially for the LOGICAL_DNS cluster and an update // switches the cluster to use the second backend. servers, cleanup2 := startTestServiceBackends(t, 2) defer cleanup2() // Configure an aggregate cluster pointing to a single LOGICAL_DNS cluster. const dnsClusterName = clusterName + "-dns" dnsHostName, dnsPort := hostAndPortFromAddress(t, servers[0].Address) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName}), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, 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() // Make an RPC and ensure that it gets routed to the first backend since the // child policy for a LOGICAL_DNS cluster is pick_first by default. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != servers[0].Address { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, servers[0].Address) } // Update the LOGICAL_DNS cluster's hostname to point to the second backend. dnsHostName, dnsPort = hostAndPortFromAddress(t, servers[1].Address) resources = e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName}), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, SkipValidation: true, } if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Ensure that traffic moves to the second backend eventually. for ctx.Err() == nil { if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() == servers[1].Address { break } } if ctx.Err() != nil { t.Fatal("Timeout when waiting for RPCs to switch to the second backend") } } // TestAggregateCluster_WithEDSAndDNS tests the case where the top-level cluster // resource is an aggregate cluster that resolves to an EDS and a LOGICAL_DNS // cluster. The test verifies that RPCs fail until both clusters are resolved to // endpoints, and RPCs are routed to the higher priority EDS cluster. func (s) TestAggregateCluster_WithEDSAndDNS(t *testing.T) { dnsTargetCh, dnsR := setupDNS(t) // Start an xDS management server that pushes the name of the requested EDS // resource onto a channel. edsResourceCh := make(chan string, 1) managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error { if req.GetTypeUrl() != version.V3EndpointsURL { return nil } if len(req.GetResourceNames()) == 0 { // This happens at the end of the test when the grpc channel is // being shut down and it is no longer interested in xDS // resources. return nil } select { case edsResourceCh <- req.GetResourceNames()[0]: default: } return nil }, AllowResourceSubset: true, }) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend is used for the EDS cluster and the second backend is used for // the LOGICAL_DNS cluster. servers, cleanup3 := startTestServiceBackends(t, 2) defer cleanup3() addrs, ports := backendAddressesAndPorts(t, servers) // Configure an aggregate cluster pointing to an EDS and DNS cluster. Also // configure an endpoints resource for the EDS cluster. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" dnsHostName = "dns_host" dnsPort = uint32(8080) ) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsClusterName, "localhost", []uint32{uint32(ports[0])})}, 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() // Ensure that an EDS request is sent for the expected resource name. select { case <-ctx.Done(): t.Fatal("Timeout when waiting for EDS request to be received on the management server") case name := <-edsResourceCh: if name != edsClusterName { t.Fatalf("Received EDS request with resource name %q, want %q", name, edsClusterName) } } // Ensure that the DNS resolver is started for the expected target. select { case <-ctx.Done(): t.Fatal("Timeout when waiting for DNS resolver to be started") case target := <-dnsTargetCh: got, want := target.Endpoint(), fmt.Sprintf("%s:%d", dnsHostName, dnsPort) if got != want { t.Fatalf("DNS resolution started for target %q, want %q", got, want) } } // Make an RPC with a short deadline. We expect this RPC to not succeed // because the DNS resolver has not responded with endpoint addresses. client := testgrpc.NewTestServiceClient(cc) sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) defer sCancel() if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); status.Code(err) != codes.DeadlineExceeded { t.Fatalf("EmptyCall() code %s, want %s", status.Code(err), codes.DeadlineExceeded) } // Update DNS resolver with test backend addresses. dnsR.UpdateState(resolver.State{Endpoints: addrsToEndpoints(addrs[1:])}) // Make an RPC and ensure that it gets routed to the first backend since the // EDS cluster is of higher priority than the LOGICAL_DNS cluster. peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } } // TestAggregateCluster_SwitchEDSAndDNS tests the case where the top-level // cluster resource is an aggregate cluster. It initially resolves to a single // EDS cluster. The test verifies that RPCs are routed to backends in the EDS // cluster. Subsequently, the aggregate cluster resolves to a single DNS // cluster. The test verifies that RPCs are successful, this time to backends in // the DNS cluster. func (s) TestAggregateCluster_SwitchEDSAndDNS(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend is used for the EDS cluster and the second backend is used for // the LOGICAL_DNS cluster. servers, cleanup3 := startTestServiceBackends(t, 2) defer cleanup3() addrs, ports := backendAddressesAndPorts(t, servers) dnsHostName, dnsPort := hostAndPortFromAddress(t, addrs[1].Addr) // Configure an aggregate cluster pointing to a single EDS cluster. Also, // configure the underlying EDS cluster (and the corresponding endpoints // resource) and DNS cluster (will be used later in the test). const dnsClusterName = clusterName + "-dns" resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsServiceName}), e2e.DefaultCluster(edsServiceName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{uint32(ports[0])})}, 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() // Ensure that the RPC is routed to the appropriate backend. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } // Update the aggregate cluster to point to a single DNS cluster. resources.Clusters = []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName}), e2e.DefaultCluster(edsServiceName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), } if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Ensure that start getting routed to the backend corresponding to the // LOGICAL_DNS cluster. for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)) if peer.Addr.String() == addrs[1].Addr { break } } if ctx.Err() != nil { t.Fatalf("Timeout when waiting for RPCs to be routed to backend %q in the DNS cluster", addrs[1].Addr) } } // TestAggregateCluster_BadEDS_GoodToBadDNS tests the case where the top-level // cluster is an aggregate cluster that resolves to an EDS and LOGICAL_DNS // cluster. The test first asserts that no RPCs can be made after receiving an // EDS response with zero endpoints because no update has been received from the // DNS resolver yet. Once the DNS resolver pushes an update, the test verifies // that we switch to the DNS cluster and can make a successful RPC. At this // point when the DNS cluster returns an error, the test verifies that RPCs are // still successful. This is the expected behavior because the cluster resolver // policy eats errors from DNS Resolver after it has returned an error. func (s) TestAggregateCluster_BadEDS_GoodToBadDNS(t *testing.T) { dnsTargetCh, dnsR := setupDNS(t) // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends. servers, cleanup := startTestServiceBackends(t, 2) defer cleanup() addrs, _ := backendAddressesAndPorts(t, servers) // Configure an aggregate cluster pointing to an EDS and LOGICAL_DNS // cluster. Also configure an endpoints resource for the EDS cluster which // triggers a NACK. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" dnsHostName = "dns_host" dnsPort = uint32(8080) ) emptyEndpointResource := e2e.DefaultEndpoint(edsServiceName, "localhost", nil) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, edsServiceName, e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{emptyEndpointResource}, 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() // Make an RPC with a short deadline. We expect this RPC to not succeed // because the EDS resource came back with no endpoints, and we are yet to // push an update through the DNS resolver. client := testgrpc.NewTestServiceClient(cc) sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout) defer sCancel() if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); status.Code(err) != codes.DeadlineExceeded { t.Fatalf("EmptyCall() code %s, want %s", status.Code(err), codes.DeadlineExceeded) } // Ensure that the DNS resolver is started for the expected target. select { case <-ctx.Done(): t.Fatal("Timeout when waiting for DNS resolver to be started") case target := <-dnsTargetCh: got, want := target.Endpoint(), fmt.Sprintf("%s:%d", dnsHostName, dnsPort) if got != want { t.Fatalf("DNS resolution started for target %q, want %q", got, want) } } // Update DNS resolver with test backend addresses. dnsR.UpdateState(resolver.State{Endpoints: addrsToEndpoints(addrs)}) // Ensure that RPCs start getting routed to the first backend since the // child policy for a LOGICAL_DNS cluster is pick_first by default. for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)); err != nil { t.Logf("EmptyCall() failed: %v", err) continue } if peer.Addr.String() == addrs[0].Addr { break } } if ctx.Err() != nil { t.Fatalf("Timeout when waiting for RPCs to be routed to backend %q in the DNS cluster", addrs[0].Addr) } // Push an error from the DNS resolver as well. dnsErr := fmt.Errorf("DNS error") dnsR.CC().ReportError(dnsErr) // Ensure that RPCs continue to succeed for the next second. for end := time.Now().Add(time.Second); time.Now().Before(end); <-time.After(defaultTestShortTimeout) { peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } } } // TestAggregateCluster_BadEDS_GoodToBadDNS tests the case where the top-level // cluster is an aggregate cluster that resolves to an EDS and LOGICAL_DNS // cluster. The test first sends an EDS response which triggers an NACK. Once // the DNS resolver pushes an update, the test verifies that we switch to the // DNS cluster and can make a successful RPC. func (s) TestAggregateCluster_BadEDSFromError_GoodToBadDNS(t *testing.T) { // Start an xDS management server and block it from continuously re-sending // the same EDS response that triggers a NACK. This is to ensure that the // test can proceed to the point of verifying that we fall back to the // LOGICAL_DNS cluster. More more details see: // https://github.com/grpc/grpc-go/issues/8994. ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() edsResponseSeen := atomic.Bool{} managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) { if resp.GetTypeUrl() != version.V3EndpointsURL { return } if edsResponseSeen.Load() { t.Logf("Received EDS response again, blocking to prevent test from busylooping on the same EDS response: %v", resp) <-ctx.Done() return } edsResponseSeen.Store(true) }, AllowResourceSubset: true, }) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start a test service backend. server := stubserver.StartTestService(t, nil) defer server.Stop() dnsHostName, dnsPort := hostAndPortFromAddress(t, server.Address) // Configure an aggregate cluster pointing to an EDS and LOGICAL_DNS // cluster. Also configure an empty endpoints resource for the EDS cluster // that contains no endpoints. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" ) nackEndpointResource := e2e.DefaultEndpoint(edsServiceName, "localhost", nil) nackEndpointResource.Endpoints = []*v3endpointpb.LocalityLbEndpoints{ { LoadBalancingWeight: &wrapperspb.UInt32Value{ Value: 0, // causes an NACK }, }, } resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, edsServiceName, e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{nackEndpointResource}, SkipValidation: true, } 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() // Ensure that RPCs start getting routed to the first backend since the // child policy for a LOGICAL_DNS cluster is pick_first by default. pickfirst.CheckRPCsToBackend(ctx, cc, resolver.Address{Addr: server.Address}) } // TestAggregateCluster_BadDNS_GoodEDS tests the case where the top-level // cluster is an aggregate cluster that resolves to an LOGICAL_DNS and EDS // cluster. When the DNS Resolver returns an error and EDS cluster returns a // good update, this test verifies the cluster_resolver balancer correctly falls // back from the LOGICAL_DNS cluster to the EDS cluster. func (s) TestAggregateCluster_BadDNS_GoodEDS(t *testing.T) { dnsTargetCh, dnsR := setupDNS(t) // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start a test service backend. server := stubserver.StartTestService(t, nil) defer server.Stop() _, edsPort := hostAndPortFromAddress(t, server.Address) // Configure an aggregate cluster pointing to an LOGICAL_DNS and EDS // cluster. Also configure an endpoints resource for the EDS cluster. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" dnsHostName = "bad.ip.v4.address" dnsPort = 8080 ) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{dnsClusterName, edsClusterName}), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), e2e.DefaultCluster(edsClusterName, edsServiceName, e2e.SecurityLevelNone), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{uint32(edsPort)})}, 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() // Ensure that the DNS resolver is started for the expected target. select { case <-ctx.Done(): t.Fatal("Timeout when waiting for DNS resolver to be started") case target := <-dnsTargetCh: got, want := target.Endpoint(), fmt.Sprintf("%s:%d", dnsHostName, dnsPort) if got != want { t.Fatalf("DNS resolution started for target %q, want %q", got, want) } } // Produce a bad resolver update from the DNS resolver. dnsErr := fmt.Errorf("DNS error") dnsR.CC().ReportError(dnsErr) // RPCs should work, higher level DNS cluster errors so should fallback to // EDS cluster. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != server.Address { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, server.Address) } } // TestAggregateCluster_BadEDS_BadDNS tests the case where the top-level cluster // is an aggregate cluster that resolves to an EDS and LOGICAL_DNS cluster. When // the EDS request returns a resource that contains no endpoints, the test // verifies that we switch to the DNS cluster. When the DNS cluster returns an // error, the test verifies that RPCs fail with the error triggered by the DNS // Discovery Mechanism (from sending an empty address list down). func (s) TestAggregateCluster_BadEDS_BadDNS(t *testing.T) { dnsTargetCh, dnsR := setupDNS(t) // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Configure an aggregate cluster pointing to an EDS and LOGICAL_DNS // cluster. Also configure an empty endpoints resource for the EDS cluster // that contains no endpoints. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" dnsHostName = "bad.ip.v4.address" dnsPort = 8080 ) emptyEndpointResource := e2e.DefaultEndpoint(edsServiceName, "localhost", nil) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, edsServiceName, e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{emptyEndpointResource}, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), 50*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() // Ensure that the DNS resolver is started for the expected target. select { case <-ctx.Done(): t.Fatal("Timeout when waiting for DNS resolver to be started") case target := <-dnsTargetCh: got, want := target.Endpoint(), fmt.Sprintf("%s:%d", dnsHostName, dnsPort) if got != want { t.Fatalf("DNS resolution started for target %q, want %q", got, want) } } // Produce a bad resolver update from the DNS resolver. dnsR.CC().ReportError(fmt.Errorf("DNS error")) // Ensure that the error from the DNS Resolver leads to an empty address // update for both priorities. client := testgrpc.NewTestServiceClient(cc) for ctx.Err() == nil { _, err := client.EmptyCall(ctx, &testpb.Empty{}) if err == nil { t.Fatal("EmptyCall() succeeded when expected to fail") } if status.Code(err) == codes.Unavailable && strings.Contains(err.Error(), "no targets to pick from") { break } } if ctx.Err() != nil { t.Fatalf("Timeout when waiting for RPCs to fail with expected code and error") } } // TestAggregateCluster_NoFallback_EDSNackedWithPreviousGoodUpdate tests the // scenario where the top-level cluster is an aggregate cluster that resolves to // an EDS and LOGICAL_DNS cluster. The management server first sends a good EDS // response for the EDS cluster and the test verifies that RPCs get routed to // the EDS cluster. The management server then sends a bad EDS response. The // test verifies that the cluster_resolver LB policy continues to use the // previously received good update and that RPCs still get routed to the EDS // cluster. func (s) TestAggregateCluster_NoFallback_EDSNackedWithPreviousGoodUpdate(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend is used for the EDS cluster and the second backend is used for // the LOGICAL_DNS cluster. servers, cleanup3 := startTestServiceBackends(t, 2) defer cleanup3() addrs, ports := backendAddressesAndPorts(t, servers) dnsHostName, dnsPort := hostAndPortFromAddress(t, servers[1].Address) // Configure an aggregate cluster pointing to an EDS and DNS cluster. Also // configure an endpoints resource for the EDS cluster. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" ) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsClusterName, "localhost", []uint32{uint32(ports[0])})}, 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() // Make an RPC and ensure that it gets routed to the first backend since the // EDS cluster is of higher priority than the LOGICAL_DNS cluster. client := testgrpc.NewTestServiceClient(cc) peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } // Push an EDS resource from the management server that is expected to be // NACKed by the xDS client. Since the cluster_resolver LB policy has a // previously received good EDS resource, it will continue to use that. resources.Endpoints[0].Endpoints[0].LbEndpoints[0].LoadBalancingWeight = &wrapperspb.UInt32Value{Value: 0} if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Ensure that RPCs continue to get routed to the EDS cluster 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{}, grpc.Peer(peer)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[0].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[0].Addr) } } } // TestAggregateCluster_Fallback_EDSNackedWithoutPreviousGoodUpdate tests the // scenario where the top-level cluster is an aggregate cluster that resolves to // an EDS and LOGICAL_DNS cluster. The management server sends a bad EDS // response. The test verifies that the cluster_resolver LB policy falls back to // the LOGICAL_DNS cluster, because it is supposed to treat the bad EDS response // as though it received an update with no endpoints. func (s) TestAggregateCluster_Fallback_EDSNackedWithoutPreviousGoodUpdate(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() // Start an xDS management server and block it from continuously re-sending // the same EDS response that triggers a NACK. This is to ensure that the // test can proceed to the point of verifying that we fall back to the // LOGICAL_DNS cluster. More more details see: // https://github.com/grpc/grpc-go/issues/8994. edsResponseSeen := atomic.Bool{} managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) { if resp.GetTypeUrl() != version.V3EndpointsURL { return } if edsResponseSeen.Load() { t.Logf("Received EDS response again, blocking to prevent test from busylooping on the same EDS response: %v", resp) <-ctx.Done() return } edsResponseSeen.Store(true) }, AllowResourceSubset: true, }) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start two test backends and extract their host and port. The first // backend is used for the EDS cluster and the second backend is used for // the LOGICAL_DNS cluster. servers, cleanup := startTestServiceBackends(t, 2) defer cleanup() addrs, ports := backendAddressesAndPorts(t, servers) dnsHostName, dnsPort := hostAndPortFromAddress(t, servers[1].Address) // Configure an aggregate cluster pointing to an EDS and DNS cluster. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" ) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsClusterName, "localhost", []uint32{uint32(ports[0])})}, SkipValidation: true, } // Set a load balancing weight of 0 for the backend in the EDS resource. // This is expected to be NACKed by the xDS client. Since the resources.Endpoints[0].Endpoints[0].LbEndpoints[0].LoadBalancingWeight = &wrapperspb.UInt32Value{Value: 0} 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() // Make an RPC and ensure that it gets routed to the LOGICAL_DNS cluster. peer := &peer.Peer{} client := testgrpc.NewTestServiceClient(cc) if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != addrs[1].Addr { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, addrs[1].Addr) } } // TestAggregateCluster_Fallback_EDS_ResourceNotFound tests the scenario where // the top-level cluster is an aggregate cluster that resolves to an EDS and // LOGICAL_DNS cluster. The management server does not respond with the EDS // cluster. The test verifies that the cluster_resolver LB policy falls back to // the LOGICAL_DNS cluster in this case. func (s) TestAggregateCluster_Fallback_EDS_ResourceNotFound(t *testing.T) { // Start an xDS management server. managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true}) // Create bootstrap configuration pointing to the above management server. nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) // Start a test backend for the LOGICAL_DNS cluster. server := stubserver.StartTestService(t, nil) defer server.Stop() dnsHostName, dnsPort := hostAndPortFromAddress(t, server.Address) // Configure an aggregate cluster pointing to an EDS and DNS cluster. No // endpoints are configured for the EDS cluster. const ( edsClusterName = clusterName + "-eds" dnsClusterName = clusterName + "-dns" ) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}), e2e.DefaultCluster(edsClusterName, "", e2e.SecurityLevelNone), makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort), }, SkipValidation: true, } ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Create an xDS client talking to the above management server, configured // with a short watch expiry timeout. config, err := bootstrap.NewConfigFromContents(bootstrapContents) if err != nil { t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err) } pool := xdsclient.NewPool(config) // Create a new xDS client with a short watch expiry to quickly detect the // missing endpoints resource. xdsClient, close, err := pool.NewClientForTesting(xdsclient.OptionsForTesting{ Name: t.Name(), WatchExpiryTimeout: defaultTestWatchExpiryTimeout, }) if err != nil { t.Fatalf("Failed to create an xDS client: %v", err) } defer close() if internal.NewXDSResolverWithClientForTesting == nil { t.Fatalf("internal.NewXDSResolverWithClientForTesting is nil") } r, err := internal.NewXDSResolverWithClientForTesting.(func(xdsclient.XDSClient) (resolver.Builder, error))(xdsClient) if err != nil { t.Fatalf("failed to create resolver") } 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() defer cc.Close() // Make an RPC and ensure that it gets routed to the LOGICAL_DNS cluster. // Even though the EDS cluster is of higher priority, since the management // server does not respond with an EDS resource, the cluster_resolver LB // policy is expected to fallback to the LOGICAL_DNS cluster once the watch // timeout expires. peer := &peer.Peer{} client := testgrpc.NewTestServiceClient(cc) if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer), grpc.WaitForReady(true)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if peer.Addr.String() != server.Address { t.Fatalf("EmptyCall() routed to backend %q, want %q", peer.Addr, server.Address) } } func addrsToEndpoints(addrs []resolver.Address) []resolver.Endpoint { endpoints := make([]resolver.Endpoint, len(addrs)) for i, addr := range addrs { endpoints[i] = resolver.Endpoint{Addresses: []resolver.Address{addr}} } return endpoints } // Tests that when an aggregate cluster (Root -> [PrimaryEDS, SecondaryEDS]) has // LRS enabled on both leaf clusters: // 1. LRS reports are sent only for the primary leaf cluster while healthy. // 2. When the primary leaf cluster fails, traffic shifts to the secondary leaf // cluster and LRS stats are reported for the secondary leaf cluster. func (s) TestAggregateCluster_LRS(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) defer cancel() const ( clusterName1 = clusterName + "-cluster-1" clusterName2 = clusterName + "-cluster-2" ) managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{ AllowResourceSubset: true, SupportLoadReportingService: true, }) nodeID := uuid.New().String() bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address) servers, cleanup := startTestServiceBackends(t, 2) defer cleanup() _, ports := backendAddressesAndPorts(t, servers) resources := e2e.UpdateOptions{ NodeID: nodeID, Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)}, Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)}, Clusters: []*v3clusterpb.Cluster{ makeAggregateClusterResource(clusterName, []string{clusterName1, clusterName2}), e2e.ClusterResourceWithOptions(e2e.ClusterOptions{ ClusterName: clusterName1, Type: e2e.ClusterTypeEDS, EnableLRS: true, }), e2e.ClusterResourceWithOptions(e2e.ClusterOptions{ ClusterName: clusterName2, Type: e2e.ClusterTypeEDS, EnableLRS: true, }), }, Endpoints: []*v3endpointpb.ClusterLoadAssignment{ e2e.DefaultEndpoint(clusterName1, "localhost", []uint32{uint32(ports[0])}), e2e.DefaultEndpoint(clusterName2, "localhost", []uint32{uint32(ports[1])}), }, } if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } cc, cleanup := setupAndDial(t, bootstrapContents) defer cleanup() client := testgrpc.NewTestServiceClient(cc) // Ensure LRS stream is opened. if _, err := managementServer.LRSServer.LRSStreamOpenChan.Receive(ctx); err != nil { t.Fatalf("Timeout waiting for LRS stream open: %v", err) } if _, err := managementServer.LRSServer.LRSRequestChan.Receive(ctx); err != nil { t.Fatalf("Timeout waiting for initial LRS request: %v", err) } // Make RPC calls and verify every call reaches the primary cluster // (clusterName1). for i := 0; i < 10; i++ { peer := &peer.Peer{} if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)); err != nil { t.Fatalf("EmptyCall() failed: %v", err) } if got, want := peer.Addr.String(), servers[0].Address; got != want { t.Fatalf("EmptyCall() call #%d routed to %q, want %q", i, got, want) } } // Send LRS response requesting load reporting every 10ms. managementServer.LRSServer.LRSResponseChan <- &fakeserver.Response{ Resp: &v3lrspb.LoadStatsResponse{ SendAllClusters: true, LoadReportingInterval: durationpb.New(defaultLoadReportingInterval), }, } // Verify LRS reports load for clusterName1 and 0 load for clusterName2. wantClusterStats := &v3endpointpb.ClusterStats{ ClusterName: clusterName1, UpstreamLocalityStats: []*v3endpointpb.UpstreamLocalityStats{ { Locality: &v3corepb.Locality{Region: "region-1", Zone: "zone-1", SubZone: "subzone-1"}, TotalSuccessfulRequests: 10, TotalIssuedRequests: 10, }, }, } waitForPrimaryStats: for { select { case <-ctx.Done(): t.Fatalf("Timeout waiting for LRS load report for %q", clusterName1) case req := <-managementServer.LRSServer.LRSRequestChan.C: loadStats := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) if len(loadStats.ClusterStats) == 0 { continue } opts := []cmp.Option{ protocmp.Transform(), protocmp.IgnoreFields(&v3endpointpb.ClusterStats{}, "load_report_interval"), } if diff := cmp.Diff([]*v3endpointpb.ClusterStats{wantClusterStats}, loadStats.ClusterStats, opts...); diff != "" { t.Fatalf("Unexpected diff in LRS ClusterStats for %q (-want +got):\n%s", clusterName1, diff) } break waitForPrimaryStats } } // Fail primary cluster by clearing endpoints for clusterName1. resources.Endpoints = []*v3endpointpb.ClusterLoadAssignment{ e2e.DefaultEndpoint(clusterName1, "localhost", nil), e2e.DefaultEndpoint(clusterName2, "localhost", []uint32{uint32(ports[1])}), } if err := managementServer.Update(ctx, resources); err != nil { t.Fatal(err) } // Make RPCs and verify failover to secondary backend. peer := &peer.Peer{} for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) { if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(peer)); err == nil && peer.Addr.String() == servers[1].Address { break } } if ctx.Err() != nil { t.Fatalf("Timeout waiting for RPCs to switch to secondary backend %q", servers[1].Address) } // Verify LRS report now includes load for secondary cluster (clusterName2). wantClusterStats = &v3endpointpb.ClusterStats{ ClusterName: clusterName2, UpstreamLocalityStats: []*v3endpointpb.UpstreamLocalityStats{ { Locality: &v3corepb.Locality{Region: "region-1", Zone: "zone-1", SubZone: "subzone-1"}, TotalSuccessfulRequests: 1, }, }, } for { select { case <-ctx.Done(): t.Fatalf("Timeout waiting for LRS load report for secondary cluster %q", clusterName2) case req := <-managementServer.LRSServer.LRSRequestChan.C: loadStats := req.(*fakeserver.Request).Req.(*v3lrspb.LoadStatsRequest) for _, load := range loadStats.ClusterStats { if load.ClusterName != clusterName2 { continue } // The periodic LRS timer may fire while a failover RPC is // in flight, producing a transitional report with 0 // successful requests. Skip such reports until the report // with the completed RPC arrives. if len(load.UpstreamLocalityStats) == 0 || load.UpstreamLocalityStats[0].TotalSuccessfulRequests == 0 { continue } opts := []cmp.Option{ protocmp.Transform(), protocmp.IgnoreFields(&v3endpointpb.ClusterStats{}, "load_report_interval"), protocmp.IgnoreFields(&v3endpointpb.UpstreamLocalityStats{}, "total_issued_requests", "total_requests_in_progress"), } if diff := cmp.Diff(wantClusterStats, load, opts...); diff != "" { t.Fatalf("Unexpected diff in LRS ClusterStats for %q (-want +got):\n%s", clusterName2, diff) } return } } } }