Skip to content

Commit

Permalink
[federation_target_parsing] xds resolver
Browse files Browse the repository at this point in the history
[federation_target_parsing] template populate

[federation_target_parsing] default bootstrap for test client

[federation_target_parsing] fixed resolver tests, new tests not added yet

[federation_target_parsing] new tests
  • Loading branch information
menghanl committed Oct 26, 2021
1 parent 8a715fc commit 448cb60
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 17 deletions.
40 changes: 35 additions & 5 deletions xds/internal/resolver/xds_resolver.go
Expand Up @@ -22,6 +22,7 @@ package resolver
import (
"errors"
"fmt"
"strings"

"google.golang.org/grpc/credentials"
"google.golang.org/grpc/internal/grpclog"
Expand All @@ -30,6 +31,7 @@ import (
iresolver "google.golang.org/grpc/internal/resolver"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/xds/internal/xdsclient"
"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
)

const xdsScheme = "xds"
Expand Down Expand Up @@ -60,15 +62,22 @@ type xdsResolverBuilder struct {
//
// The xds bootstrap process is performed (and a new xds client is built) every
// time an xds resolver is built.
func (b *xdsResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
func (b *xdsResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (_ resolver.Resolver, retErr error) {
r := &xdsResolver{
target: t,
cc: cc,
closed: grpcsync.NewEvent(),
updateCh: make(chan suWithError, 1),
activeClusters: make(map[string]*clusterInfo),
}
r.logger = prefixLogger((r))
defer func() {
if retErr != nil {
if r.client != nil {
r.client.Close()
}
}
}()
r.logger = prefixLogger(r)
r.logger.Infof("Creating resolver for target: %+v", t)

newXDSClient := newXDSClient
Expand All @@ -81,6 +90,10 @@ func (b *xdsResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, op
return nil, fmt.Errorf("xds: failed to create xds-client: %v", err)
}
r.client = client
bootstrapConfig := client.BootstrapConfig()
if bootstrapConfig == nil {
return nil, errors.New("bootstrap configuration is empty")
}

// If xds credentials were specified by the user, but bootstrap configs do
// not contain any certificate provider configuration, it is better to fail
Expand All @@ -94,14 +107,31 @@ func (b *xdsResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, op
creds = opts.CredsBundle.TransportCredentials()
}
if xc, ok := creds.(interface{ UsesXDS() bool }); ok && xc.UsesXDS() {
bc := client.BootstrapConfig()
if len(bc.CertProviderConfigs) == 0 {
if len(bootstrapConfig.CertProviderConfigs) == 0 {
return nil, errors.New("xds: xdsCreds specified but certificate_providers config missing in bootstrap file")
}
}

// Find the client listener template to use from the bootstrap config:
// - If authority is not set in the target, use the top level template
// - If authority is set, use the template from the authority map.
template := bootstrapConfig.ClientDefaultListenerResourceNameTemplate
if authority := r.target.URL.Host; authority != "" {
a := bootstrapConfig.Authorities[authority]
if a == nil {
return nil, fmt.Errorf("xds: authority %q is not found in the bootstrap file", authority)
}
if a.ClientListenerResourceNameTemplate == "" {
// This check will never be true, because
// ClientListenerResourceNameTemplate is required to start with
// xdstp://, and has a default value if unset.
template = a.ClientListenerResourceNameTemplate
}
}
resourceName := bootstrap.PopulateResourceTemplate(template, strings.TrimPrefix(r.target.URL.Path, "/"))

// Register a watch on the xdsClient for the user's dial target.
cancelWatch := watchService(r.client, r.target.Endpoint, r.handleServiceUpdate, r.logger)
cancelWatch := watchService(r.client, resourceName, r.handleServiceUpdate, r.logger)
r.logger.Infof("Watch started on resource name %v with xds-client %p", r.target.Endpoint, r.client)
r.cancelWatch = func() {
cancelWatch()
Expand Down
126 changes: 123 additions & 3 deletions xds/internal/resolver/xds_resolver_test.go
Expand Up @@ -21,6 +21,7 @@ package resolver
import (
"context"
"errors"
"net/url"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -61,7 +62,7 @@ const (
defaultTestShortTimeout = 100 * time.Microsecond
)

var target = resolver.Target{Endpoint: targetStr}
var target = resolver.Target{Endpoint: targetStr, URL: url.URL{Scheme: "xds", Path: "/" + targetStr}}

var routerFilter = xdsclient.HTTPFilter{Name: "rtr", Filter: httpfilter.Get(router.TypeURL)}
var routerFilterList = []xdsclient.HTTPFilter{routerFilter}
Expand Down Expand Up @@ -116,20 +117,45 @@ func (s) TestResolverBuilder(t *testing.T) {
tests := []struct {
name string
xdsClientFunc func() (xdsclient.XDSClient, error)
target resolver.Target
wantErr bool
}{
{
name: "simple-good",
xdsClientFunc: func() (xdsclient.XDSClient, error) {
return fakeclient.NewClient(), nil
},
target: target,
wantErr: false,
},
{
name: "newXDSClient-throws-error",
xdsClientFunc: func() (xdsclient.XDSClient, error) {
return nil, errors.New("newXDSClient-throws-error")
},
target: target,
wantErr: true,
},
{
name: "authority not defined in bootstrap",
xdsClientFunc: func() (xdsclient.XDSClient, error) {
c := fakeclient.NewClient()
c.SetBootstrapConfig(&bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "%s",
Authorities: map[string]*bootstrap.Authority{
"test-authority": {
ClientListenerResourceNameTemplate: "xdstp://test-authority/%s",
},
},
})
return c, nil
},
target: resolver.Target{
URL: url.URL{
Host: "non-existing-authority",
Path: "/" + targetStr,
},
},
wantErr: true,
},
}
Expand All @@ -147,7 +173,7 @@ func (s) TestResolverBuilder(t *testing.T) {
t.Fatalf("resolver.Get(%v) returned nil", xdsScheme)
}

r, err := builder.Build(target, newTestClientConn(), resolver.BuildOptions{})
r, err := builder.Build(test.target, newTestClientConn(), resolver.BuildOptions{})
if (err != nil) != test.wantErr {
t.Fatalf("builder.Build(%v) returned err: %v, wantErr: %v", target, err, test.wantErr)
}
Expand Down Expand Up @@ -196,6 +222,7 @@ func (s) TestResolverBuilder_xdsCredsBootstrapMismatch(t *testing.T) {

type setupOpts struct {
xdsClientFunc func() (xdsclient.XDSClient, error)
target *resolver.Target
}

func testSetup(t *testing.T, opts setupOpts) (*xdsResolver, *testClientConn, func()) {
Expand All @@ -213,7 +240,11 @@ func testSetup(t *testing.T, opts setupOpts) (*xdsResolver, *testClientConn, fun
}

tcc := newTestClientConn()
r, err := builder.Build(target, tcc, resolver.BuildOptions{})
tgt := target
if opts.target != nil {
tgt = *opts.target
}
r, err := builder.Build(tgt, tcc, resolver.BuildOptions{})
if err != nil {
t.Fatalf("builder.Build(%v) returned err: %v", target, err)
}
Expand Down Expand Up @@ -250,6 +281,95 @@ func waitForWatchRouteConfig(ctx context.Context, t *testing.T, xdsC *fakeclient
}
}

// TestXDSResolverResourceNameToWatch tests that the correct resource name is
// used to watch for the service. This covers cases with different bootstrap
// config, and different authority.
func (s) TestXDSResolverResourceNameToWatch(t *testing.T) {
tests := []struct {
name string
bc *bootstrap.Config
target *resolver.Target
want string
}{
{
name: "default %s, old style",
bc: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "%s",
},
target: &resolver.Target{
URL: url.URL{Path: "/" + targetStr},
},
want: targetStr,
},
{
name: "old style, no percent encoding",
bc: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "/path/to/%s",
},
target: &resolver.Target{
URL: url.URL{Path: "/" + targetStr},
},
want: "/path/to/" + targetStr,
},
{
name: "new style, with %s",
bc: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "xdstp://authority.com/%s",
Authorities: nil,
},
target: &resolver.Target{
URL: url.URL{Path: "/0.0.0.0:8080"},
},
want: "xdstp://authority.com/0.0.0.0:8080",
},
{
name: "new style, percent encoding",
bc: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "xdstp://authority.com/%s",
Authorities: nil,
},
target: &resolver.Target{
URL: url.URL{Path: "/[::1]:8080"},
},
want: "xdstp://authority.com/%5B::1%5D:8080",
},
{
name: "new style, different authority",
bc: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "xdstp://authority.com/%s",
Authorities: map[string]*bootstrap.Authority{
"test-authority": {
ClientListenerResourceNameTemplate: "xdstp://test-authority/%s",
},
},
},
target: &resolver.Target{
URL: url.URL{
Host: "test-authority",
Path: "/" + targetStr,
},
},
want: "xdstp://test-authority/" + targetStr,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
xdsC := fakeclient.NewClient()
xdsC.SetBootstrapConfig(tt.bc)
xdsR, _, cancel := testSetup(t, setupOpts{
xdsClientFunc: func() (xdsclient.XDSClient, error) { return xdsC, nil },
target: tt.target,
})
defer cancel()
defer xdsR.Close()

ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
waitForWatchListener(ctx, t, xdsC, tt.want)
})
}
}

// TestXDSResolverWatchCallbackAfterClose tests the case where a service update
// from the underlying xdsClient is received after the resolver is closed.
func (s) TestXDSResolverWatchCallbackAfterClose(t *testing.T) {
Expand Down
11 changes: 7 additions & 4 deletions xds/internal/testutils/fakeclient/client.go
Expand Up @@ -316,9 +316,12 @@ func NewClientWithName(name string) *Client {
loadReportCh: testutils.NewChannel(),
lrsCancelCh: testutils.NewChannel(),
loadStore: load.NewStore(),
rdsCbs: make(map[string]func(xdsclient.RouteConfigUpdate, error)),
cdsCbs: make(map[string]func(xdsclient.ClusterUpdate, error)),
edsCbs: make(map[string]func(xdsclient.EndpointsUpdate, error)),
Closed: grpcsync.NewEvent(),
bootstrapCfg: &bootstrap.Config{
ClientDefaultListenerResourceNameTemplate: "%s",
},
rdsCbs: make(map[string]func(xdsclient.RouteConfigUpdate, error)),
cdsCbs: make(map[string]func(xdsclient.ClusterUpdate, error)),
edsCbs: make(map[string]func(xdsclient.EndpointsUpdate, error)),
Closed: grpcsync.NewEvent(),
}
}
48 changes: 48 additions & 0 deletions xds/internal/xdsclient/bootstrap/template.go
@@ -0,0 +1,48 @@
/*
*
* Copyright 2021 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 bootstrap

import (
"net/url"
"strings"
)

// PopulateResourceTemplate populates the given template using the target string
// t. "%s", if exists in the template, will be replaced with t.
//
// If the template starts with "xdstp:", the replaced string will be %-encoded.
// But note that "/" is not percent encoded.
func PopulateResourceTemplate(template string, t string) string {
if !strings.Contains(template, "%s") {
return template
}
if strings.HasPrefix(template, "xdstp:") {
// Percent encode t.
t = percentEncode(t)
}
return strings.Replace(template, "%s", t, -1)
}

// percentEncode percent encode t, except for "/". See the tests for examples.
func percentEncode(t string) string {
segs := strings.Split(t, "/")
for i := range segs {
segs[i] = url.PathEscape(segs[i])
}
return strings.Join(segs, "/")
}

0 comments on commit 448cb60

Please sign in to comment.