xds: Initial implementation of a client using the v2 API (#3144)
This object will be used by a higher level xdsClient object, which will provide the watch API used by the xds resolver and balancer implementations.
This commit is contained in:

committed by
GitHub

parent
460b1d2ced
commit
dc9615bb06
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module google.golang.org/grpc
|
||||
go 1.11
|
||||
|
||||
require (
|
||||
github.com/envoyproxy/go-control-plane v0.9.0
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
|
||||
github.com/golang/mock v1.1.1
|
||||
|
4
go.sum
4
go.sum
@ -3,8 +3,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0 h1:67WMNTvGrl7V1dWdKCeTwxDr7nio9clKoTlLhwIPnT4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
|
156
xds/internal/client/fakexds/fakexds.go
Normal file
156
xds/internal/client/fakexds/fakexds.go
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
*
|
||||
* 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 fakexds provides a very basic fake implementation of the xDS server
|
||||
// for unit testing purposes.
|
||||
package fakexds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
discoverypb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
|
||||
)
|
||||
|
||||
// TODO: Make this a var or a field in the server if there is a need to use a
|
||||
// value other than this default.
|
||||
const defaultChannelBufferSize = 50
|
||||
|
||||
// Request wraps an xDS request and error.
|
||||
type Request struct {
|
||||
Req *discoverypb.DiscoveryRequest
|
||||
Err error
|
||||
}
|
||||
|
||||
// Response wraps an xDS response and error.
|
||||
type Response struct {
|
||||
Resp *discoverypb.DiscoveryResponse
|
||||
Err error
|
||||
}
|
||||
|
||||
// Server is a very basic implementation of a fake xDS server. It provides a
|
||||
// request and response channel for the user to control the requests that are
|
||||
// expected and the responses that needs to be sent out.
|
||||
type Server struct {
|
||||
// RequestChan is a buffered channel on which the fake server writes the
|
||||
// received requests onto.
|
||||
RequestChan chan *Request
|
||||
// ResponseChan is a buffered channel from which the fake server reads the
|
||||
// responses that it must send out to the client.
|
||||
ResponseChan chan *Response
|
||||
}
|
||||
|
||||
// StartClientAndServer starts a fakexds.Server and creates a ClientConn
|
||||
// talking to it. The returned cleanup function should be invoked by the caller
|
||||
// once the test is done.
|
||||
func StartClientAndServer(t *testing.T) (*Server, *grpc.ClientConn, func()) {
|
||||
t.Helper()
|
||||
|
||||
var lis net.Listener
|
||||
var err error
|
||||
lis, err = net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen() failed: %v", err)
|
||||
}
|
||||
|
||||
server := grpc.NewServer()
|
||||
fs := &Server{
|
||||
RequestChan: make(chan *Request, defaultChannelBufferSize),
|
||||
ResponseChan: make(chan *Response, defaultChannelBufferSize),
|
||||
}
|
||||
adsgrpc.RegisterAggregatedDiscoveryServiceServer(server, fs)
|
||||
go server.Serve(lis)
|
||||
t.Logf("Starting fake xDS server at %v...", lis.Addr().String())
|
||||
defer func() {
|
||||
if err != nil {
|
||||
server.Stop()
|
||||
lis.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cc *grpc.ClientConn
|
||||
cc, err = grpc.DialContext(ctx, lis.Addr().String(), grpc.WithInsecure(), grpc.WithBlock())
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.DialContext(%s) failed: %v", lis.Addr().String(), err)
|
||||
}
|
||||
t.Log("Started xDS gRPC client...")
|
||||
|
||||
return fs, cc, func() {
|
||||
server.Stop()
|
||||
lis.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// StreamAggregatedResources is the fake implementation to handle an ADS
|
||||
// stream.
|
||||
func (fs *Server) StreamAggregatedResources(s adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesServer) error {
|
||||
errCh := make(chan error, 2)
|
||||
go func() {
|
||||
for {
|
||||
req, err := s.Recv()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
fs.RequestChan <- &Request{req, err}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
var retErr error
|
||||
defer func() {
|
||||
errCh <- retErr
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case r := <-fs.ResponseChan:
|
||||
if r.Err != nil {
|
||||
retErr = r.Err
|
||||
return
|
||||
}
|
||||
if err := s.Send(r.Resp); err != nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
case <-s.Context().Done():
|
||||
retErr = s.Context().Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeltaAggregatedResources helps implement the ADS service.
|
||||
func (fs *Server) DeltaAggregatedResources(adsgrpc.AggregatedDiscoveryService_DeltaAggregatedResourcesServer) error {
|
||||
return status.Error(codes.Unimplemented, "")
|
||||
}
|
125
xds/internal/client/lds.go
Normal file
125
xds/internal/client/lds.go
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
httppb "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
|
||||
)
|
||||
|
||||
// newLDSRequest generates an LDS request proto for the provided target, to be
|
||||
// sent out on the wire.
|
||||
func (v2c *v2Client) newLDSRequest(target []string) *xdspb.DiscoveryRequest {
|
||||
return &xdspb.DiscoveryRequest{
|
||||
Node: v2c.nodeProto,
|
||||
TypeUrl: listenerURL,
|
||||
ResourceNames: target,
|
||||
}
|
||||
}
|
||||
|
||||
// sendLDS sends an LDS request for provided target on the provided stream.
|
||||
func (v2c *v2Client) sendLDS(stream adsStream, target []string) bool {
|
||||
if err := stream.Send(v2c.newLDSRequest(target)); err != nil {
|
||||
grpclog.Warningf("xds: LDS request for resource %v failed: %v", target, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleLDSResponse processes an LDS response received from the xDS server. On
|
||||
// receipt of a good response, it also invokes the registered watcher callback.
|
||||
func (v2c *v2Client) handleLDSResponse(resp *xdspb.DiscoveryResponse) error {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
|
||||
wi := v2c.watchMap[ldsResource]
|
||||
if wi == nil {
|
||||
return fmt.Errorf("xds: no LDS watcher found when handling LDS response: %+v", resp)
|
||||
}
|
||||
|
||||
routeName := ""
|
||||
for _, r := range resp.GetResources() {
|
||||
var resource ptypes.DynamicAny
|
||||
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
|
||||
return fmt.Errorf("xds: failed to unmarshal resource in LDS response: %v", err)
|
||||
}
|
||||
lis, ok := resource.Message.(*xdspb.Listener)
|
||||
if !ok {
|
||||
return fmt.Errorf("xds: unexpected resource type: %T in LDS response", resource.Message)
|
||||
}
|
||||
if lis.GetName() != wi.target[0] {
|
||||
// We ignore listeners we are not watching for because LDS is
|
||||
// special in the sense that there is only one resource we are
|
||||
// interested in, and this resource does not change over the
|
||||
// lifetime of the v2Client. So, we don't have to cache other
|
||||
// listeners which we are not interested in.
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
routeName, err = getRouteConfigNameFromListener(lis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if routeName == "" {
|
||||
err = fmt.Errorf("xds: LDS target %s not found in received response %+v", wi.target, resp)
|
||||
}
|
||||
wi.expiryTimer.Stop()
|
||||
wi.callback.(ldsCallback)(ldsUpdate{routeName: routeName}, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRouteConfigNameFromListener checks if the provided Listener proto meets
|
||||
// the expected criteria. If so, it returns a non-empty routeConfigName.
|
||||
func getRouteConfigNameFromListener(lis *xdspb.Listener) (string, error) {
|
||||
if lis.GetApiListener() == nil {
|
||||
return "", fmt.Errorf("xds: no api_listener field in LDS response %+v", lis)
|
||||
}
|
||||
var apiAny ptypes.DynamicAny
|
||||
if err := ptypes.UnmarshalAny(lis.GetApiListener().GetApiListener(), &apiAny); err != nil {
|
||||
return "", fmt.Errorf("xds: failed to unmarshal api_listner in LDS response: %v", err)
|
||||
}
|
||||
apiLis, ok := apiAny.Message.(*httppb.HttpConnectionManager)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("xds: unexpected api_listener type: %T in LDS response", apiAny.Message)
|
||||
}
|
||||
switch apiLis.RouteSpecifier.(type) {
|
||||
case *httppb.HttpConnectionManager_Rds:
|
||||
name := apiLis.GetRds().GetRouteConfigName()
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("xds: empty route_config_name in LDS response: %+v", lis)
|
||||
}
|
||||
return name, nil
|
||||
case *httppb.HttpConnectionManager_RouteConfig:
|
||||
// TODO: Add support for specifying the RouteConfiguration inline
|
||||
// in the LDS response.
|
||||
return "", fmt.Errorf("xds: LDS response contains RDS config inline. Not supported for now: %+v", apiLis)
|
||||
case nil:
|
||||
return "", fmt.Errorf("xds: no RouteSpecifier in received LDS response: %+v", apiLis)
|
||||
default:
|
||||
return "", fmt.Errorf("xds: unsupported type %T for RouteSpecifier in received LDS response", apiLis.RouteSpecifier)
|
||||
}
|
||||
}
|
270
xds/internal/client/lds_test.go
Normal file
270
xds/internal/client/lds_test.go
Normal file
@ -0,0 +1,270 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
"google.golang.org/grpc/xds/internal/client/fakexds"
|
||||
)
|
||||
|
||||
func TestGetRouteConfigNameFromListener(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lis *xdspb.Listener
|
||||
wantRoute string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no-apiListener-field",
|
||||
lis: &xdspb.Listener{},
|
||||
wantRoute: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "badly-marshaled-apiListener",
|
||||
lis: badAPIListener1,
|
||||
wantRoute: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong-type-in-apiListener",
|
||||
lis: badResourceListener,
|
||||
wantRoute: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty-httpConnMgr-in-apiListener",
|
||||
lis: listenerWithEmptyHTTPConnMgr,
|
||||
wantRoute: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "scopedRoutes-routeConfig-in-apiListener",
|
||||
lis: listenerWithScopedRoutesRouteConfig,
|
||||
wantRoute: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "goodListener1",
|
||||
lis: goodListener1,
|
||||
wantRoute: goodRouteName1,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gotRoute, err := getRouteConfigNameFromListener(test.lis)
|
||||
if gotRoute != test.wantRoute {
|
||||
t.Errorf("%s: getRouteConfigNameFromListener(%+v) = %v, want %v", test.name, test.lis, gotRoute, test.wantRoute)
|
||||
}
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("%s: getRouteConfigNameFromListener(%+v) = %v, want %v", test.name, test.lis, err, test.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleLDSResponse starts a fake xDS server, makes a ClientConn to it,
|
||||
// and creates a v2Client using it. Then, it registers a watchLDS and tests
|
||||
// different LDS responses.
|
||||
func TestHandleLDSResponse(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ldsResponse *xdspb.DiscoveryResponse
|
||||
wantErr bool
|
||||
wantUpdate *ldsUpdate
|
||||
wantUpdateErr bool
|
||||
}{
|
||||
// Badly marshaled LDS response.
|
||||
{
|
||||
name: "badly-marshaled-response",
|
||||
ldsResponse: badlyMarshaledLDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response does not contain Listener proto.
|
||||
{
|
||||
name: "no-listener-proto-in-response",
|
||||
ldsResponse: badResourceTypeInLDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// No APIListener in the response. Just one test case here for a bad
|
||||
// ApiListener, since the others are covered in
|
||||
// TestGetRouteConfigNameFromListener.
|
||||
{
|
||||
name: "no-apiListener-in-response",
|
||||
ldsResponse: noAPIListenerLDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains one listener and it is good.
|
||||
{
|
||||
name: "one-good-listener",
|
||||
ldsResponse: goodLDSResponse1,
|
||||
wantErr: false,
|
||||
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains multiple good listeners, including the one we are
|
||||
// interested in.
|
||||
{
|
||||
name: "multiple-good-listener",
|
||||
ldsResponse: ldsResponseWithMultipleResources,
|
||||
wantErr: false,
|
||||
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains two good listeners (one interesting and one
|
||||
// uninteresting), and one badly marshaled listener.
|
||||
{
|
||||
name: "good-bad-ugly-listeners",
|
||||
ldsResponse: goodBadUglyLDSResponse,
|
||||
wantErr: false,
|
||||
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains one listener, but we are not interested in it.
|
||||
{
|
||||
name: "one-uninteresting-listener",
|
||||
ldsResponse: goodLDSResponse2,
|
||||
wantErr: false,
|
||||
wantUpdate: &ldsUpdate{routeName: ""},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
// Response constains no resources. This is the case where the server
|
||||
// does not know about the target we are interested in.
|
||||
{
|
||||
name: "empty-response",
|
||||
ldsResponse: emptyLDSResponse,
|
||||
wantErr: false,
|
||||
wantUpdate: &ldsUpdate{routeName: ""},
|
||||
wantUpdateErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gotUpdateCh := make(chan ldsUpdate, 1)
|
||||
gotUpdateErrCh := make(chan error, 1)
|
||||
|
||||
// Register a watcher, to trigger the v2Client to send an LDS request.
|
||||
cancelWatch := v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("%s: in v2c.watchLDS callback, ldsUpdate: %+v, err: %v", test.name, u, err)
|
||||
gotUpdateCh <- u
|
||||
gotUpdateErrCh <- err
|
||||
})
|
||||
|
||||
// Wait till the request makes it to the fakeServer. This ensures that
|
||||
// the watch request has been processed by the v2Client.
|
||||
<-fakeServer.RequestChan
|
||||
|
||||
// Directly push the response through a call to handleLDSResponse,
|
||||
// thereby bypassing the fakeServer.
|
||||
if err := v2c.handleLDSResponse(test.ldsResponse); (err != nil) != test.wantErr {
|
||||
t.Fatalf("%s: v2c.handleLDSResponse() returned err: %v, wantErr: %v", test.name, err, test.wantErr)
|
||||
}
|
||||
|
||||
// If the test needs the callback to be invoked, verify the update and
|
||||
// error pushed to the callback.
|
||||
if test.wantUpdate != nil {
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting LDS update")
|
||||
case gotUpdate := <-gotUpdateCh:
|
||||
timer.Stop()
|
||||
if !reflect.DeepEqual(gotUpdate, *test.wantUpdate) {
|
||||
t.Fatalf("%s: got LDS update : %+v, want %+v", test.name, gotUpdate, *test.wantUpdate)
|
||||
}
|
||||
}
|
||||
// Since the callback that we registered pushes to both channels at
|
||||
// the same time, this channel read should return immediately.
|
||||
gotUpdateErr := <-gotUpdateErrCh
|
||||
if (gotUpdateErr != nil) != test.wantUpdateErr {
|
||||
t.Fatalf("%s: got LDS update error {%v}, wantErr: %v", test.name, gotUpdateErr, test.wantUpdateErr)
|
||||
}
|
||||
}
|
||||
cancelWatch()
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleLDSResponseWithoutWatch tests the case where the v2Client receives
|
||||
// an LDS response without a registered watcher.
|
||||
func TestHandleLDSResponseWithoutWatch(t *testing.T) {
|
||||
_, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
if v2c.handleLDSResponse(goodLDSResponse1) == nil {
|
||||
t.Fatal("v2c.handleLDSResponse() succeeded, should have failed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLDSWatchExpiryTimer tests the case where the client does not receive an
|
||||
// LDS response for the request that it sends out. We want the watch callback
|
||||
// to be invoked with an error once the watchExpiryTimer fires.
|
||||
func TestLDSWatchExpiryTimer(t *testing.T) {
|
||||
oldWatchExpiryTimeout := defaultWatchExpiryTimeout
|
||||
defaultWatchExpiryTimeout = 1 * time.Second
|
||||
defer func() {
|
||||
defaultWatchExpiryTimeout = oldWatchExpiryTimeout
|
||||
}()
|
||||
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
// Wait till the request makes it to the fakeServer. This ensures that
|
||||
// the watch request has been processed by the v2Client.
|
||||
callbackCh := make(chan error, 1)
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("in v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
|
||||
if u.routeName != "" {
|
||||
callbackCh <- fmt.Errorf("received routeName %v in ldsCallback, wanted empty string", u.routeName)
|
||||
}
|
||||
if err == nil {
|
||||
callbackCh <- errors.New("received nil error in ldsCallback")
|
||||
}
|
||||
callbackCh <- nil
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
|
||||
timer := time.NewTimer(2 * time.Second)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatalf("Timeout expired when expecting LDS update")
|
||||
case err := <-callbackCh:
|
||||
timer.Stop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
144
xds/internal/client/rds.go
Normal file
144
xds/internal/client/rds.go
Normal file
@ -0,0 +1,144 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
)
|
||||
|
||||
// newRDSRequest generates an RDS request proto for the provided routeName, to
|
||||
// be sent out on the wire.
|
||||
func (v2c *v2Client) newRDSRequest(routeName []string) *xdspb.DiscoveryRequest {
|
||||
return &xdspb.DiscoveryRequest{
|
||||
Node: v2c.nodeProto,
|
||||
TypeUrl: routeURL,
|
||||
ResourceNames: routeName,
|
||||
}
|
||||
}
|
||||
|
||||
// sendRDS sends an RDS request for provided routeName on the provided stream.
|
||||
func (v2c *v2Client) sendRDS(stream adsStream, routeName []string) bool {
|
||||
if err := stream.Send(v2c.newRDSRequest(routeName)); err != nil {
|
||||
grpclog.Infof("xds: RDS request for resource %v failed: %v", routeName, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handleRDSResponse processes an RDS response received from the xDS server. On
|
||||
// receipt of a good response, it caches validated resources and also invokes
|
||||
// the registered watcher callback.
|
||||
func (v2c *v2Client) handleRDSResponse(resp *xdspb.DiscoveryResponse) error {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
|
||||
if v2c.watchMap[ldsResource] == nil {
|
||||
return fmt.Errorf("xds: unexpected RDS response when no LDS watcher is registered: %+v", resp)
|
||||
}
|
||||
target := v2c.watchMap[ldsResource].target[0]
|
||||
|
||||
wi := v2c.watchMap[rdsResource]
|
||||
if wi == nil {
|
||||
return fmt.Errorf("xds: no RDS watcher found when handling RDS response: %+v", resp)
|
||||
}
|
||||
|
||||
returnCluster := ""
|
||||
localCache := make(map[string]string)
|
||||
for _, r := range resp.GetResources() {
|
||||
var resource ptypes.DynamicAny
|
||||
if err := ptypes.UnmarshalAny(r, &resource); err != nil {
|
||||
return fmt.Errorf("xds: failed to unmarshal resource in RDS response: %v", err)
|
||||
}
|
||||
rc, ok := resource.Message.(*xdspb.RouteConfiguration)
|
||||
if !ok {
|
||||
return fmt.Errorf("xds: unexpected resource type: %T in RDS response", resource.Message)
|
||||
}
|
||||
cluster := getClusterFromRouteConfiguration(rc, target)
|
||||
if cluster == "" {
|
||||
return fmt.Errorf("xds: received invalid RouteConfiguration in RDS response: %+v", rc)
|
||||
}
|
||||
|
||||
// If we get here, it means that this resource was a good one.
|
||||
localCache[rc.GetName()] = cluster
|
||||
if rc.GetName() == wi.target[0] {
|
||||
returnCluster = cluster
|
||||
}
|
||||
}
|
||||
|
||||
// Update the cache in the v2Client only after we have confirmed that all
|
||||
// resources in the received response were good.
|
||||
for k, v := range localCache {
|
||||
// TODO: Need to handle deletion of entries from the cache based on LDS
|
||||
// watch calls. Not handling it does not affect correctness, but leads
|
||||
// to unnecessary memory consumption.
|
||||
v2c.rdsCache[k] = v
|
||||
}
|
||||
|
||||
if returnCluster != "" {
|
||||
// We stop the expiry timer and invoke the callback only when we have
|
||||
// received the resource that we are watching for. Since RDS is an
|
||||
// incremental protocol, the fact that we did not receive the resource
|
||||
// that we are watching for in this response does not mean that the
|
||||
// server does not know about it.
|
||||
wi.expiryTimer.Stop()
|
||||
wi.callback.(rdsCallback)(rdsUpdate{clusterName: returnCluster}, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClusterFromRouteConfiguration checks if the provided RouteConfiguration
|
||||
// meets the expected criteria. If so, it returns a non-empty clusterName.
|
||||
//
|
||||
// A RouteConfiguration resource is considered valid when only if it contains a
|
||||
// VirtualHost whose domain field matches the server name from the URI passed
|
||||
// to the gRPC channel, and it contains a clusterName.
|
||||
//
|
||||
// The RouteConfiguration includes a list of VirtualHosts, which may have zero
|
||||
// or more elements. We are interested in the element whose domains field
|
||||
// matches the server name specified in the "xds:" URI (with port, if any,
|
||||
// stripped off). The only field in the VirtualHost proto that the we are
|
||||
// interested in is the list of routes. We only look at the last route in the
|
||||
// list (the default route), whose match field must be empty and whose route
|
||||
// field must be set. Inside that route message, the cluster field will
|
||||
// contain the clusterName we are looking for.
|
||||
func getClusterFromRouteConfiguration(rc *xdspb.RouteConfiguration, target string) string {
|
||||
host, _, err := net.SplitHostPort(target)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, vh := range rc.GetVirtualHosts() {
|
||||
for _, domain := range vh.GetDomains() {
|
||||
// TODO: Add support for wildcard matching here?
|
||||
if domain != host || len(vh.GetRoutes()) == 0 {
|
||||
continue
|
||||
}
|
||||
dr := vh.Routes[len(vh.Routes)-1]
|
||||
if dr.GetMatch() == nil && dr.GetRoute() != nil {
|
||||
return dr.GetRoute().GetCluster()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
492
xds/internal/client/rds_test.go
Normal file
492
xds/internal/client/rds_test.go
Normal file
@ -0,0 +1,492 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
|
||||
"google.golang.org/grpc/xds/internal/client/fakexds"
|
||||
)
|
||||
|
||||
func (v2c *v2Client) cloneRDSCacheForTesting() map[string]string {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
|
||||
cloneCache := make(map[string]string)
|
||||
for k, v := range v2c.rdsCache {
|
||||
cloneCache[k] = v
|
||||
}
|
||||
return cloneCache
|
||||
}
|
||||
|
||||
func TestGetClusterFromRouteConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rc *xdspb.RouteConfiguration
|
||||
wantCluster string
|
||||
}{
|
||||
{
|
||||
name: "no-virtual-hosts-in-rc",
|
||||
rc: emptyRouteConfig,
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "no-domains-in-rc",
|
||||
rc: noDomainsInRouteConfig,
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "non-matching-domain-in-rc",
|
||||
rc: &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{Domains: []string{uninterestingDomain}},
|
||||
},
|
||||
},
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "no-routes-in-rc",
|
||||
rc: &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{Domains: []string{goodMatchingDomain}},
|
||||
},
|
||||
},
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "default-route-match-field-is-non-nil",
|
||||
rc: &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{goodMatchingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Match: &routepb.RouteMatch{},
|
||||
Action: &routepb.Route_Route{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "default-route-routeaction-field-is-nil",
|
||||
rc: &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{goodMatchingDomain},
|
||||
Routes: []*routepb.Route{{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "default-route-cluster-field-is-empty",
|
||||
rc: &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{goodMatchingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_ClusterHeader{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCluster: "",
|
||||
},
|
||||
{
|
||||
name: "good-route-config",
|
||||
rc: goodRouteConfig1,
|
||||
wantCluster: goodClusterName1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if gotCluster := getClusterFromRouteConfiguration(test.rc, goodLDSTarget1); gotCluster != test.wantCluster {
|
||||
t.Errorf("%s: getClusterFromRouteConfiguration(%+v, %v) = %v, want %v", test.name, test.rc, goodLDSTarget1, gotCluster, test.wantCluster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleRDSResponse starts a fake xDS server, makes a ClientConn to it,
|
||||
// and creates a v2Client using it. Then, it registers an LDS and RDS watcher
|
||||
// and tests different RDS responses.
|
||||
func TestHandleRDSResponse(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
// Register an LDS watcher, and wait till the request is sent out, the
|
||||
// response is received and the callback is invoked.
|
||||
cbCh := make(chan error, 1)
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
|
||||
cbCh <- err
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
if err := <-cbCh; err != nil {
|
||||
t.Fatalf("v2c.watchLDS returned error in callback: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
rdsResponse *xdspb.DiscoveryResponse
|
||||
wantErr bool
|
||||
wantUpdate *rdsUpdate
|
||||
wantUpdateErr bool
|
||||
}{
|
||||
// Badly marshaled RDS response.
|
||||
{
|
||||
name: "badly-marshaled-response",
|
||||
rdsResponse: badlyMarshaledRDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response does not contain RouteConfiguration proto.
|
||||
{
|
||||
name: "no-route-config-in-response",
|
||||
rdsResponse: badResourceTypeInRDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// No VirtualHosts in the response. Just one test case here for a bad
|
||||
// RouteConfiguration, since the others are covered in
|
||||
// TestGetClusterFromRouteConfiguration.
|
||||
{
|
||||
name: "no-virtual-hosts-in-response",
|
||||
rdsResponse: noVirtualHostsInRDSResponse,
|
||||
wantErr: true,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains one good RouteConfiguration, uninteresting though.
|
||||
{
|
||||
name: "one-uninteresting-route-config",
|
||||
rdsResponse: goodRDSResponse2,
|
||||
wantErr: false,
|
||||
wantUpdate: nil,
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
// Response contains one good interesting RouteConfiguration.
|
||||
{
|
||||
name: "one-good-route-config",
|
||||
rdsResponse: goodRDSResponse1,
|
||||
wantErr: false,
|
||||
wantUpdate: &rdsUpdate{clusterName: goodClusterName1},
|
||||
wantUpdateErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gotUpdateCh := make(chan rdsUpdate, 1)
|
||||
gotUpdateErrCh := make(chan error, 1)
|
||||
|
||||
// Register a watcher, to trigger the v2Client to send an RDS request.
|
||||
cancelWatch := v2c.watchRDS(goodRouteName1, func(u rdsUpdate, err error) {
|
||||
t.Logf("%s: in v2c.watchRDS callback, rdsUpdate: %+v, err: %v", test.name, u, err)
|
||||
gotUpdateCh <- u
|
||||
gotUpdateErrCh <- err
|
||||
})
|
||||
|
||||
// Wait till the request makes it to the fakeServer. This ensures that
|
||||
// the watch request has been processed by the v2Client.
|
||||
<-fakeServer.RequestChan
|
||||
|
||||
// Directly push the response through a call to handleRDSResponse,
|
||||
// thereby bypassing the fakeServer.
|
||||
if err := v2c.handleRDSResponse(test.rdsResponse); (err != nil) != test.wantErr {
|
||||
t.Fatalf("%s: v2c.handleRDSResponse() returned err: %v, wantErr: %v", test.name, err, test.wantErr)
|
||||
}
|
||||
|
||||
// If the test needs the callback to be invoked, verify the update and
|
||||
// error pushed to the callback.
|
||||
if test.wantUpdate != nil {
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting RDS update")
|
||||
case gotUpdate := <-gotUpdateCh:
|
||||
timer.Stop()
|
||||
if !reflect.DeepEqual(gotUpdate, *test.wantUpdate) {
|
||||
t.Fatalf("%s: got RDS update : %+v, want %+v", test.name, gotUpdate, *test.wantUpdate)
|
||||
}
|
||||
}
|
||||
// Since the callback that we registered pushes to both channels at
|
||||
// the same time, this channel read should return immediately.
|
||||
gotUpdateErr := <-gotUpdateErrCh
|
||||
if (gotUpdateErr != nil) != test.wantUpdateErr {
|
||||
t.Fatalf("%s: got RDS update error {%v}, wantErr: %v", test.name, gotUpdateErr, test.wantUpdateErr)
|
||||
}
|
||||
}
|
||||
cancelWatch()
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleRDSResponseWithoutLDSWatch tests the case where the v2Client
|
||||
// receives an RDS response without a registered LDS watcher.
|
||||
func TestHandleRDSResponseWithoutLDSWatch(t *testing.T) {
|
||||
_, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
if v2c.handleRDSResponse(goodRDSResponse1) == nil {
|
||||
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleRDSResponseWithoutRDSWatch tests the case where the v2Client
|
||||
// receives an RDS response without a registered RDS watcher.
|
||||
func TestHandleRDSResponseWithoutRDSWatch(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
|
||||
// Register an LDS watcher, and wait till the request is sent out, the
|
||||
// response is received and the callback is invoked.
|
||||
cbCh := make(chan error, 1)
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
|
||||
cbCh <- err
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
if err := <-cbCh; err != nil {
|
||||
t.Fatalf("v2c.watchLDS returned error in callback: %v", err)
|
||||
}
|
||||
|
||||
if v2c.handleRDSResponse(goodRDSResponse1) == nil {
|
||||
t.Fatal("v2c.handleRDSResponse() succeeded, should have failed")
|
||||
}
|
||||
}
|
||||
|
||||
// testOp contains all data related to one particular test operation. Not all
|
||||
// fields make sense for all tests.
|
||||
type testOp struct {
|
||||
// target is the resource name to watch for.
|
||||
target string
|
||||
// responseToSend is the xDS response sent to the client
|
||||
responseToSend *fakexds.Response
|
||||
// wantOpErr specfies whether the main operation should return an error.
|
||||
wantOpErr bool
|
||||
// wantRDSCache is the expected rdsCache at the end of an operation.
|
||||
wantRDSCache map[string]string
|
||||
// wantWatchCallback specifies if the watch callback should be invoked.
|
||||
wantWatchCallback bool
|
||||
}
|
||||
|
||||
// testRDSCaching is a helper function which starts a fake xDS server, makes a
|
||||
// ClientConn to it, creates a v2Client using it, registers an LDS watcher and
|
||||
// pushes a good LDS response. It then reads a bunch of test operations to be
|
||||
// performed from testOps and returns error, if any, on the provided error
|
||||
// channel. This is executed in a separate goroutine.
|
||||
func testRDSCaching(t *testing.T, testOps []testOp, errCh chan error) {
|
||||
t.Helper()
|
||||
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
defer v2c.close()
|
||||
t.Log("Started xds v2Client...")
|
||||
|
||||
// Register an LDS watcher, and wait till the request is sent out, the
|
||||
// response is received and the callback is invoked.
|
||||
cbCh := make(chan error, 1)
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
|
||||
cbCh <- err
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
if err := <-cbCh; err != nil {
|
||||
errCh <- fmt.Errorf("v2c.watchLDS returned error in callback: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
callbackCh := make(chan struct{}, 1)
|
||||
for _, testOp := range testOps {
|
||||
// Register a watcher if required, and use a channel to signal the
|
||||
// successful invocation of the callback.
|
||||
if testOp.target != "" {
|
||||
v2c.watchRDS(testOp.target, func(u rdsUpdate, err error) {
|
||||
t.Logf("Received callback with rdsUpdate {%+v} and error {%v}", u, err)
|
||||
callbackCh <- struct{}{}
|
||||
})
|
||||
t.Logf("Registered a watcher for LDS target: %v...", testOp.target)
|
||||
|
||||
// Wait till the request makes it to the fakeServer. This ensures that
|
||||
// the watch request has been processed by the v2Client.
|
||||
<-fakeServer.RequestChan
|
||||
t.Log("FakeServer received request...")
|
||||
}
|
||||
|
||||
// Directly push the response through a call to handleRDSResponse,
|
||||
// thereby bypassing the fakeServer.
|
||||
if testOp.responseToSend != nil {
|
||||
if err := v2c.handleRDSResponse(testOp.responseToSend.Resp); (err != nil) != testOp.wantOpErr {
|
||||
errCh <- fmt.Errorf("v2c.handleRDSResponse() returned err: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the test needs the callback to be invoked, just verify that
|
||||
// it was invoked. Since we verify the contents of the cache, it's
|
||||
// ok not to verify the contents of the callback.
|
||||
if testOp.wantWatchCallback {
|
||||
<-callbackCh
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(v2c.cloneRDSCacheForTesting(), testOp.wantRDSCache) {
|
||||
errCh <- fmt.Errorf("gotRDSCache: %v, wantRDSCache: %v", v2c.rdsCache, testOp.wantRDSCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Log("Completed all test ops successfully...")
|
||||
errCh <- nil
|
||||
}
|
||||
|
||||
// TestRDSCaching tests some end-to-end RDS flows using a fake xDS server, and
|
||||
// verifies the RDS data cached at the v2Client.
|
||||
func TestRDSCaching(t *testing.T) {
|
||||
errCh := make(chan error, 1)
|
||||
ops := []testOp{
|
||||
// Add an RDS watch for a resource name (goodRouteName1), which returns one
|
||||
// matching resource in the response.
|
||||
{
|
||||
target: goodRouteName1,
|
||||
responseToSend: &fakexds.Response{Resp: goodRDSResponse1},
|
||||
wantRDSCache: map[string]string{goodRouteName1: goodClusterName1},
|
||||
wantWatchCallback: true,
|
||||
},
|
||||
// Push an RDS response with a new resource. This resource is considered
|
||||
// good because its domain field matches our LDS watch target, but the
|
||||
// routeConfigName does not match our RDS watch (so the watch callback will
|
||||
// not be invoked). But this should still be cached.
|
||||
{
|
||||
responseToSend: &fakexds.Response{Resp: goodRDSResponse2},
|
||||
wantRDSCache: map[string]string{
|
||||
goodRouteName1: goodClusterName1,
|
||||
goodRouteName2: goodClusterName2,
|
||||
},
|
||||
},
|
||||
// Push an uninteresting RDS response. This should cause handleRDSResponse
|
||||
// to return an error. But the watch callback should not be invoked, and
|
||||
// the cache should not be updated.
|
||||
{
|
||||
responseToSend: &fakexds.Response{Resp: uninterestingRDSResponse},
|
||||
wantOpErr: true,
|
||||
wantRDSCache: map[string]string{
|
||||
goodRouteName1: goodClusterName1,
|
||||
goodRouteName2: goodClusterName2,
|
||||
},
|
||||
},
|
||||
// Switch the watch target to goodRouteName2, which was already cached. No
|
||||
// response is received from the server (as expected), but we want the
|
||||
// callback to be invoked with the new clusterName.
|
||||
{
|
||||
target: goodRouteName2,
|
||||
wantRDSCache: map[string]string{
|
||||
goodRouteName1: goodClusterName1,
|
||||
goodRouteName2: goodClusterName2,
|
||||
},
|
||||
wantWatchCallback: true,
|
||||
},
|
||||
}
|
||||
go testRDSCaching(t, ops, errCh)
|
||||
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting RDS update")
|
||||
case err := <-errCh:
|
||||
timer.Stop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRDSWatchExpiryTimer(t *testing.T) {
|
||||
oldWatchExpiryTimeout := defaultWatchExpiryTimeout
|
||||
defaultWatchExpiryTimeout = 1 * time.Second
|
||||
defer func() {
|
||||
defaultWatchExpiryTimeout = oldWatchExpiryTimeout
|
||||
}()
|
||||
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
defer v2c.close()
|
||||
t.Log("Started xds v2Client...")
|
||||
|
||||
// Register an LDS watcher, and wait till the request is sent out, the
|
||||
// response is received and the callback is invoked.
|
||||
ldsCallbackCh := make(chan struct{})
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
|
||||
close(ldsCallbackCh)
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
<-ldsCallbackCh
|
||||
|
||||
// Wait till the request makes it to the fakeServer. This ensures that
|
||||
// the watch request has been processed by the v2Client.
|
||||
rdsCallbackCh := make(chan error, 1)
|
||||
v2c.watchRDS(goodRouteName1, func(u rdsUpdate, err error) {
|
||||
t.Logf("Received callback with rdsUpdate {%+v} and error {%v}", u, err)
|
||||
if u.clusterName != "" {
|
||||
rdsCallbackCh <- fmt.Errorf("received clusterName %v in rdsCallback, wanted empty string", u.clusterName)
|
||||
}
|
||||
if err == nil {
|
||||
rdsCallbackCh <- errors.New("received nil error in rdsCallback")
|
||||
}
|
||||
rdsCallbackCh <- nil
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
|
||||
timer := time.NewTimer(2 * time.Second)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatalf("Timeout expired when expecting RDS update")
|
||||
case err := <-rdsCallbackCh:
|
||||
timer.Stop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
82
xds/internal/client/types.go
Normal file
82
xds/internal/client/types.go
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
|
||||
)
|
||||
|
||||
type adsStream adsgrpc.AggregatedDiscoveryService_StreamAggregatedResourcesClient
|
||||
|
||||
const (
|
||||
listenerURL = "type.googleapis.com/envoy.api.v2.Listener"
|
||||
routeURL = "type.googleapis.com/envoy.api.v2.RouteConfiguration"
|
||||
clusterURL = "type.googleapis.com/envoy.api.v2.Cluster"
|
||||
endpointURL = "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment"
|
||||
)
|
||||
|
||||
// resourceType is an enum to represent the different xDS resources.
|
||||
type resourceType int
|
||||
|
||||
const (
|
||||
ldsResource resourceType = iota
|
||||
rdsResource
|
||||
cdsResource
|
||||
edsResource
|
||||
)
|
||||
|
||||
// watchState is an enum to represent the state of a watch call.
|
||||
type watchState int
|
||||
|
||||
const (
|
||||
watchEnqueued watchState = iota
|
||||
watchCancelled
|
||||
watchStarted
|
||||
)
|
||||
|
||||
// watchInfo holds all the information about a watch call.
|
||||
type watchInfo struct {
|
||||
wType resourceType
|
||||
target []string
|
||||
state watchState
|
||||
callback interface{}
|
||||
expiryTimer *time.Timer
|
||||
}
|
||||
|
||||
// cancel marks the state as cancelled, and also stops the expiry timer.
|
||||
func (wi *watchInfo) cancel() {
|
||||
wi.state = watchCancelled
|
||||
if wi.expiryTimer != nil {
|
||||
wi.expiryTimer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
type ldsUpdate struct {
|
||||
routeName string
|
||||
}
|
||||
|
||||
type ldsCallback func(ldsUpdate, error)
|
||||
|
||||
type rdsUpdate struct {
|
||||
clusterName string
|
||||
}
|
||||
|
||||
type rdsCallback func(rdsUpdate, error)
|
326
xds/internal/client/v2client.go
Normal file
326
xds/internal/client/v2client.go
Normal file
@ -0,0 +1,326 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/buffer"
|
||||
|
||||
corepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
adsgrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v2"
|
||||
)
|
||||
|
||||
// The value chosen here is based on the default value of the
|
||||
// initial_fetch_timeout field in corepb.ConfigSource proto.
|
||||
var defaultWatchExpiryTimeout = 15 * time.Second
|
||||
|
||||
// v2Client performs the actual xDS RPCs using the xDS v2 API. It creates a
|
||||
// single ADS stream on which the different types of xDS requests and responses
|
||||
// are multiplexed.
|
||||
// The reason for splitting this out from the top level xdsClient object is
|
||||
// because there is already an xDS v3Aplha API in development. If and when we
|
||||
// want to switch to that, this separation will ease that process.
|
||||
type v2Client struct {
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
// ClientConn to the xDS gRPC server. Owned by the parent xdsClient.
|
||||
cc *grpc.ClientConn
|
||||
nodeProto *corepb.Node
|
||||
backoff func(int) time.Duration
|
||||
|
||||
// watchCh in the channel onto which watchInfo objects are pushed by the
|
||||
// watch API, and it is read and acted upon by the send() goroutine.
|
||||
watchCh *buffer.Unbounded
|
||||
|
||||
mu sync.Mutex
|
||||
// Message specific watch infos, protected by the above mutex. These are
|
||||
// written to, after successfully reading from the update channel, and are
|
||||
// read from when recovering from a broken stream to resend the xDS
|
||||
// messages. When the user of this client object cancels a watch call,
|
||||
// these are set to nil. All accesses to the map protected and any value
|
||||
// inside the map should be protected with the above mutex.
|
||||
watchMap map[resourceType]*watchInfo
|
||||
// rdsCache maintains a mapping of {routeConfigName --> clusterName} from
|
||||
// validated route configurations received in RDS responses. We cache all
|
||||
// valid route configurations, whether or not we are interested in them
|
||||
// when we received them (because we could become interested in them in the
|
||||
// future and the server wont send us those resources again).
|
||||
// Protected by the above mutex.
|
||||
rdsCache map[string]string
|
||||
}
|
||||
|
||||
// newV2Client creates a new v2Client initialized with the passed arguments.
|
||||
func newV2Client(cc *grpc.ClientConn, nodeProto *corepb.Node, backoff func(int) time.Duration) *v2Client {
|
||||
v2c := &v2Client{
|
||||
cc: cc,
|
||||
nodeProto: nodeProto,
|
||||
backoff: backoff,
|
||||
watchCh: buffer.NewUnbounded(),
|
||||
watchMap: make(map[resourceType]*watchInfo),
|
||||
rdsCache: make(map[string]string),
|
||||
}
|
||||
v2c.ctx, v2c.cancelCtx = context.WithCancel(context.Background())
|
||||
|
||||
go v2c.run()
|
||||
return v2c
|
||||
}
|
||||
|
||||
// close cleans up resources and goroutines allocated by this client.
|
||||
func (v2c *v2Client) close() {
|
||||
v2c.cancelCtx()
|
||||
}
|
||||
|
||||
// run starts an ADS stream (and backs off exponentially, if the previous
|
||||
// stream failed without receiving a single reply) and runs the sender and
|
||||
// receiver routines to send and receive data from the stream respectively.
|
||||
func (v2c *v2Client) run() {
|
||||
retries := 0
|
||||
for {
|
||||
select {
|
||||
case <-v2c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if retries != 0 {
|
||||
t := time.NewTimer(v2c.backoff(retries))
|
||||
select {
|
||||
case <-t.C:
|
||||
case <-v2c.ctx.Done():
|
||||
if !t.Stop() {
|
||||
<-t.C
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
retries++
|
||||
cli := adsgrpc.NewAggregatedDiscoveryServiceClient(v2c.cc)
|
||||
stream, err := cli.StreamAggregatedResources(v2c.ctx, grpc.WaitForReady(true))
|
||||
if err != nil {
|
||||
grpclog.Infof("xds: ADS stream creation failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// send() could be blocked on reading updates from the different update
|
||||
// channels when it is not actually sending out messages. So, we need a
|
||||
// way to break out of send() when recv() returns. This done channel is
|
||||
// used to for that purpose.
|
||||
done := make(chan struct{})
|
||||
go v2c.send(stream, done)
|
||||
if v2c.recv(stream) {
|
||||
retries = 0
|
||||
}
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
|
||||
// sendExisting sends out xDS requests for registered watchers when recovering
|
||||
// from a broken stream.
|
||||
//
|
||||
// We call stream.Send() here with the lock being held. It should be OK to do
|
||||
// that here because the stream has just started and Send() usually returns
|
||||
// quickly (once it pushes the message onto the transport layer) and is only
|
||||
// ever blocked if we don't have enough flow control quota.
|
||||
func (v2c *v2Client) sendExisting(stream adsStream) bool {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
|
||||
for wType, wi := range v2c.watchMap {
|
||||
switch wType {
|
||||
case ldsResource:
|
||||
if !v2c.sendLDS(stream, wi.target) {
|
||||
return false
|
||||
}
|
||||
case rdsResource:
|
||||
if !v2c.sendRDS(stream, wi.target) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// send reads watch infos from update channel and sends out actual xDS requests
|
||||
// on the provided ADS stream.
|
||||
func (v2c *v2Client) send(stream adsStream, done chan struct{}) {
|
||||
if !v2c.sendExisting(stream) {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-v2c.ctx.Done():
|
||||
return
|
||||
case u := <-v2c.watchCh.Get():
|
||||
v2c.watchCh.Load()
|
||||
wi := u.(*watchInfo)
|
||||
v2c.mu.Lock()
|
||||
if wi.state == watchCancelled {
|
||||
v2c.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
wi.state = watchStarted
|
||||
target := wi.target
|
||||
v2c.checkCacheAndUpdateWatchMap(wi)
|
||||
v2c.mu.Unlock()
|
||||
|
||||
switch wi.wType {
|
||||
case ldsResource:
|
||||
if !v2c.sendLDS(stream, target) {
|
||||
return
|
||||
}
|
||||
case rdsResource:
|
||||
if !v2c.sendRDS(stream, target) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recv receives xDS responses on the provided ADS stream and branches out to
|
||||
// message specific handlers.
|
||||
func (v2c *v2Client) recv(stream adsStream) bool {
|
||||
success := false
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
grpclog.Warningf("xds: ADS stream recv failed: %v", err)
|
||||
return success
|
||||
}
|
||||
switch resp.GetTypeUrl() {
|
||||
case listenerURL:
|
||||
if err := v2c.handleLDSResponse(resp); err != nil {
|
||||
grpclog.Warningf("xds: LDS response handler failed: %v", err)
|
||||
return success
|
||||
}
|
||||
case routeURL:
|
||||
if err := v2c.handleRDSResponse(resp); err != nil {
|
||||
grpclog.Warningf("xds: RDS response handler failed: %v", err)
|
||||
return success
|
||||
}
|
||||
default:
|
||||
grpclog.Warningf("xds: unknown response URL type: %v", resp.GetTypeUrl())
|
||||
}
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
// watchLDS registers an LDS watcher for the provided target. Updates
|
||||
// corresponding to received LDS responses will be pushed to the provided
|
||||
// callback. The caller can cancel the watch by invoking the returned cancel
|
||||
// function.
|
||||
// The provided callback should not block or perform any expensive operations
|
||||
// or call other methods of the v2Client object.
|
||||
func (v2c *v2Client) watchLDS(target string, ldsCb ldsCallback) (cancel func()) {
|
||||
wi := &watchInfo{wType: ldsResource, target: []string{target}, callback: ldsCb}
|
||||
v2c.watchCh.Put(wi)
|
||||
return func() {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
if wi.state == watchEnqueued {
|
||||
wi.state = watchCancelled
|
||||
return
|
||||
}
|
||||
v2c.watchMap[ldsResource].cancel()
|
||||
delete(v2c.watchMap, ldsResource)
|
||||
}
|
||||
}
|
||||
|
||||
// watchRDS registers an RDS watcher for the provided routeName. Updates
|
||||
// corresponding to received RDS responses will be pushed to the provided
|
||||
// callback. The caller can cancel the watch by invoking the returned cancel
|
||||
// function.
|
||||
// The provided callback should not block or perform any expensive operations
|
||||
// or call other methods of the v2Client object.
|
||||
func (v2c *v2Client) watchRDS(routeName string, rdsCb rdsCallback) (cancel func()) {
|
||||
wi := &watchInfo{wType: rdsResource, target: []string{routeName}, callback: rdsCb}
|
||||
v2c.watchCh.Put(wi)
|
||||
return func() {
|
||||
v2c.mu.Lock()
|
||||
defer v2c.mu.Unlock()
|
||||
if wi.state == watchEnqueued {
|
||||
wi.state = watchCancelled
|
||||
return
|
||||
}
|
||||
v2c.watchMap[rdsResource].cancel()
|
||||
delete(v2c.watchMap, rdsResource)
|
||||
// TODO: Once a registered RDS watch is cancelled, we should send an
|
||||
// RDS request with no resources. This will let the server know that we
|
||||
// are no longer interested in this resource.
|
||||
}
|
||||
}
|
||||
|
||||
// checkCacheAndUpdateWatchMap is called when a new watch call is handled in
|
||||
// send(). If an existing watcher is found, its expiry timer is stopped. If the
|
||||
// watchInfo to be added to the watchMap is found in the cache, the watcher
|
||||
// callback is immediately invoked.
|
||||
//
|
||||
// Caller should hold v2c.mu
|
||||
func (v2c *v2Client) checkCacheAndUpdateWatchMap(wi *watchInfo) {
|
||||
if existing := v2c.watchMap[wi.wType]; existing != nil {
|
||||
existing.cancel()
|
||||
}
|
||||
|
||||
v2c.watchMap[wi.wType] = wi
|
||||
switch wi.wType {
|
||||
case ldsResource:
|
||||
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
|
||||
// We need to grab the lock here because we are accessing the
|
||||
// watchInfo (which is now stored in the watchMap) from this
|
||||
// method which will be called when the timer fires.
|
||||
v2c.mu.Lock()
|
||||
wi.callback.(ldsCallback)(ldsUpdate{routeName: ""}, fmt.Errorf("xds: LDS target %s not found", wi.target))
|
||||
v2c.mu.Unlock()
|
||||
})
|
||||
case rdsResource:
|
||||
routeName := wi.target[0]
|
||||
if cluster := v2c.rdsCache[routeName]; cluster != "" {
|
||||
// Invoke the callback now, since we found the entry in the cache.
|
||||
var err error
|
||||
if v2c.watchMap[ldsResource] == nil {
|
||||
cluster = ""
|
||||
err = fmt.Errorf("xds: no LDS watcher found when handling RDS watch for route {%v} from cache", routeName)
|
||||
}
|
||||
wi.callback.(rdsCallback)(rdsUpdate{clusterName: cluster}, err)
|
||||
return
|
||||
}
|
||||
// Add the watch expiry timer only for new watches we don't find in
|
||||
// the cache, and return from here.
|
||||
wi.expiryTimer = time.AfterFunc(defaultWatchExpiryTimeout, func() {
|
||||
// We need to grab the lock here because we are accessing the
|
||||
// watchInfo (which is now stored in the watchMap) from this
|
||||
// method which will be called when the timer fires.
|
||||
v2c.mu.Lock()
|
||||
wi.callback.(rdsCallback)(rdsUpdate{clusterName: ""}, fmt.Errorf("xds: RDS target %s not found", wi.target))
|
||||
v2c.mu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
497
xds/internal/client/v2client_test.go
Normal file
497
xds/internal/client/v2client_test.go
Normal file
@ -0,0 +1,497 @@
|
||||
/*
|
||||
*
|
||||
* 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 client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"google.golang.org/grpc/xds/internal/client/fakexds"
|
||||
|
||||
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
||||
basepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
||||
routepb "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
|
||||
httppb "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
|
||||
listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v2"
|
||||
anypb "github.com/golang/protobuf/ptypes/any"
|
||||
structpb "github.com/golang/protobuf/ptypes/struct"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestTimeout = 2 * time.Second
|
||||
goodLDSTarget1 = "lds.target.good:1111"
|
||||
goodLDSTarget2 = "lds.target.good:2222"
|
||||
goodRouteName1 = "GoodRouteConfig1"
|
||||
goodRouteName2 = "GoodRouteConfig2"
|
||||
uninterestingRouteName = "UninterestingRouteName"
|
||||
goodMatchingDomain = "lds.target.good"
|
||||
uninterestingDomain = "uninteresting.domain"
|
||||
goodClusterName1 = "GoodClusterName1"
|
||||
goodClusterName2 = "GoodClusterName2"
|
||||
uninterestingClusterName = "UninterestingClusterName"
|
||||
httpConnManagerURL = "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"
|
||||
)
|
||||
|
||||
var (
|
||||
goodNodeProto = &basepb.Node{
|
||||
Id: "ENVOY_NODE_ID",
|
||||
Metadata: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"TRAFFICDIRECTOR_GRPC_HOSTNAME": {
|
||||
Kind: &structpb.Value_StringValue{StringValue: "trafficdirector"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
goodLDSRequest = &xdspb.DiscoveryRequest{
|
||||
Node: goodNodeProto,
|
||||
TypeUrl: listenerURL,
|
||||
ResourceNames: []string{goodLDSTarget1},
|
||||
}
|
||||
goodHTTPConnManager1 = &httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &httppb.HttpConnectionManager_Rds{
|
||||
Rds: &httppb.Rds{
|
||||
RouteConfigName: goodRouteName1,
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledConnMgr1, _ = proto.Marshal(goodHTTPConnManager1)
|
||||
emptyHTTPConnManager = &httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &httppb.HttpConnectionManager_Rds{
|
||||
Rds: &httppb.Rds{},
|
||||
},
|
||||
}
|
||||
emptyMarshaledConnMgr, _ = proto.Marshal(emptyHTTPConnManager)
|
||||
connMgrWithScopedRoutes = &httppb.HttpConnectionManager{
|
||||
RouteSpecifier: &httppb.HttpConnectionManager_ScopedRoutes{},
|
||||
}
|
||||
marshaledConnMgrWithScopedRoutes, _ = proto.Marshal(connMgrWithScopedRoutes)
|
||||
goodListener1 = &xdspb.Listener{
|
||||
Name: goodLDSTarget1,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: marshaledConnMgr1,
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledListener1, _ = proto.Marshal(goodListener1)
|
||||
goodListener2 = &xdspb.Listener{
|
||||
Name: goodLDSTarget2,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: marshaledConnMgr1,
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledListener2, _ = proto.Marshal(goodListener2)
|
||||
noAPIListener = &xdspb.Listener{Name: goodLDSTarget1}
|
||||
marshaledNoAPIListener, _ = proto.Marshal(noAPIListener)
|
||||
badAPIListener1 = &xdspb.Listener{
|
||||
Name: goodLDSTarget1,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
}
|
||||
badAPIListener2 = &xdspb.Listener{
|
||||
Name: goodLDSTarget2,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
}
|
||||
badlyMarshaledAPIListener2, _ = proto.Marshal(badAPIListener2)
|
||||
badResourceListener = &xdspb.Listener{
|
||||
Name: goodLDSTarget1,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener1,
|
||||
},
|
||||
},
|
||||
}
|
||||
listenerWithEmptyHTTPConnMgr = &xdspb.Listener{
|
||||
Name: goodLDSTarget1,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: emptyMarshaledConnMgr,
|
||||
},
|
||||
},
|
||||
}
|
||||
listenerWithScopedRoutesRouteConfig = &xdspb.Listener{
|
||||
Name: goodLDSTarget1,
|
||||
ApiListener: &listenerpb.ApiListener{
|
||||
ApiListener: &anypb.Any{
|
||||
TypeUrl: httpConnManagerURL,
|
||||
Value: marshaledConnMgrWithScopedRoutes,
|
||||
},
|
||||
},
|
||||
}
|
||||
goodLDSResponse1 = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener1,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
goodLDSResponse2 = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener2,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
emptyLDSResponse = &xdspb.DiscoveryResponse{TypeUrl: listenerURL}
|
||||
badlyMarshaledLDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
badResourceTypeInLDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledConnMgr1,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
ldsResponseWithMultipleResources = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener2,
|
||||
},
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener1,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
noAPIListenerLDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledNoAPIListener,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
goodBadUglyLDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener2,
|
||||
},
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: marshaledListener1,
|
||||
},
|
||||
{
|
||||
TypeUrl: listenerURL,
|
||||
Value: badlyMarshaledAPIListener2,
|
||||
},
|
||||
},
|
||||
TypeUrl: listenerURL,
|
||||
}
|
||||
badlyMarshaledRDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: []byte{1, 2, 3, 4},
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
badResourceTypeInRDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: marshaledConnMgr1,
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
emptyRouteConfig = &xdspb.RouteConfiguration{}
|
||||
marshaledEmptyRouteConfig, _ = proto.Marshal(emptyRouteConfig)
|
||||
noDomainsInRouteConfig = &xdspb.RouteConfiguration{
|
||||
VirtualHosts: []*routepb.VirtualHost{{}},
|
||||
}
|
||||
noVirtualHostsInRDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: marshaledEmptyRouteConfig,
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
goodRouteConfig1 = &xdspb.RouteConfiguration{
|
||||
Name: goodRouteName1,
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{uninterestingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: []string{goodMatchingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: goodClusterName1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledGoodRouteConfig1, _ = proto.Marshal(goodRouteConfig1)
|
||||
goodRouteConfig2 = &xdspb.RouteConfiguration{
|
||||
Name: goodRouteName2,
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{uninterestingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: []string{goodMatchingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: goodClusterName2},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledGoodRouteConfig2, _ = proto.Marshal(goodRouteConfig2)
|
||||
uninterestingRouteConfig = &xdspb.RouteConfiguration{
|
||||
Name: uninterestingRouteName,
|
||||
VirtualHosts: []*routepb.VirtualHost{
|
||||
{
|
||||
Domains: []string{uninterestingDomain},
|
||||
Routes: []*routepb.Route{
|
||||
{
|
||||
Action: &routepb.Route_Route{
|
||||
Route: &routepb.RouteAction{
|
||||
ClusterSpecifier: &routepb.RouteAction_Cluster{Cluster: uninterestingClusterName},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
marshaledUninterestingRouteConfig, _ = proto.Marshal(uninterestingRouteConfig)
|
||||
goodRDSResponse1 = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: marshaledGoodRouteConfig1,
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
goodRDSResponse2 = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: marshaledGoodRouteConfig2,
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
uninterestingRDSResponse = &xdspb.DiscoveryResponse{
|
||||
Resources: []*anypb.Any{
|
||||
{
|
||||
TypeUrl: routeURL,
|
||||
Value: marshaledUninterestingRouteConfig,
|
||||
},
|
||||
},
|
||||
TypeUrl: routeURL,
|
||||
}
|
||||
)
|
||||
|
||||
// TestV2ClientBackoffAfterRecvError verifies if the v2Client backoffs when it
|
||||
// encounters a Recv error while receiving an LDS response.
|
||||
func TestV2ClientBackoffAfterRecvError(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
|
||||
// Override the v2Client backoff function with this, so that we can verify
|
||||
// that a backoff actually was triggerred.
|
||||
boCh := make(chan int, 1)
|
||||
clientBackoff := func(v int) time.Duration {
|
||||
boCh <- v
|
||||
return 0
|
||||
}
|
||||
|
||||
v2c := newV2Client(client, goodNodeProto, clientBackoff)
|
||||
defer v2c.close()
|
||||
t.Log("Started xds v2Client...")
|
||||
|
||||
callbackCh := make(chan struct{})
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
close(callbackCh)
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
t.Log("FakeServer received request...")
|
||||
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Err: errors.New("RPC error")}
|
||||
t.Log("Bad LDS response pushed to fakeServer...")
|
||||
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting LDS update")
|
||||
case <-boCh:
|
||||
timer.Stop()
|
||||
t.Log("v2Client backed off before retrying...")
|
||||
case <-callbackCh:
|
||||
t.Fatal("Received unexpected LDS callback")
|
||||
}
|
||||
}
|
||||
|
||||
// TestV2ClientRetriesAfterBrokenStream verifies the case where a stream
|
||||
// encountered a Recv() error, and is expected to send out xDS requests for
|
||||
// registered watchers once it comes back up again.
|
||||
func TestV2ClientRetriesAfterBrokenStream(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
defer v2c.close()
|
||||
t.Log("Started xds v2Client...")
|
||||
|
||||
callbackCh := make(chan struct{}, 1)
|
||||
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("Received LDS callback with ldsUpdate {%+v} and error {%v}", u, err)
|
||||
callbackCh <- struct{}{}
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
t.Log("FakeServer received request...")
|
||||
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
t.Log("Good LDS response pushed to fakeServer...")
|
||||
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting LDS update")
|
||||
case <-callbackCh:
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Err: errors.New("RPC error")}
|
||||
t.Log("Bad LDS response pushed to fakeServer...")
|
||||
|
||||
timer = time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting LDS update")
|
||||
case gotRequest := <-fakeServer.RequestChan:
|
||||
timer.Stop()
|
||||
t.Log("received LDS request after stream re-creation")
|
||||
if !proto.Equal(gotRequest.Req, goodLDSRequest) {
|
||||
t.Fatalf("gotRequest: %+v, wantRequest: %+v", gotRequest.Req, goodLDSRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestV2ClientCancelWatch verifies that the registered watch callback is not
|
||||
// invoked if a response is received after the watcher is cancelled.
|
||||
func TestV2ClientCancelWatch(t *testing.T) {
|
||||
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
|
||||
defer cleanup()
|
||||
|
||||
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
|
||||
defer v2c.close()
|
||||
t.Log("Started xds v2Client...")
|
||||
|
||||
callbackCh := make(chan struct{}, 1)
|
||||
cancelFunc := v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
|
||||
t.Logf("Received LDS callback with ldsUpdate {%+v} and error {%v}", u, err)
|
||||
callbackCh <- struct{}{}
|
||||
})
|
||||
<-fakeServer.RequestChan
|
||||
t.Log("FakeServer received request...")
|
||||
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
t.Log("Good LDS response pushed to fakeServer...")
|
||||
|
||||
timer := time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
t.Fatal("Timeout when expecting LDS update")
|
||||
case <-callbackCh:
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
cancelFunc()
|
||||
|
||||
fakeServer.ResponseChan <- &fakexds.Response{Resp: goodLDSResponse1}
|
||||
t.Log("Another good LDS response pushed to fakeServer...")
|
||||
|
||||
timer = time.NewTimer(defaultTestTimeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-callbackCh:
|
||||
timer.Stop()
|
||||
t.Fatalf("Watch callback invoked after the watcher was cancelled")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user