Files
grpc-go/xds/internal/resolver/xds_resolver_test.go

383 lines
12 KiB
Go

/*
*
* Copyright 2019 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 resolver
import (
"context"
"errors"
"fmt"
"net"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
xdsinternal "google.golang.org/grpc/xds/internal"
_ "google.golang.org/grpc/xds/internal/balancer/cdsbalancer" // To parse LB config
xdsclient "google.golang.org/grpc/xds/internal/client"
"google.golang.org/grpc/xds/internal/client/bootstrap"
"google.golang.org/grpc/xds/internal/testutils"
"google.golang.org/grpc/xds/internal/testutils/fakeclient"
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
)
const (
targetStr = "target"
cluster = "cluster"
balancerName = "dummyBalancer"
)
var (
validConfig = bootstrap.Config{
BalancerName: balancerName,
Creds: grpc.WithInsecure(),
NodeProto: &corepb.Node{},
}
target = resolver.Target{Endpoint: targetStr}
)
func TestRegister(t *testing.T) {
for _, s := range []string{"xds", "xds-experimental"} {
b := resolver.Get(s)
if b == nil {
t.Errorf("scheme %v is not registered", s)
}
}
}
// testClientConn is a fake implemetation of resolver.ClientConn. All is does
// is to store the state received from the resolver locally and signal that
// event through a channel.
type testClientConn struct {
resolver.ClientConn
stateCh *testutils.Channel
errorCh *testutils.Channel
}
func (t *testClientConn) UpdateState(s resolver.State) {
t.stateCh.Send(s)
}
func (t *testClientConn) ReportError(err error) {
t.errorCh.Send(err)
}
func (t *testClientConn) ParseServiceConfig(jsonSC string) *serviceconfig.ParseResult {
return internal.ParseServiceConfigForTesting.(func(string) *serviceconfig.ParseResult)(jsonSC)
}
func newTestClientConn() *testClientConn {
return &testClientConn{
stateCh: testutils.NewChannel(),
errorCh: testutils.NewChannel(),
}
}
func getXDSClientMakerFunc(wantOpts xdsclient.Options) func(xdsclient.Options) (xdsClientInterface, error) {
return func(gotOpts xdsclient.Options) (xdsClientInterface, error) {
if gotOpts.Config.BalancerName != wantOpts.Config.BalancerName {
return nil, fmt.Errorf("got balancerName: %s, want: %s", gotOpts.Config.BalancerName, wantOpts.Config.BalancerName)
}
// We cannot compare two DialOption objects to see if they are equal
// because each of these is a function pointer. So, the only thing we
// can do here is to check if the got option is nil or not based on
// what the want option is. We should be able to do extensive
// credential testing in e2e tests.
if (gotOpts.Config.Creds != nil) != (wantOpts.Config.Creds != nil) {
return nil, fmt.Errorf("got len(creds): %s, want: %s", gotOpts.Config.Creds, wantOpts.Config.Creds)
}
if len(gotOpts.DialOpts) != len(wantOpts.DialOpts) {
return nil, fmt.Errorf("got len(DialOpts): %v, want: %v", len(gotOpts.DialOpts), len(wantOpts.DialOpts))
}
return fakeclient.NewClient(), nil
}
}
func errorDialer(_ context.Context, _ string) (net.Conn, error) {
return nil, errors.New("dial error")
}
// TestResolverBuilder tests the xdsResolverBuilder's Build method with
// different parameters.
func TestResolverBuilder(t *testing.T) {
tests := []struct {
name string
rbo resolver.BuildOptions
config bootstrap.Config
xdsClientFunc func(xdsclient.Options) (xdsClientInterface, error)
wantErr bool
}{
{
name: "empty-config",
rbo: resolver.BuildOptions{},
config: bootstrap.Config{},
wantErr: true,
},
{
name: "no-balancer-name-in-config",
rbo: resolver.BuildOptions{},
config: bootstrap.Config{
Creds: grpc.WithInsecure(),
NodeProto: &corepb.Node{},
},
wantErr: true,
},
{
name: "no-creds-in-config",
rbo: resolver.BuildOptions{},
config: bootstrap.Config{
BalancerName: balancerName,
NodeProto: &corepb.Node{},
},
xdsClientFunc: getXDSClientMakerFunc(xdsclient.Options{Config: validConfig}),
wantErr: false,
},
{
name: "error-dialer-in-rbo",
rbo: resolver.BuildOptions{Dialer: errorDialer},
config: validConfig,
xdsClientFunc: getXDSClientMakerFunc(xdsclient.Options{
Config: validConfig,
DialOpts: []grpc.DialOption{grpc.WithContextDialer(errorDialer)},
}),
wantErr: false,
},
{
name: "simple-good",
rbo: resolver.BuildOptions{},
config: validConfig,
xdsClientFunc: getXDSClientMakerFunc(xdsclient.Options{Config: validConfig}),
wantErr: false,
},
{
name: "newXDSClient-throws-error",
rbo: resolver.BuildOptions{},
config: validConfig,
xdsClientFunc: func(_ xdsclient.Options) (xdsClientInterface, error) {
return nil, errors.New("newXDSClient-throws-error")
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Fake out the bootstrap process by providing our own config.
oldConfigMaker := newXDSConfig
newXDSConfig = func() (*bootstrap.Config, error) {
if test.config.BalancerName == "" {
return nil, fmt.Errorf("no balancer name found in config")
}
return &test.config, nil
}
// Fake out the xdsClient creation process by providing a fake.
oldClientMaker := newXDSClient
newXDSClient = test.xdsClientFunc
defer func() {
newXDSConfig = oldConfigMaker
newXDSClient = oldClientMaker
}()
builder := resolver.Get(xdsScheme)
if builder == nil {
t.Fatalf("resolver.Get(%v) returned nil", xdsScheme)
}
r, err := builder.Build(target, newTestClientConn(), test.rbo)
if (err != nil) != test.wantErr {
t.Fatalf("builder.Build(%v) returned err: %v, wantErr: %v", target, err, test.wantErr)
}
if err != nil {
// This is the case where we expect an error and got it.
return
}
r.Close()
})
}
}
type setupOpts struct {
config *bootstrap.Config
xdsClientFunc func(xdsclient.Options) (xdsClientInterface, error)
}
func testSetup(t *testing.T, opts setupOpts) (*xdsResolver, *testClientConn, func()) {
t.Helper()
oldConfigMaker := newXDSConfig
newXDSConfig = func() (*bootstrap.Config, error) { return opts.config, nil }
oldClientMaker := newXDSClient
newXDSClient = opts.xdsClientFunc
cancel := func() {
newXDSConfig = oldConfigMaker
newXDSClient = oldClientMaker
}
builder := resolver.Get(xdsScheme)
if builder == nil {
t.Fatalf("resolver.Get(%v) returned nil", xdsScheme)
}
tcc := newTestClientConn()
r, err := builder.Build(target, tcc, resolver.BuildOptions{})
if err != nil {
t.Fatalf("builder.Build(%v) returned err: %v", target, err)
}
return r.(*xdsResolver), tcc, cancel
}
// waitForWatchService waits for the WatchService method to be called on the
// xdsClient within a reasonable amount of time, and also verifies that the
// watch is called with the expected target.
func waitForWatchService(t *testing.T, xdsC *fakeclient.Client, wantTarget string) {
t.Helper()
gotTarget, err := xdsC.WaitForWatchService()
if err != nil {
t.Fatalf("xdsClient.WatchService failed with error: %v", err)
}
if gotTarget != wantTarget {
t.Fatalf("xdsClient.WatchService() called with target: %v, want %v", gotTarget, wantTarget)
}
}
// TestXDSResolverWatchCallbackAfterClose tests the case where a service update
// from the underlying xdsClient is received after the resolver is closed.
func TestXDSResolverWatchCallbackAfterClose(t *testing.T) {
xdsC := fakeclient.NewClient()
xdsR, tcc, cancel := testSetup(t, setupOpts{
config: &validConfig,
xdsClientFunc: func(_ xdsclient.Options) (xdsClientInterface, error) { return xdsC, nil },
})
defer cancel()
waitForWatchService(t, xdsC, targetStr)
// Call the watchAPI callback after closing the resolver, and make sure no
// update is triggerred on the ClientConn.
xdsR.Close()
xdsC.InvokeWatchServiceCallback(cluster, nil)
if gotVal, gotErr := tcc.stateCh.Receive(); gotErr != testutils.ErrRecvTimeout {
t.Fatalf("ClientConn.UpdateState called after xdsResolver is closed: %v", gotVal)
}
}
// TestXDSResolverBadServiceUpdate tests the case the xdsClient returns a bad
// service update.
func TestXDSResolverBadServiceUpdate(t *testing.T) {
xdsC := fakeclient.NewClient()
xdsR, tcc, cancel := testSetup(t, setupOpts{
config: &validConfig,
xdsClientFunc: func(_ xdsclient.Options) (xdsClientInterface, error) { return xdsC, nil },
})
defer func() {
cancel()
xdsR.Close()
}()
waitForWatchService(t, xdsC, targetStr)
// Invoke the watchAPI callback with a bad service update and wait for the
// ReportError method to be called on the ClientConn.
suErr := errors.New("bad serviceupdate")
xdsC.InvokeWatchServiceCallback("", suErr)
if gotErrVal, gotErr := tcc.errorCh.Receive(); gotErr != nil || gotErrVal != suErr {
t.Fatalf("ClientConn.ReportError() received %v, want %v", gotErrVal, suErr)
}
}
// TestXDSResolverGoodServiceUpdate tests the happy case where the resolver
// gets a good service update from the xdsClient.
func TestXDSResolverGoodServiceUpdate(t *testing.T) {
xdsC := fakeclient.NewClient()
xdsR, tcc, cancel := testSetup(t, setupOpts{
config: &validConfig,
xdsClientFunc: func(_ xdsclient.Options) (xdsClientInterface, error) { return xdsC, nil },
})
defer func() {
cancel()
xdsR.Close()
}()
waitForWatchService(t, xdsC, targetStr)
// Invoke the watchAPI callback with a good service update and wait for the
// UpdateState method to be called on the ClientConn.
xdsC.InvokeWatchServiceCallback(cluster, nil)
gotState, err := tcc.stateCh.Receive()
if err != nil {
t.Fatalf("ClientConn.UpdateState returned error: %v", err)
}
rState := gotState.(resolver.State)
if gotClient := rState.Attributes.Value(xdsinternal.XDSClientID); gotClient != xdsC {
t.Fatalf("ClientConn.UpdateState got xdsClient: %v, want %v", gotClient, xdsC)
}
if err := rState.ServiceConfig.Err; err != nil {
t.Fatalf("ClientConn.UpdateState received error in service config: %v", rState.ServiceConfig.Err)
}
}
// TestXDSResolverUpdates tests the cases where the resolver gets a good update
// after an error, and an error after the good update.
func TestXDSResolverGoodUpdateAfterError(t *testing.T) {
xdsC := fakeclient.NewClient()
xdsR, tcc, cancel := testSetup(t, setupOpts{
config: &validConfig,
xdsClientFunc: func(_ xdsclient.Options) (xdsClientInterface, error) { return xdsC, nil },
})
defer func() {
cancel()
xdsR.Close()
}()
waitForWatchService(t, xdsC, targetStr)
// Invoke the watchAPI callback with a bad service update and wait for the
// ReportError method to be called on the ClientConn.
suErr := errors.New("bad serviceupdate")
xdsC.InvokeWatchServiceCallback("", suErr)
if gotErrVal, gotErr := tcc.errorCh.Receive(); gotErr != nil || gotErrVal != suErr {
t.Fatalf("ClientConn.ReportError() received %v, want %v", gotErrVal, suErr)
}
// Invoke the watchAPI callback with a good service update and wait for the
// UpdateState method to be called on the ClientConn.
xdsC.InvokeWatchServiceCallback(cluster, nil)
gotState, err := tcc.stateCh.Receive()
if err != nil {
t.Fatalf("ClientConn.UpdateState returned error: %v", err)
}
rState := gotState.(resolver.State)
if gotClient := rState.Attributes.Value(xdsinternal.XDSClientID); gotClient != xdsC {
t.Fatalf("ClientConn.UpdateState got xdsClient: %v, want %v", gotClient, xdsC)
}
if err := rState.ServiceConfig.Err; err != nil {
t.Fatalf("ClientConn.UpdateState received error in service config: %v", rState.ServiceConfig.Err)
}
// Invoke the watchAPI callback with a bad service update and wait for the
// ReportError method to be called on the ClientConn.
suErr2 := errors.New("bad serviceupdate 2")
xdsC.InvokeWatchServiceCallback("", suErr2)
if gotErrVal, gotErr := tcc.errorCh.Receive(); gotErr != nil || gotErrVal != suErr2 {
t.Fatalf("ClientConn.ReportError() received %v, want %v", gotErrVal, suErr2)
}
}