Skip to content

Commit 7aee163

Browse files
xds: add xDS transport custom Dialer support (#7586)
1 parent 9affdbb commit 7aee163

File tree

4 files changed

+264
-2
lines changed

4 files changed

+264
-2
lines changed

internal/xds/bootstrap/bootstrap.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ package bootstrap
2222

2323
import (
2424
"bytes"
25+
"context"
2526
"encoding/json"
2627
"fmt"
2728
"maps"
29+
"net"
2830
"net/url"
2931
"os"
3032
"slices"
@@ -179,6 +181,7 @@ type ServerConfig struct {
179181
// credentials and store it here for easy access.
180182
selectedCreds ChannelCreds
181183
credsDialOption grpc.DialOption
184+
dialerOption grpc.DialOption
182185

183186
cleanups []func()
184187
}
@@ -223,6 +226,16 @@ func (sc *ServerConfig) CredsDialOption() grpc.DialOption {
223226
return sc.credsDialOption
224227
}
225228

229+
// DialerOption returns the Dialer function that specifies how to dial the xDS
230+
// server determined by the first supported credentials from the configuration,
231+
// as a dial option.
232+
//
233+
// TODO(https://github.com/grpc/grpc-go/issues/7661): change ServerConfig type
234+
// to have a single method that returns all configured dial options.
235+
func (sc *ServerConfig) DialerOption() grpc.DialOption {
236+
return sc.dialerOption
237+
}
238+
226239
// Cleanups returns a collection of functions to be called when the xDS client
227240
// for this server is closed. Allows cleaning up resources created specifically
228241
// for this server.
@@ -275,6 +288,12 @@ func (sc *ServerConfig) MarshalJSON() ([]byte, error) {
275288
return json.Marshal(server)
276289
}
277290

291+
// dialer captures the Dialer method specified via the credentials bundle.
292+
type dialer interface {
293+
// Dialer specifies how to dial the xDS server.
294+
Dialer(context.Context, string) (net.Conn, error)
295+
}
296+
278297
// UnmarshalJSON takes the json data (a server) and unmarshals it to the struct.
279298
func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
280299
server := serverConfigJSON{}
@@ -298,6 +317,9 @@ func (sc *ServerConfig) UnmarshalJSON(data []byte) error {
298317
}
299318
sc.selectedCreds = cc
300319
sc.credsDialOption = grpc.WithCredentialsBundle(bundle)
320+
if d, ok := bundle.(dialer); ok {
321+
sc.dialerOption = grpc.WithContextDialer(d.Dialer)
322+
}
301323
sc.cleanups = append(sc.cleanups, cancel)
302324
break
303325
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
*
3+
* Copyright 2024 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package xds_test
20+
21+
import (
22+
"context"
23+
"encoding/json"
24+
"fmt"
25+
"net"
26+
"testing"
27+
28+
"github.com/google/uuid"
29+
"google.golang.org/grpc"
30+
"google.golang.org/grpc/credentials"
31+
"google.golang.org/grpc/credentials/insecure"
32+
"google.golang.org/grpc/internal"
33+
"google.golang.org/grpc/internal/stubserver"
34+
"google.golang.org/grpc/internal/testutils"
35+
"google.golang.org/grpc/internal/testutils/xds/e2e"
36+
internalbootstrap "google.golang.org/grpc/internal/xds/bootstrap"
37+
"google.golang.org/grpc/resolver"
38+
"google.golang.org/grpc/xds/bootstrap"
39+
40+
testgrpc "google.golang.org/grpc/interop/grpc_testing"
41+
testpb "google.golang.org/grpc/interop/grpc_testing"
42+
)
43+
44+
const testDialerCredsBuilderName = "test_dialer_creds"
45+
46+
// testDialerCredsBuilder implements the `Credentials` interface defined in
47+
// package `xds/bootstrap` and encapsulates an insecure credential with a
48+
// custom Dialer that specifies how to dial the xDS server.
49+
type testDialerCredsBuilder struct {
50+
dialerCalled chan struct{}
51+
}
52+
53+
func (t *testDialerCredsBuilder) Build(config json.RawMessage) (credentials.Bundle, func(), error) {
54+
cfg := &struct {
55+
MgmtServerAddress string `json:"mgmt_server_address"`
56+
}{}
57+
if err := json.Unmarshal(config, &cfg); err != nil {
58+
return nil, func() {}, fmt.Errorf("failed to unmarshal config: %v", err)
59+
}
60+
return &testDialerCredsBundle{insecure.NewBundle(), t.dialerCalled, cfg.MgmtServerAddress}, func() {}, nil
61+
}
62+
63+
func (t *testDialerCredsBuilder) Name() string {
64+
return testDialerCredsBuilderName
65+
}
66+
67+
// testDialerCredsBundle implements the `Bundle` interface defined in package
68+
// `credentials` and encapsulates an insecure credential with a custom Dialer
69+
// that specifies how to dial the xDS server.
70+
type testDialerCredsBundle struct {
71+
credentials.Bundle
72+
dialerCalled chan struct{}
73+
mgmtServerAddress string
74+
}
75+
76+
// Dialer specifies how to dial the xDS management server.
77+
func (t *testDialerCredsBundle) Dialer(context.Context, string) (net.Conn, error) {
78+
close(t.dialerCalled)
79+
// Create a pass-through connection (no-op) to the xDS management server.
80+
return net.Dial("tcp", t.mgmtServerAddress)
81+
}
82+
83+
func (s) TestClientCustomDialerFromCredentialsBundle(t *testing.T) {
84+
// Create and register the credentials bundle builder.
85+
credsBuilder := &testDialerCredsBuilder{dialerCalled: make(chan struct{})}
86+
bootstrap.RegisterCredentials(credsBuilder)
87+
88+
// Start an xDS management server.
89+
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
90+
91+
// Create bootstrap configuration pointing to the above management server.
92+
nodeID := uuid.New().String()
93+
bc, err := internalbootstrap.NewContentsForTesting(internalbootstrap.ConfigOptionsForTesting{
94+
Servers: []byte(fmt.Sprintf(`[{
95+
"server_uri": %q,
96+
"channel_creds": [{
97+
"type": %q,
98+
"config": {"mgmt_server_address": %q}
99+
}]
100+
}]`, mgmtServer.Address, testDialerCredsBuilderName, mgmtServer.Address)),
101+
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
102+
})
103+
if err != nil {
104+
t.Fatalf("Failed to create bootstrap configuration: %v", err)
105+
}
106+
107+
// Create an xDS resolver with the above bootstrap configuration.
108+
var resolverBuilder resolver.Builder
109+
if newResolver := internal.NewXDSResolverWithConfigForTesting; newResolver != nil {
110+
resolverBuilder, err = newResolver.(func([]byte) (resolver.Builder, error))(bc)
111+
if err != nil {
112+
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
113+
}
114+
}
115+
116+
// Spin up a test backend.
117+
server := stubserver.StartTestService(t, nil)
118+
defer server.Stop()
119+
120+
// Configure client side xDS resources on the management server.
121+
const serviceName = "my-service-client-side-xds"
122+
resources := e2e.DefaultClientResources(e2e.ResourceParams{
123+
DialTarget: serviceName,
124+
NodeID: nodeID,
125+
Host: "localhost",
126+
Port: testutils.ParsePort(t, server.Address),
127+
SecLevel: e2e.SecurityLevelNone,
128+
})
129+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
130+
defer cancel()
131+
if err := mgmtServer.Update(ctx, resources); err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
// Create a ClientConn and make a successful RPC. The insecure transport credentials passed into
136+
// the gRPC.NewClient is the credentials for the data plane communication with the test backend.
137+
cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolverBuilder))
138+
if err != nil {
139+
t.Fatalf("failed to dial local test server: %v", err)
140+
}
141+
defer cc.Close()
142+
143+
client := testgrpc.NewTestServiceClient(cc)
144+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
145+
t.Fatalf("EmptyCall() failed: %v", err)
146+
}
147+
148+
// Verify that the custom dialer was called.
149+
select {
150+
case <-ctx.Done():
151+
t.Fatalf("Timeout when waiting for custom dialer to be called")
152+
case <-credsBuilder.dialerCalled:
153+
}
154+
}

xds/internal/xdsclient/transport/transport.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ func New(opts Options) (*Transport, error) {
202202
Timeout: 20 * time.Second,
203203
}),
204204
}
205+
if dialerOpts := opts.ServerCfg.DialerOption(); dialerOpts != nil {
206+
dopts = append(dopts, dialerOpts)
207+
}
205208
grpcNewClient := transportinternal.GRPCNewClient.(func(string, ...grpc.DialOption) (*grpc.ClientConn, error))
206209
cc, err := grpcNewClient(opts.ServerCfg.ServerURI(), dopts...)
207210
if err != nil {

xds/internal/xdsclient/transport/transport_test.go

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@
1818
package transport_test
1919

2020
import (
21+
"context"
22+
"encoding/json"
23+
"net"
2124
"testing"
2225

2326
"google.golang.org/grpc"
24-
"google.golang.org/grpc/internal/xds/bootstrap"
27+
"google.golang.org/grpc/credentials"
28+
"google.golang.org/grpc/credentials/insecure"
29+
internalbootstrap "google.golang.org/grpc/internal/xds/bootstrap"
30+
"google.golang.org/grpc/xds/bootstrap"
2531
"google.golang.org/grpc/xds/internal/xdsclient/transport"
2632
"google.golang.org/grpc/xds/internal/xdsclient/transport/internal"
2733

@@ -39,7 +45,7 @@ func (s) TestNewWithGRPCDial(t *testing.T) {
3945
internal.GRPCNewClient = customDialer
4046
defer func() { internal.GRPCNewClient = oldDial }()
4147

42-
serverCfg, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{URI: "server-address"})
48+
serverCfg, err := internalbootstrap.ServerConfigForTesting(internalbootstrap.ServerConfigTestingOptions{URI: "server-address"})
4349
if err != nil {
4450
t.Fatalf("Failed to create server config for testing: %v", err)
4551
}
@@ -82,3 +88,80 @@ func (s) TestNewWithGRPCDial(t *testing.T) {
8288
t.Fatalf("transport.New(%+v) custom dialer called = true, want false", opts)
8389
}
8490
}
91+
92+
const testDialerCredsBuilderName = "test_dialer_creds"
93+
94+
// testDialerCredsBuilder implements the `Credentials` interface defined in
95+
// package `xds/bootstrap` and encapsulates an insecure credential with a
96+
// custom Dialer that specifies how to dial the xDS server.
97+
type testDialerCredsBuilder struct{}
98+
99+
func (t *testDialerCredsBuilder) Build(json.RawMessage) (credentials.Bundle, func(), error) {
100+
return &testDialerCredsBundle{insecure.NewBundle()}, func() {}, nil
101+
}
102+
103+
func (t *testDialerCredsBuilder) Name() string {
104+
return testDialerCredsBuilderName
105+
}
106+
107+
// testDialerCredsBundle implements the `Bundle` interface defined in package
108+
// `credentials` and encapsulates an insecure credential with a custom Dialer
109+
// that specifies how to dial the xDS server.
110+
type testDialerCredsBundle struct {
111+
credentials.Bundle
112+
}
113+
114+
func (t *testDialerCredsBundle) Dialer(context.Context, string) (net.Conn, error) {
115+
return nil, nil
116+
}
117+
118+
func (s) TestNewWithDialerFromCredentialsBundle(t *testing.T) {
119+
// Override grpc.NewClient with a custom one.
120+
doptsLen := 0
121+
customGRPCNewClient := func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
122+
doptsLen = len(opts)
123+
return grpc.NewClient(target, opts...)
124+
}
125+
oldGRPCNewClient := internal.GRPCNewClient
126+
internal.GRPCNewClient = customGRPCNewClient
127+
defer func() { internal.GRPCNewClient = oldGRPCNewClient }()
128+
129+
bootstrap.RegisterCredentials(&testDialerCredsBuilder{})
130+
serverCfg, err := internalbootstrap.ServerConfigForTesting(internalbootstrap.ServerConfigTestingOptions{
131+
URI: "trafficdirector.googleapis.com:443",
132+
ChannelCreds: []internalbootstrap.ChannelCreds{{Type: testDialerCredsBuilderName}},
133+
})
134+
if err != nil {
135+
t.Fatalf("Failed to create server config for testing: %v", err)
136+
}
137+
if serverCfg.DialerOption() == nil {
138+
t.Fatalf("Dialer for xDS transport in server config for testing is nil, want non-nil")
139+
}
140+
// Create a new transport.
141+
opts := transport.Options{
142+
ServerCfg: serverCfg,
143+
NodeProto: &v3corepb.Node{},
144+
OnRecvHandler: func(update transport.ResourceUpdate, onDone func()) error {
145+
onDone()
146+
return nil
147+
},
148+
OnErrorHandler: func(error) {},
149+
OnSendHandler: func(*transport.ResourceSendInfo) {},
150+
}
151+
c, err := transport.New(opts)
152+
defer func() {
153+
if c != nil {
154+
c.Close()
155+
}
156+
}()
157+
if err != nil {
158+
t.Fatalf("transport.New(%v) failed: %v", opts, err)
159+
}
160+
// Verify there are three dial options passed to the custom grpc.NewClient.
161+
// The first is opts.ServerCfg.CredsDialOption(), the second is
162+
// grpc.WithKeepaliveParams(), and the third is opts.ServerCfg.DialerOption()
163+
// from the credentials bundle.
164+
if doptsLen != 3 {
165+
t.Fatalf("transport.New(%v) custom grpc.NewClient called with %d dial options, want 3", opts, doptsLen)
166+
}
167+
}

0 commit comments

Comments
 (0)