Skip to content

Commit

Permalink
xdsclient: use dataplane authority for virtualhost lookup (#6997)
Browse files Browse the repository at this point in the history
  • Loading branch information
arvindbr8 authored Feb 28, 2024
1 parent c267d5b commit 90fc697
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 12 deletions.
2 changes: 2 additions & 0 deletions clientconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,8 @@ func parseTarget(target string) (resolver.Target, error) {
return resolver.Target{URL: *u}, nil
}

// encodeAuthority escapes the authority string based on valid chars defined in
// https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.
func encodeAuthority(authority string) string {
const upperhex = "0123456789ABCDEF"

Expand Down
4 changes: 2 additions & 2 deletions internal/testutils/xds/e2e/clientresources.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,11 @@ func HTTPFilter(name string, config proto.Message) *v3httppb.HttpFilter {
}

// DefaultRouteConfig returns a basic xds RouteConfig resource.
func DefaultRouteConfig(routeName, ldsTarget, clusterName string) *v3routepb.RouteConfiguration {
func DefaultRouteConfig(routeName, vhDomain, clusterName string) *v3routepb.RouteConfiguration {
return &v3routepb.RouteConfiguration{
Name: routeName,
VirtualHosts: []*v3routepb.VirtualHost{{
Domains: []string{ldsTarget},
Domains: []string{vhDomain},
Routes: []*v3routepb.Route{{
Match: &v3routepb.RouteMatch{PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"}},
Action: &v3routepb.Route_Route{Route: &v3routepb.RouteAction{
Expand Down
3 changes: 3 additions & 0 deletions resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ type BuildOptions struct {
// field. In most cases though, it is not appropriate, and this field may
// be ignored.
Dialer func(context.Context, string) (net.Conn, error)
// Authority is the effective authority of the clientconn for which the
// resolver is built.
Authority string
}

// An Endpoint is one network endpoint, or server, which may have multiple
Expand Down
1 change: 1 addition & 0 deletions resolver_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func (ccr *ccResolverWrapper) start() error {
DialCreds: ccr.cc.dopts.copts.TransportCredentials,
CredsBundle: ccr.cc.dopts.copts.CredsBundle,
Dialer: ccr.cc.dopts.copts.Dialer,
Authority: ccr.cc.authority,
}
var err error
ccr.resolver, err = ccr.cc.resolverBuilder.Build(ccr.cc.parsedTarget, ccr, opts)
Expand Down
85 changes: 85 additions & 0 deletions test/xds/xds_client_federation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,91 @@ func (s) TestClientSideFederation(t *testing.T) {
}
}

// TestClientSideFederationWithOnlyXDSTPStyleLDS tests that federation is
// supported with new xdstp style names for LDS only while using the old style
// for other resources. This test in addition also checks that when service name
// contains escapable characters, we "fully" encode it for looking up
// VirtualHosts in xDS RouteConfigurtion.
func (s) TestClientSideFederationWithOnlyXDSTPStyleLDS(t *testing.T) {
// Start a management server as a sophisticated authority.
const authority = "traffic-manager.xds.notgoogleapis.com"
mgmtServer, err := e2e.StartManagementServer(e2e.ManagementServerOptions{})
if err != nil {
t.Fatalf("Failed to spin up the xDS management server: %v", err)
}
t.Cleanup(mgmtServer.Stop)

// Create a bootstrap file in a temporary directory.
nodeID := uuid.New().String()
bootstrapContents, err := bootstrap.Contents(bootstrap.Options{
NodeID: nodeID,
ServerURI: mgmtServer.Address,
ClientDefaultListenerResourceNameTemplate: fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%%s", authority),
Authorities: map[string]string{authority: mgmtServer.Address},
})
if err != nil {
t.Fatalf("Failed to create bootstrap file: %v", err)
}

resolverBuilder := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))
resolver, err := resolverBuilder(bootstrapContents)
if err != nil {
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
}
server := stubserver.StartTestService(t, nil)
defer server.Stop()

// serviceName with escapable characters - ' ', and '/'.
const serviceName = "my-service-client-side-xds/2nd component"

// All other resources are with old style name.
const rdsName = "route-" + serviceName
const cdsName = "cluster-" + serviceName
const edsName = "endpoints-" + serviceName

// Resource update sent to go-control-plane mgmt server.
resourceUpdate := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: func() []*v3listenerpb.Listener {
// LDS is new style xdstp name. Since the LDS resource name is prefixed
// with xdstp, the string will be %-encoded excluding '/'s. See
// bootstrap.PopulateResourceTemplate().
const specialEscapedServiceName = "my-service-client-side-xds/2nd%20component" // same as bootstrap.percentEncode(serviceName)
ldsName := fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%s", authority, specialEscapedServiceName)
return []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}
}(),
Routes: func() []*v3routepb.RouteConfiguration {
// RouteConfiguration will has one entry in []VirutalHosts that contains the
// "fully" escaped service name in []Domains. This is to assert that gRPC
// uses the escaped service name to lookup VirtualHosts. RDS is also with
// old style name.
const fullyEscapedServiceName = "my-service-client-side-xds%2F2nd%20component" // same as url.PathEscape(serviceName)
return []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, fullyEscapedServiceName, cdsName)}
}(),
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(cdsName, edsName, e2e.SecurityLevelNone)},
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
SkipValidation: true,
}

ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if err := mgmtServer.Update(ctx, resourceUpdate); err != nil {
t.Fatal(err)
}

// Create a ClientConn and make a successful RPC.
cc, err := grpc.Dial(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
if err != nil {
t.Fatalf("failed to dial local test server: %v", err)
}
defer cc.Close()

client := testgrpc.NewTestServiceClient(cc)
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
t.Fatalf("rpc EmptyCall() failed: %v", err)
}
}

// TestFederation_UnknownAuthorityInDialTarget tests the case where a ClientConn
// is created with a dial target containing an authority which is not specified
// in the bootstrap configuration. The test verifies that RPCs on the ClientConn
Expand Down
5 changes: 4 additions & 1 deletion xds/internal/resolver/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package resolver_test
import (
"context"
"fmt"
"net/url"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -104,7 +105,9 @@ func buildResolverForTarget(t *testing.T, target resolver.Target) (chan resolver
}
}
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF}
r, err := builder.Build(target, tcc, resolver.BuildOptions{})
r, err := builder.Build(target, tcc, resolver.BuildOptions{
Authority: url.PathEscape(target.Endpoint()),
})
if err != nil {
t.Fatalf("Failed to build xDS resolver for target %q: %v", target, err)
}
Expand Down
19 changes: 10 additions & 9 deletions xds/internal/resolver/xds_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ package resolver
import (
"context"
"fmt"
"strings"
"sync/atomic"

"google.golang.org/grpc/internal"
Expand Down Expand Up @@ -114,12 +113,8 @@ func (b *xdsResolverBuilder) Build(target resolver.Target, cc resolver.ClientCon
if err != nil {
return nil, err
}
endpoint := target.URL.Path
if endpoint == "" {
endpoint = target.URL.Opaque
}
endpoint = strings.TrimPrefix(endpoint, "/")
r.ldsResourceName = bootstrap.PopulateResourceTemplate(template, endpoint)
r.dataplaneAuthority = opts.Authority
r.ldsResourceName = bootstrap.PopulateResourceTemplate(template, target.Endpoint())
r.listenerWatcher = newListenerWatcher(r.ldsResourceName, r)
return r, nil
}
Expand Down Expand Up @@ -190,6 +185,12 @@ type xdsResolver struct {
serializer *grpcsync.CallbackSerializer
serializerCancel context.CancelFunc

// dataplaneAuthority is the authority used for the data plane connections,
// which is also used to select the VirtualHost within the xDS
// RouteConfiguration. This is %-encoded to match with VirtualHost Domain
// in xDS RouteConfiguration.
dataplaneAuthority string

ldsResourceName string
listenerWatcher *listenerWatcher
listenerUpdateRecvd bool
Expand Down Expand Up @@ -413,9 +414,9 @@ func (r *xdsResolver) onResolutionComplete() {
}

func (r *xdsResolver) applyRouteConfigUpdate(update xdsresource.RouteConfigUpdate) {
matchVh := xdsresource.FindBestMatchingVirtualHost(r.ldsResourceName, update.VirtualHosts)
matchVh := xdsresource.FindBestMatchingVirtualHost(r.dataplaneAuthority, update.VirtualHosts)
if matchVh == nil {
r.onError(fmt.Errorf("no matching virtual host found for %q", r.ldsResourceName))
r.onError(fmt.Errorf("no matching virtual host found for %q", r.dataplaneAuthority))
return
}
r.currentRouteConfig = update
Expand Down

0 comments on commit 90fc697

Please sign in to comment.