]> git.feebdaed.xyz Git - 0xmirror/grpc-go.git/commitdiff
xds/googlec2p: support custom bootstrap config per channel. (#8648)
authorPranjali-2501 <87357388+Pranjali-2501@users.noreply.github.com>
Mon, 27 Oct 2025 08:21:01 +0000 (13:51 +0530)
committerGitHub <noreply@github.com>
Mon, 27 Oct 2025 08:21:01 +0000 (13:51 +0530)
xds/googlec2p: Fix channel-specific xDS bootstrap configurations by
allowing xdsclient creation with per-target config. Removes global
fallback config usage, enabling multiple distinct xDS clients to coexist
in the same process.

internal/xds/xdsclient/pool.go
xds/googledirectpath/googlec2p.go
xds/googledirectpath/googlec2p_test.go

index eb0197e09a7f9f2419725b6b691c0dac4e620e92..fa11a950077548817b8eb5ddeb18207ff7419196 100644 (file)
@@ -49,7 +49,7 @@ type Pool struct {
        // 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)
@@ -73,6 +73,11 @@ type OptionsForTesting struct {
        // 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.
@@ -91,6 +96,17 @@ func NewPool(config *bootstrap.Config) *Pool {
        }
 }
 
+// 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.
@@ -99,7 +115,7 @@ func NewPool(config *bootstrap.Config) *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
@@ -126,7 +142,7 @@ func (p *Pool) NewClientForTesting(opts OptionsForTesting) (XDSClient, func(), e
        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
        }
@@ -159,6 +175,7 @@ func (p *Pool) GetClientForTesting(name string) (XDSClient, func(), error) {
 // 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()
@@ -251,30 +268,34 @@ func (p *Pool) clientRefCountedClose(name string) {
 // 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
index 9ef59f1a92a7ba6b534bc87acdf5b7f0473fb9e6..01cca3d3e720f7fffaa4bb948c05fc8b690d60b3 100644 (file)
@@ -119,6 +119,16 @@ func getXdsServerURI() string {
        return fmt.Sprintf("dns:///directpath-pa.%s", universeDomain)
 }
 
+type c2pResolverWrapper struct {
+       resolver.Resolver
+       cancel func() // Release the reference to the xDS client that was created in Build().
+}
+
+func (r *c2pResolverWrapper) Close() {
+       r.Resolver.Close()
+       r.cancel()
+}
+
 type c2pResolverBuilder struct{}
 
 func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
@@ -161,7 +171,6 @@ func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts
        if err != nil {
                return nil, fmt.Errorf("failed to parse bootstrap contents: %s, %v", string(cfgJSON), err)
        }
-       xdsClientPool.SetFallbackBootstrapConfig(config)
 
        t = resolver.Target{
                URL: url.URL{
@@ -170,7 +179,24 @@ func (c2pResolverBuilder) Build(t resolver.Target, cc resolver.ClientConn, opts
                        Path:   t.URL.Path,
                },
        }
-       return resolver.Get(xdsName).Build(t, cc, opts)
+
+       // Create a new xDS client for this target using the provided bootstrap
+       // configuration. This client is stored in the xdsclient pool’s internal
+       // cache, keeping it alive and associated with this resolver until Closed().
+       // While the c2p resolver itself does not directly use the client, creating
+       // it ensures that when the xDS resolver later requests a client for the
+       // same target, the existing instance will be reused.
+       _, cancel, err := xdsClientPool.NewClientWithConfig(t.String(), opts.MetricsRecorder, config)
+       if err != nil {
+               return nil, fmt.Errorf("failed to create xds client: %v", err)
+       }
+
+       r, err := resolver.Get(xdsName).Build(t, cc, opts)
+       if err != nil {
+               cancel()
+               return nil, err
+       }
+       return &c2pResolverWrapper{Resolver: r, cancel: cancel}, nil
 }
 
 func (b c2pResolverBuilder) Scheme() string {
index 74ee65db49876a8d4c8c714e8fac058141caa6c8..3d9972baef23b97672dd279ba4b364fca1415876 100644 (file)
@@ -21,6 +21,7 @@ package googledirectpath
 import (
        "context"
        "encoding/json"
+       "net/url"
        "strconv"
        "strings"
        "testing"
@@ -31,6 +32,7 @@ import (
        "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"
@@ -126,6 +128,25 @@ func expectedNodeJSON(ipv6Capable bool) []byte {
        }`)
 }
 
+// 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
@@ -155,9 +176,9 @@ func (s) TestBuildWithBootstrapEnvSet(t *testing.T) {
                        }
                        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)
                        }
                })
        }
@@ -314,24 +335,20 @@ func (s) TestBuildXDS(t *testing.T) {
                        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)
                })
        }
 }
@@ -415,20 +432,16 @@ func (s) TestSetUniverseDomainNonDefault(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)
+       // 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
@@ -452,9 +465,9 @@ func (s) TestSetUniverseDomainNonDefault(t *testing.T) {
                },
                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) {
@@ -484,20 +497,16 @@ 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
@@ -520,9 +529,8 @@ func (s) TestDefaultUniverseDomain(t *testing.T) {
                },
                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"
@@ -549,3 +557,88 @@ func (s) TestSetUniverseDomainEmptyString(t *testing.T) {
                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)
+}