// config.
mu sync.Mutex
clients map[string]*clientImpl
- fallbackConfig *bootstrap.Config
+ fallbackConfig *bootstrap.Config // TODO(i/8661): remove fallbackConfig.
// getConfiguration is a sync.OnceValues that attempts to read the bootstrap
// configuration from environment variables once.
getConfiguration func() (*bootstrap.Config, error)
// MetricsRecorder is the metrics recorder the xDS Client will use. If
// unspecified, uses a no-op MetricsRecorder.
MetricsRecorder estats.MetricsRecorder
+
+ // Config is the xDS bootstrap configuration that will be used to initialize
+ // the client. If unset, the client will use the config provided by env
+ // variables.
+ Config *bootstrap.Config
}
// NewPool creates a new xDS client pool with the given bootstrap config.
}
}
+// NewClientWithConfig returns an xDS client with the given name from the pool. If the
+// client doesn't already exist, it creates a new xDS client and adds it to the
+// pool.
+//
+// The second return value represents a close function which the caller is
+// expected to invoke once they are done using the client. It is safe for the
+// caller to invoke this close function multiple times.
+func (p *Pool) NewClientWithConfig(name string, metricsRecorder estats.MetricsRecorder, config *bootstrap.Config) (XDSClient, func(), error) {
+ return p.newRefCounted(name, metricsRecorder, defaultWatchExpiryTimeout, config)
+}
+
// NewClient returns an xDS client with the given name from the pool. If the
// client doesn't already exist, it creates a new xDS client and adds it to the
// pool.
// expected to invoke once they are done using the client. It is safe for the
// caller to invoke this close function multiple times.
func (p *Pool) NewClient(name string, metricsRecorder estats.MetricsRecorder) (XDSClient, func(), error) {
- return p.newRefCounted(name, metricsRecorder, defaultWatchExpiryTimeout)
+ return p.newRefCounted(name, metricsRecorder, defaultWatchExpiryTimeout, nil)
}
// NewClientForTesting returns an xDS client configured with the provided
if opts.MetricsRecorder == nil {
opts.MetricsRecorder = istats.NewMetricsRecorderList(nil)
}
- c, cancel, err := p.newRefCounted(opts.Name, opts.MetricsRecorder, opts.WatchExpiryTimeout)
+ c, cancel, err := p.newRefCounted(opts.Name, opts.MetricsRecorder, opts.WatchExpiryTimeout, opts.Config)
if err != nil {
return nil, nil, err
}
// SetFallbackBootstrapConfig is used to specify a bootstrap configuration
// that will be used as a fallback when the bootstrap environment variables
// are not defined.
+// TODO(i/8661): remove SetFallbackBootstrapConfig function.
func (p *Pool) SetFallbackBootstrapConfig(config *bootstrap.Config) {
p.mu.Lock()
defer p.mu.Unlock()
// newRefCounted creates a new reference counted xDS client implementation for
// name, if one does not exist already. If an xDS client for the given name
// exists, it gets a reference to it and returns it.
-func (p *Pool) newRefCounted(name string, metricsRecorder estats.MetricsRecorder, watchExpiryTimeout time.Duration) (*clientImpl, func(), error) {
+func (p *Pool) newRefCounted(name string, metricsRecorder estats.MetricsRecorder, watchExpiryTimeout time.Duration, bConfig *bootstrap.Config) (*clientImpl, func(), error) {
p.mu.Lock()
defer p.mu.Unlock()
- config, err := p.getConfiguration()
- if err != nil {
- return nil, nil, fmt.Errorf("xds: failed to read xDS bootstrap config from env vars: %v", err)
+ if c := p.clients[name]; c != nil {
+ c.incrRef()
+ return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil
}
+ config := bConfig
if config == nil {
- // If the environment variables are not set, then fallback bootstrap
- // configuration should be set before attempting to create an xDS client,
- // else xDS client creation will fail.
- config = p.fallbackConfig
+ var err error
+ config, err = p.getConfiguration()
+ if err != nil {
+ return nil, nil, fmt.Errorf("xds: failed to read xDS bootstrap config from env vars: %v", err)
+ }
+ if config == nil {
+ // If the environment variables are not set, then fallback bootstrap
+ // configuration should be set before attempting to create an xDS client,
+ // else xDS client creation will fail.
+ config = p.fallbackConfig
+ }
}
+
if config == nil {
return nil, nil, fmt.Errorf("failed to read xDS bootstrap config from env vars: bootstrap environment variables (%q or %q) not defined and fallback config not set", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
}
- if c := p.clients[name]; c != nil {
- c.incrRef()
- return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil
- }
-
c, err := newClientImpl(config, metricsRecorder, name, watchExpiryTimeout)
if err != nil {
return nil, nil, err
import (
"context"
"encoding/json"
+ "net/url"
"strconv"
"strings"
"testing"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
+ "google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/xds/bootstrap"
"google.golang.org/grpc/internal/xds/xdsclient"
testgrpc "google.golang.org/grpc/interop/grpc_testing"
}`)
}
+// verifyXDSClientBootstrapConfig checks that an xDS client with the given name
+// exists in the pool and that its bootstrap config matches the expected config.
+func verifyXDSClientBootstrapConfig(t *testing.T, pool *xdsclient.Pool, name string, wantConfig *bootstrap.Config) {
+ t.Helper()
+ client, close, err := pool.GetClientForTesting(name)
+ if err != nil {
+ t.Fatalf("GetClientForTesting(%q) failed to get xds client: %v", name, err)
+ }
+ defer close()
+
+ gotConfig := client.BootstrapConfig()
+ if gotConfig == nil {
+ t.Fatalf("Failed to get bootstrap config: %v", err)
+ }
+ if diff := cmp.Diff(wantConfig, gotConfig); diff != "" {
+ t.Fatalf("xdsClient.BootstrapConfig() for client %q returned unexpected diff (-want +got):\n%s", name, diff)
+ }
+}
+
// Tests the scenario where the bootstrap env vars are set and we're running on
// GCE. The test builds a google-c2p resolver and verifies that an xDS resolver
// is built and that we don't fallback to DNS (because federation is enabled by
}
defer r.Close()
- // Build should return xDS, not DNS.
- if r != testXDSResolver {
- t.Fatalf("Build() returned %#v, want xds resolver", r)
+ // Build should return wrapped xDS resolver, not DNS.
+ if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
+ t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
}
})
}
defer func() { getIPv6Capable = oldGetIPv6Capability }()
// Build the google-c2p resolver.
- r, err := builder.Build(resolver.Target{}, nil, resolver.BuildOptions{})
+ target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
+ r, err := builder.Build(target, nil, resolver.BuildOptions{})
if err != nil {
t.Fatalf("failed to build resolver: %v", err)
}
defer r.Close()
- // Build should return xDS, not DNS.
- if r != testXDSResolver {
- t.Fatalf("Build() returned %#v, want xds resolver", r)
+ // Build should return wrapped xDS resolver, not DNS.
+ if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
+ t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
}
- gotConfig := xdsClientPool.BootstrapConfigForTesting()
- if gotConfig == nil {
- t.Fatalf("Failed to get bootstrap config: %v", err)
- }
- if diff := cmp.Diff(tt.wantBootstrapConfig, gotConfig); diff != "" {
- t.Fatalf("Unexpected diff in bootstrap config (-want +got):\n%s", diff)
- }
+ xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
+ verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), tt.wantBootstrapConfig)
})
}
}
defer func() { xdsClientPool = oldXdsClientPool }()
// Build the google-c2p resolver.
- r, err := builder.Build(resolver.Target{}, nil, resolver.BuildOptions{})
+ target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
+ r, err := builder.Build(target, nil, resolver.BuildOptions{})
if err != nil {
t.Fatalf("failed to build resolver: %v", err)
}
defer r.Close()
- // Build should return xDS, not DNS.
- if r != testXDSResolver {
- t.Fatalf("Build() returned %#v, want xds resolver", r)
- }
-
- gotConfig := xdsClientPool.BootstrapConfigForTesting()
- if gotConfig == nil {
- t.Fatalf("Failed to get bootstrap config: %v", err)
+ // Build should return wrapped xDS resolver, not DNS.
+ if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
+ t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
}
// Check that we use directpath-pa.test-universe-domain.test in the
},
Node: expectedNodeJSON(false),
})
- if diff := cmp.Diff(wantBootstrapConfig, gotConfig); diff != "" {
- t.Fatalf("Unexpected diff in bootstrap config (-want +got):\n%s", diff)
- }
+
+ xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
+ verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), wantBootstrapConfig)
}
func (s) TestDefaultUniverseDomain(t *testing.T) {
defer func() { xdsClientPool = oldXdsClientPool }()
// Build the google-c2p resolver.
- r, err := builder.Build(resolver.Target{}, nil, resolver.BuildOptions{})
+ target := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
+ r, err := builder.Build(target, nil, resolver.BuildOptions{})
if err != nil {
t.Fatalf("failed to build resolver: %v", err)
}
defer r.Close()
// Build should return xDS, not DNS.
- if r != testXDSResolver {
- t.Fatalf("Build() returned %#v, want xds resolver", r)
- }
-
- gotConfig := xdsClientPool.BootstrapConfigForTesting()
- if gotConfig == nil {
- t.Fatalf("Failed to get bootstrap config: %v", err)
+ if r, ok := r.(*c2pResolverWrapper); !ok || r.Resolver != testXDSResolver {
+ t.Fatalf("Build() returned %#v, want c2pResolverWrapper", r)
}
// Check that we use directpath-pa.googleapis.com in the bootstrap config
},
Node: expectedNodeJSON(false),
})
- if diff := cmp.Diff(wantBootstrapConfig, gotConfig); diff != "" {
- t.Fatalf("Unexpected diff in bootstrap config (-want +got):\n%s", diff)
- }
+ xdsTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: target.URL.Path}}
+ verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), wantBootstrapConfig)
// Now set universe domain to something different than the default, it should fail
domain := "test-universe-domain.test"
t.Fatalf("googlec2p.SetUniverseDomain(\"\") returned error: %v, want: %v", err, wantErr)
}
}
+
+// TestCreateMultipleXDSClients validates that multiple xds clients with
+// different bootstrap config coexist in the same pool. It confirms that
+// a client created by the google-c2p resolver does not interfere with an
+// explicitly created client using a different bootstrap configuration.
+func (s) TestCreateMultipleXDSClients(t *testing.T) {
+ replaceResolvers(t)
+ simulateRunningOnGCE(t, true)
+ useCleanUniverseDomain(t)
+
+ // Override the zone returned by the metadata server.
+ oldGetZone := getZone
+ getZone = func(time.Duration) string { return "test-zone" }
+ defer func() { getZone = oldGetZone }()
+
+ // Override the random func used in the node ID.
+ origRandInd := randInt
+ randInt = func() int { return 666 }
+ defer func() { randInt = origRandInd }()
+
+ // Override IPv6 capability returned by the metadata server.
+ oldGetIPv6Capability := getIPv6Capable
+ getIPv6Capable = func(time.Duration) bool { return false }
+ defer func() { getIPv6Capable = oldGetIPv6Capability }()
+
+ // Define bootstrap config for generic xds resolver
+ genericXDSConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
+ Servers: []byte(`[{
+ "server_uri": "dns:///regular-xds.googleapis.com",
+ "channel_creds": [{"type": "google_default"}],
+ "server_features": []
+ }]`),
+ Node: []byte(`{"id": "regular-xds-node", "locality": {"zone": "test-zone"}}`),
+ })
+
+ // Override xDS client pool.
+ oldXdsClientPool := xdsClientPool
+ xdsClientPool = xdsclient.NewPool(genericXDSConfig)
+ defer func() { xdsClientPool = oldXdsClientPool }()
+
+ // Create generic xds client.
+ xdsTarget := resolver.Target{URL: *testutils.MustParseURL("xds:///target")}
+ _, closeGeneric, err := xdsClientPool.NewClientForTesting(xdsclient.OptionsForTesting{
+ Name: xdsTarget.String(),
+ Config: genericXDSConfig,
+ })
+ if err != nil {
+ t.Fatalf("xdsClientPool.NewClientForTesting(%q) failed: %v", xdsTarget.String(), err)
+ }
+ defer closeGeneric()
+
+ verifyXDSClientBootstrapConfig(t, xdsClientPool, xdsTarget.String(), genericXDSConfig)
+
+ // Build the google-c2p resolver.
+ c2pBuilder := resolver.Get(c2pScheme)
+ c2pTarget := resolver.Target{URL: url.URL{Scheme: c2pScheme, Path: "test-path"}}
+ tcc := &testutils.ResolverClientConn{Logger: t}
+ c2pRes, err := c2pBuilder.Build(c2pTarget, tcc, resolver.BuildOptions{})
+ if err != nil {
+ t.Fatalf("Failed to build resolver: %v", err)
+ }
+ defer c2pRes.Close()
+
+ // Bootstrap config for c2p resolver.
+ c2pConfig := bootstrapConfig(t, bootstrap.ConfigOptionsForTesting{
+ Servers: []byte(`[{
+ "server_uri": "dns:///directpath-pa.googleapis.com",
+ "channel_creds": [{"type": "google_default"}],
+ "server_features": ["ignore_resource_deletion"]
+ }]`),
+ Authorities: map[string]json.RawMessage{
+ "traffic-director-c2p.xds.googleapis.com": []byte(`{
+ "xds_servers": [{
+ "server_uri": "dns:///directpath-pa.googleapis.com",
+ "channel_creds": [{"type": "google_default"}],
+ "server_features": ["ignore_resource_deletion"]
+ }]
+ }`),
+ },
+ Node: expectedNodeJSON(false),
+ })
+
+ c2pXDSTarget := resolver.Target{URL: url.URL{Scheme: xdsName, Host: c2pAuthority, Path: c2pTarget.URL.Path}}
+ verifyXDSClientBootstrapConfig(t, xdsClientPool, c2pXDSTarget.String(), c2pConfig)
+}