Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
1468 lines
60 KiB
Go
1468 lines
60 KiB
Go
/*
|
|
* 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
|
|
}
|
|
}
|
|
}
|
|
}
|