Repository: grpc/grpc-go
Pull request #8413 (2025-06-25 11:28:53)

xds: cleanup internal testing functions for env vars that have long been removed
RELEASE NOTES: none


Patch:
diff --git a/.github/mergeable.yml b/.github/mergeable.yml
deleted file mode 100644
index 187de982..00000000
--- a/.github/mergeable.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-version: 2
-mergeable:
-  - when: pull_request.*
-    validate:
-      - do: label
-        must_include:
-          regex: '^Type:'
-      - do: description
-        must_include:
-          # Allow:
-          # RELEASE NOTES: none (case insensitive)
-          #
-          # RELEASE NOTES: N/A (case insensitive)
-          #
-          # RELEASE NOTES:
-          # * <text>
-          regex: '^RELEASE NOTES:\s*([Nn][Oo][Nn][Ee]|[Nn]/[Aa]|\n(\*|-)\s*.+)$'
-          regex_flag: 'm'
-      - do: milestone
-        must_include:
-          regex: 'Release$'
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index 1edd5609..5a836e2f 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -3,6 +3,9 @@ on:
   pull_request:
     types: [opened, edited, synchronize, labeled, unlabeled, milestoned, demilestoned]
 
+permissions:
+  contents: read
+
 jobs:
   validate:
     name: Validate PR
diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go
index 2fdaed88..65fde7dc 100644
--- a/internal/envconfig/envconfig.go
+++ b/internal/envconfig/envconfig.go
@@ -26,26 +26,37 @@ import (
 )
 
 var (
-	// TXTErrIgnore is set if TXT errors should be ignored ("GRPC_GO_IGNORE_TXT_ERRORS" is not "false").
+	// EnableTXTServiceConfig is set if the DNS resolver should perform TXT
+	// lookups for service config ("GRPC_ENABLE_TXT_SERVICE_CONFIG" is not
+	// "false").
+	EnableTXTServiceConfig = boolFromEnv("GRPC_ENABLE_TXT_SERVICE_CONFIG", true)
+
+	// TXTErrIgnore is set if TXT errors should be ignored
+	// ("GRPC_GO_IGNORE_TXT_ERRORS" is not "false").
 	TXTErrIgnore = boolFromEnv("GRPC_GO_IGNORE_TXT_ERRORS", true)
+
 	// RingHashCap indicates the maximum ring size which defaults to 4096
 	// entries but may be overridden by setting the environment variable
 	// "GRPC_RING_HASH_CAP".  This does not override the default bounds
 	// checking which NACKs configs specifying ring sizes > 8*1024*1024 (~8M).
 	RingHashCap = uint64FromEnv("GRPC_RING_HASH_CAP", 4096, 1, 8*1024*1024)
+
 	// ALTSMaxConcurrentHandshakes is the maximum number of concurrent ALTS
 	// handshakes that can be performed.
 	ALTSMaxConcurrentHandshakes = uint64FromEnv("GRPC_ALTS_MAX_CONCURRENT_HANDSHAKES", 100, 1, 100)
+
 	// EnforceALPNEnabled is set if TLS connections to servers with ALPN disabled
 	// should be rejected. The HTTP/2 protocol requires ALPN to be enabled, this
 	// option is present for backward compatibility. This option may be overridden
 	// by setting the environment variable "GRPC_ENFORCE_ALPN_ENABLED" to "true"
 	// or "false".
 	EnforceALPNEnabled = boolFromEnv("GRPC_ENFORCE_ALPN_ENABLED", true)
+
 	// XDSFallbackSupport is the env variable that controls whether support for
 	// xDS fallback is turned on. If this is unset or is false, only the first
 	// xDS server in the list of server configs will be used.
 	XDSFallbackSupport = boolFromEnv("GRPC_EXPERIMENTAL_XDS_FALLBACK", true)
+
 	// NewPickFirstEnabled is set if the new pickfirst leaf policy is to be used
 	// instead of the exiting pickfirst implementation. This can be disabled by
 	// setting the environment variable "GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST"
diff --git a/internal/internal.go b/internal/internal.go
index 3ac798e8..2699223a 100644
--- a/internal/internal.go
+++ b/internal/internal.go
@@ -182,35 +182,6 @@ var (
 	// other features, including the CSDS service.
 	NewXDSResolverWithClientForTesting any // func(xdsclient.XDSClient) (resolver.Builder, error)
 
-	// RegisterRLSClusterSpecifierPluginForTesting registers the RLS Cluster
-	// Specifier Plugin for testing purposes, regardless of the XDSRLS environment
-	// variable.
-	//
-	// TODO: Remove this function once the RLS env var is removed.
-	RegisterRLSClusterSpecifierPluginForTesting func()
-
-	// UnregisterRLSClusterSpecifierPluginForTesting unregisters the RLS Cluster
-	// Specifier Plugin for testing purposes. This is needed because there is no way
-	// to unregister the RLS Cluster Specifier Plugin after registering it solely
-	// for testing purposes using RegisterRLSClusterSpecifierPluginForTesting().
-	//
-	// TODO: Remove this function once the RLS env var is removed.
-	UnregisterRLSClusterSpecifierPluginForTesting func()
-
-	// RegisterRBACHTTPFilterForTesting registers the RBAC HTTP Filter for testing
-	// purposes, regardless of the RBAC environment variable.
-	//
-	// TODO: Remove this function once the RBAC env var is removed.
-	RegisterRBACHTTPFilterForTesting func()
-
-	// UnregisterRBACHTTPFilterForTesting unregisters the RBAC HTTP Filter for
-	// testing purposes. This is needed because there is no way to unregister the
-	// HTTP Filter after registering it solely for testing purposes using
-	// RegisterRBACHTTPFilterForTesting().
-	//
-	// TODO: Remove this function once the RBAC env var is removed.
-	UnregisterRBACHTTPFilterForTesting func()
-
 	// ORCAAllowAnyMinReportingInterval is for examples/orca use ONLY.
 	ORCAAllowAnyMinReportingInterval any // func(so *orca.ServiceOptions)
 
diff --git a/internal/resolver/dns/dns_resolver.go b/internal/resolver/dns/dns_resolver.go
index ba5c5a95..ada5251c 100644
--- a/internal/resolver/dns/dns_resolver.go
+++ b/internal/resolver/dns/dns_resolver.go
@@ -132,13 +132,13 @@ func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts
 	// DNS address (non-IP).
 	ctx, cancel := context.WithCancel(context.Background())
 	d := &dnsResolver{
-		host:                 host,
-		port:                 port,
-		ctx:                  ctx,
-		cancel:               cancel,
-		cc:                   cc,
-		rn:                   make(chan struct{}, 1),
-		disableServiceConfig: opts.DisableServiceConfig,
+		host:                host,
+		port:                port,
+		ctx:                 ctx,
+		cancel:              cancel,
+		cc:                  cc,
+		rn:                  make(chan struct{}, 1),
+		enableServiceConfig: envconfig.EnableTXTServiceConfig && !opts.DisableServiceConfig,
 	}
 
 	d.resolver, err = internal.NewNetResolver(target.URL.Host)
@@ -181,8 +181,8 @@ type dnsResolver struct {
 	// finishes, race detector sometimes will warn lookup (READ the lookup
 	// function pointers) inside watcher() goroutine has data race with
 	// replaceNetFunc (WRITE the lookup function pointers).
-	wg                   sync.WaitGroup
-	disableServiceConfig bool
+	wg                  sync.WaitGroup
+	enableServiceConfig bool
 }
 
 // ResolveNow invoke an immediate resolution of the target that this
@@ -346,7 +346,7 @@ func (d *dnsResolver) lookup() (*resolver.State, error) {
 	if len(srv) > 0 {
 		state = grpclbstate.Set(state, &grpclbstate.State{BalancerAddresses: srv})
 	}
-	if !d.disableServiceConfig {
+	if d.enableServiceConfig {
 		state.ServiceConfig = d.lookupTXT(ctx)
 	}
 	return &state, nil
diff --git a/internal/xds/bootstrap/bootstrap.go b/internal/xds/bootstrap/bootstrap.go
index f409e4bd..6c7b2332 100644
--- a/internal/xds/bootstrap/bootstrap.go
+++ b/internal/xds/bootstrap/bootstrap.go
@@ -576,6 +576,9 @@ func (c *Config) UnmarshalJSON(data []byte) error {
 // the presence of the errors) and may return a Config object with certain
 // fields left unspecified, in which case the caller should use some sane
 // defaults.
+//
+// This function returns an error if it's unable to parse the contents of the
+// bootstrap config. It returns (nil, nil) if none of the env vars are set.
 func GetConfiguration() (*Config, error) {
 	fName := envconfig.XDSBootstrapFileName
 	fContent := envconfig.XDSBootstrapFileContent
@@ -598,7 +601,7 @@ func GetConfiguration() (*Config, error) {
 		return NewConfigFromContents([]byte(fContent))
 	}
 
-	return nil, fmt.Errorf("bootstrap environment variables (%q or %q) not defined", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
+	return nil, nil
 }
 
 // NewConfigFromContents creates a new bootstrap configuration from the provided
diff --git a/xds/internal/clients/xdsclient/channel.go b/xds/internal/clients/xdsclient/channel.go
index 0e12e057..2d424b81 100644
--- a/xds/internal/clients/xdsclient/channel.go
+++ b/xds/internal/clients/xdsclient/channel.go
@@ -253,20 +253,10 @@ func decodeResponse(opts *DecodeOptions, rType *ResourceType, resp response) (ma
 	perResourceErrors := make(map[string]error) // Tracks resource validation errors, where we have a resource name.
 	ret := make(map[string]dataAndErrTuple)     // Return result, a map from resource name to either resource data or error.
 	for _, r := range resp.resources {
-		// Unwrap and validate the resource, but preserve the original bytes for
-		// decoding. This is required for resource types that don't have a name
-		// field in the resource itself, but only have one in the wrapped
-		// resource.
-		inner, err := xdsresource.UnwrapResource(r)
-		if err != nil {
-			topLevelErrors = append(topLevelErrors, err)
-			continue
-		}
-		if _, ok := opts.Config.ResourceTypes[inner.GetTypeUrl()]; !ok || inner.GetTypeUrl() != resp.typeURL {
-			topLevelErrors = append(topLevelErrors, xdsresource.NewErrorf(xdsresource.ErrorTypeResourceTypeUnsupported, "unexpected resource type: %q ", inner.GetTypeUrl()))
-			continue
-		}
-		result, err := rType.Decoder.Decode(r.GetValue(), *opts)
+		result, err := rType.Decoder.Decode(AnyProto{
+			TypeURL: r.GetTypeUrl(),
+			Value:   r.GetValue(),
+		}, *opts)
 
 		// Name field of the result is left unpopulated only when resource
 		// deserialization fails.
diff --git a/xds/internal/clients/xdsclient/resource_type.go b/xds/internal/clients/xdsclient/resource_type.go
index 8ca466ed..ae8e21df 100644
--- a/xds/internal/clients/xdsclient/resource_type.go
+++ b/xds/internal/clients/xdsclient/resource_type.go
@@ -51,9 +51,21 @@ type Decoder interface {
 	// Decode deserializes and validates an xDS resource as received from the
 	// xDS management server.
 	//
-	// If deserialization fails or resource validation fails, it returns a
-	// non-nil error. Otherwise, returns a fully populated DecodeResult.
-	Decode(resource []byte, options DecodeOptions) (*DecodeResult, error)
+	// The `resource` parameter may contain a value of the serialized wrapped
+	// resource (i.e. with the type URL
+	// `type.googleapis.com/envoy.service.discovery.v3.Resource`).
+	// Implementations are responsible for unwrapping the underlying resource if
+	// it is wrapped.
+	//
+	// If unmarshalling or validation fails, it returns a non-nil error.
+	// Otherwise, returns a fully populated DecodeResult.
+	Decode(resource AnyProto, options DecodeOptions) (*DecodeResult, error)
+}
+
+// AnyProto contains the type URL and serialized proto data of an xDS resource.
+type AnyProto struct {
+	TypeURL string
+	Value   []byte
 }
 
 // DecodeOptions wraps the options required by ResourceType implementations for
diff --git a/xds/internal/clusterspecifier/rls/rls.go b/xds/internal/clusterspecifier/rls/rls.go
index 89837605..50f58cd4 100644
--- a/xds/internal/clusterspecifier/rls/rls.go
+++ b/xds/internal/clusterspecifier/rls/rls.go
@@ -34,16 +34,6 @@ import (
 
 func init() {
 	clusterspecifier.Register(rls{})
-
-	// TODO: Remove these once the RLS env var is removed.
-	internal.RegisterRLSClusterSpecifierPluginForTesting = func() {
-		clusterspecifier.Register(rls{})
-	}
-	internal.UnregisterRLSClusterSpecifierPluginForTesting = func() {
-		for _, typeURL := range rls.TypeURLs(rls{}) {
-			clusterspecifier.UnregisterForTesting(typeURL)
-		}
-	}
 }
 
 type rls struct{}
diff --git a/xds/internal/httpfilter/rbac/rbac.go b/xds/internal/httpfilter/rbac/rbac.go
index bcda2ab0..260dad75 100644
--- a/xds/internal/httpfilter/rbac/rbac.go
+++ b/xds/internal/httpfilter/rbac/rbac.go
@@ -25,7 +25,6 @@ import (
 	"fmt"
 	"strings"
 
-	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/resolver"
 	"google.golang.org/grpc/internal/xds/rbac"
 	"google.golang.org/grpc/xds/internal/httpfilter"
@@ -38,16 +37,6 @@ import (
 
 func init() {
 	httpfilter.Register(builder{})
-
-	// TODO: Remove these once the RBAC env var is removed.
-	internal.RegisterRBACHTTPFilterForTesting = func() {
-		httpfilter.Register(builder{})
-	}
-	internal.UnregisterRBACHTTPFilterForTesting = func() {
-		for _, typeURL := range builder.TypeURLs(builder{}) {
-			httpfilter.UnregisterForTesting(typeURL)
-		}
-	}
 }
 
 type builder struct {
diff --git a/xds/internal/xdsclient/pool.go b/xds/internal/xdsclient/pool.go
index 113e6e8e..e5a34ae3 100644
--- a/xds/internal/xdsclient/pool.go
+++ b/xds/internal/xdsclient/pool.go
@@ -25,6 +25,7 @@ import (
 
 	v3statuspb "github.com/envoyproxy/go-control-plane/envoy/service/status/v3"
 	estats "google.golang.org/grpc/experimental/stats"
+	"google.golang.org/grpc/internal/envconfig"
 	istats "google.golang.org/grpc/internal/stats"
 	"google.golang.org/grpc/internal/xds/bootstrap"
 	"google.golang.org/protobuf/proto"
@@ -34,7 +35,10 @@ var (
 	// DefaultPool is the default pool for xDS clients. It is created at init
 	// time and reads bootstrap configuration from env vars to create the xDS
 	// client.
-	DefaultPool = &Pool{clients: make(map[string]*clientImpl)}
+	DefaultPool = &Pool{
+		clients:          make(map[string]*clientImpl),
+		getConfiguration: sync.OnceValues(bootstrap.GetConfiguration),
+	}
 )
 
 // Pool represents a pool of xDS clients that share the same bootstrap
@@ -43,9 +47,12 @@ type Pool struct {
 	// Note that mu should ideally only have to guard clients. But here, we need
 	// it to guard config as well since SetFallbackBootstrapConfig writes to
 	// config.
-	mu      sync.Mutex
-	clients map[string]*clientImpl
-	config  *bootstrap.Config
+	mu             sync.Mutex
+	clients        map[string]*clientImpl
+	fallbackConfig *bootstrap.Config
+	// getConfiguration is a sync.OnceValues that attempts to read the bootstrap
+	// configuration from environment variables once.
+	getConfiguration func() (*bootstrap.Config, error)
 }
 
 // OptionsForTesting contains options to configure xDS client creation for
@@ -78,7 +85,9 @@ type OptionsForTesting struct {
 func NewPool(config *bootstrap.Config) *Pool {
 	return &Pool{
 		clients: make(map[string]*clientImpl),
-		config:  config,
+		getConfiguration: func() (*bootstrap.Config, error) {
+			return config, nil
+		},
 	}
 }
 
@@ -154,12 +163,7 @@ func (p *Pool) GetClientForTesting(name string) (XDSClient, func(), error) {
 func (p *Pool) SetFallbackBootstrapConfig(config *bootstrap.Config) {
 	p.mu.Lock()
 	defer p.mu.Unlock()
-
-	if p.config != nil {
-		logger.Error("Attempt to set a bootstrap configuration even though one is already set via environment variables.")
-		return
-	}
-	p.config = config
+	p.fallbackConfig = config
 }
 
 // DumpResources returns the status and contents of all xDS resources.
@@ -191,7 +195,11 @@ func (p *Pool) DumpResources() *v3statuspb.ClientStatusResponse {
 func (p *Pool) BootstrapConfigForTesting() *bootstrap.Config {
 	p.mu.Lock()
 	defer p.mu.Unlock()
-	return p.config
+	cfg, _ := p.getConfiguration()
+	if cfg != nil {
+		return cfg
+	}
+	return p.fallbackConfig
 }
 
 // UnsetBootstrapConfigForTesting unsets the bootstrap configuration used by
@@ -201,7 +209,8 @@ func (p *Pool) BootstrapConfigForTesting() *bootstrap.Config {
 func (p *Pool) UnsetBootstrapConfigForTesting() {
 	p.mu.Lock()
 	defer p.mu.Unlock()
-	p.config = nil
+	p.fallbackConfig = nil
+	p.getConfiguration = sync.OnceValues(bootstrap.GetConfiguration)
 }
 
 func (p *Pool) clientRefCountedClose(name string) {
@@ -243,26 +252,19 @@ func (p *Pool) newRefCounted(name string, metricsRecorder estats.MetricsRecorder
 	p.mu.Lock()
 	defer p.mu.Unlock()
 
-	if p.config == nil {
-		if len(p.clients) != 0 || p != DefaultPool {
-			// If the current pool `p` already contains xDS clients or it is not
-			// the `DefaultPool`, the bootstrap config should have been already
-			// present in the pool.
-			return nil, nil, fmt.Errorf("xds: bootstrap configuration not set in the pool")
-		}
-		// If the current pool `p` is the `DefaultPool` and has no clients, it
-		// might be the first time an xDS client is being created on it. So,
-		// the bootstrap configuration is read from environment variables.
-		//
-		// DefaultPool is initialized with bootstrap configuration from one of the
-		// supported environment variables. 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, err := bootstrap.GetConfiguration()
-		if err != nil {
-			return nil, nil, fmt.Errorf("xds: failed to read xDS bootstrap config from env vars:  %v", err)
-		}
-		p.config = config
+	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 {
@@ -270,16 +272,16 @@ func (p *Pool) newRefCounted(name string, metricsRecorder estats.MetricsRecorder
 		return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil
 	}
 
-	c, err := newClientImpl(p.config, metricsRecorder, name)
+	c, err := newClientImpl(config, metricsRecorder, name)
 	if err != nil {
 		return nil, nil, err
 	}
 	if logger.V(2) {
-		c.logger.Infof("Created client with name %q and bootstrap configuration:\n %s", name, p.config)
+		c.logger.Infof("Created client with name %q and bootstrap configuration:\n %s", name, config)
 	}
 	p.clients[name] = c
 	xdsClientImplCreateHook(name)
 
-	logger.Infof("xDS node ID: %s", p.config.Node().GetId())
+	logger.Infof("xDS node ID: %s", config.Node().GetId())
 	return c, sync.OnceFunc(func() { p.clientRefCountedClose(name) }), nil
 }
diff --git a/xds/internal/xdsclient/xdsresource/resource_type.go b/xds/internal/xdsclient/xdsresource/resource_type.go
index 202bc8ef..4ecf63a1 100644
--- a/xds/internal/xdsclient/xdsresource/resource_type.go
+++ b/xds/internal/xdsclient/xdsresource/resource_type.go
@@ -184,14 +184,17 @@ type GenericResourceTypeDecoder struct {
 
 // Decode deserialize and validate resource bytes of an xDS resource received
 // from the xDS management server.
-func (gd *GenericResourceTypeDecoder) Decode(resourceBytes []byte, gOpts xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
-	raw := &anypb.Any{TypeUrl: gd.ResourceType.TypeURL(), Value: resourceBytes}
+func (gd *GenericResourceTypeDecoder) Decode(resource xdsclient.AnyProto, gOpts xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
+	rProto := &anypb.Any{
+		TypeUrl: resource.TypeURL,
+		Value:   resource.Value,
+	}
 	opts := &DecodeOptions{BootstrapConfig: gd.BootstrapConfig}
 	if gOpts.ServerConfig != nil {
 		opts.ServerConfig = gd.ServerConfigMap[*gOpts.ServerConfig]
 	}
 
-	result, err := gd.ResourceType.Decode(opts, raw)
+	result, err := gd.ResourceType.Decode(opts, rProto)
 	if result == nil {
 		return nil, err
 	}


Test patch:
diff --git a/internal/resolver/dns/dns_resolver_test.go b/internal/resolver/dns/dns_resolver_test.go
index 1541efb8..a1119b42 100644
--- a/internal/resolver/dns/dns_resolver_test.go
+++ b/internal/resolver/dns/dns_resolver_test.go
@@ -896,36 +896,68 @@ func (s) TestDisableServiceConfig(t *testing.T) {
 		target               string
 		hostLookupTable      map[string][]string
 		txtLookupTable       map[string][]string
+		txtLookupsEnabled    bool
 		disableServiceConfig bool
 		wantAddrs            []resolver.Address
 		wantSC               string
 	}{
 		{
-			name:            "false",
+			name:            "not disabled in BuildOptions; enabled globally",
 			target:          "foo.bar.com",
 			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
 			txtLookupTable: map[string][]string{
 				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
 			},
+			txtLookupsEnabled:    true,
 			disableServiceConfig: false,
 			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
 			wantSC:               scJSON,
 		},
 		{
-			name:            "true",
+			name:            "disabled in BuildOptions; enabled globally",
 			target:          "foo.bar.com",
 			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
 			txtLookupTable: map[string][]string{
 				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
 			},
+			txtLookupsEnabled:    true,
+			disableServiceConfig: true,
+			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
+			wantSC:               "{}",
+		},
+		{
+			name:            "not disabled in BuildOptions; disabled globally",
+			target:          "foo.bar.com",
+			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
+			txtLookupTable: map[string][]string{
+				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
+			},
+			txtLookupsEnabled:    false,
+			disableServiceConfig: false,
+			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
+			wantSC:               "{}",
+		},
+		{
+			name:            "disabled in BuildOptions; disabled globally",
+			target:          "foo.bar.com",
+			hostLookupTable: map[string][]string{"foo.bar.com": {"1.2.3.4", "5.6.7.8"}},
+			txtLookupTable: map[string][]string{
+				"_grpc_config.foo.bar.com": txtRecordServiceConfig(txtRecordGood),
+			},
+			txtLookupsEnabled:    false,
 			disableServiceConfig: true,
 			wantAddrs:            []resolver.Address{{Addr: "1.2.3.4" + colonDefaultPort}, {Addr: "5.6.7.8" + colonDefaultPort}},
 			wantSC:               "{}",
 		},
 	}
 
+	oldEnableTXTServiceConfig := envconfig.EnableTXTServiceConfig
+	defer func() {
+		envconfig.EnableTXTServiceConfig = oldEnableTXTServiceConfig
+	}()
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
+			envconfig.EnableTXTServiceConfig = test.txtLookupsEnabled
 			overrideTimeAfterFunc(t, 2*defaultTestTimeout)
 			overrideNetResolver(t, &testNetResolver{
 				hostLookupTable: test.hostLookupTable,
diff --git a/internal/xds/bootstrap/bootstrap_test.go b/internal/xds/bootstrap/bootstrap_test.go
index d0571978..88f27a4a 100644
--- a/internal/xds/bootstrap/bootstrap_test.go
+++ b/internal/xds/bootstrap/bootstrap_test.go
@@ -383,12 +383,19 @@ func (s) TestGetConfiguration_Failure(t *testing.T) {
 	cancel := setupBootstrapOverride(bootstrapFileMap)
 	defer cancel()
 
-	for _, name := range []string{"nonExistentBootstrapFile", "empty", "badJSON", "noBalancerName", "emptyXdsServer"} {
+	for _, name := range []string{"nonExistentBootstrapFile", "badJSON", "noBalancerName", "emptyXdsServer"} {
 		t.Run(name, func(t *testing.T) {
 			testGetConfigurationWithFileNameEnv(t, name, true, nil)
 			testGetConfigurationWithFileContentEnv(t, name, true, nil)
 		})
 	}
+	const name = "empty"
+	t.Run(name, func(t *testing.T) {
+		testGetConfigurationWithFileNameEnv(t, name, true, nil)
+		// If both the env vars are empty, a nil config with a nil error must be
+		// returned.
+		testGetConfigurationWithFileContentEnv(t, name, false, nil)
+	})
 }
 
 // Tests the functionality in GetConfiguration with different bootstrap file
@@ -462,9 +469,9 @@ func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) {
 	envconfig.XDSBootstrapFileContent = ""
 	defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
 
-	// When both env variables are empty, GetConfiguration should fail.
-	if _, err := GetConfiguration(); err == nil {
-		t.Errorf("GetConfiguration() returned nil error, expected to fail")
+	// When both env variables are empty, GetConfiguration should return nil.
+	if cfg, err := GetConfiguration(); err != nil || cfg != nil {
+		t.Errorf("GetConfiguration() returned (%v, %v), want (<nil>, <nil>)", cfg, err)
 	}
 
 	// When one of them is set, it should be used.
diff --git a/test/xds/xds_rls_clusterspecifier_plugin_test.go b/test/xds/xds_rls_clusterspecifier_plugin_test.go
index b01ddcb5..d2c55dc1 100644
--- a/test/xds/xds_rls_clusterspecifier_plugin_test.go
+++ b/test/xds/xds_rls_clusterspecifier_plugin_test.go
@@ -25,7 +25,6 @@ import (
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/stubserver"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/internal/testutils/rls"
@@ -100,9 +99,6 @@ func (s) TestRLSinxDS(t *testing.T) {
 }
 
 func testRLSinxDS(t *testing.T, lbPolicy e2e.LoadBalancingPolicy) {
-	internal.RegisterRLSClusterSpecifierPluginForTesting()
-	defer internal.UnregisterRLSClusterSpecifierPluginForTesting()
-
 	// Set up all components and configuration necessary - management server,
 	// xDS resolver, fake RLS Server, and xDS configuration which specifies an
 	// RLS Balancer that communicates to this set up fake RLS Server.
diff --git a/test/xds/xds_server_rbac_test.go b/test/xds/xds_server_rbac_test.go
index 4d535bbe..40d2b362 100644
--- a/test/xds/xds_server_rbac_test.go
+++ b/test/xds/xds_server_rbac_test.go
@@ -35,7 +35,6 @@ import (
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/connectivity"
 	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/testutils"
 	"google.golang.org/grpc/internal/testutils/xds/e2e"
 	"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
@@ -418,8 +417,6 @@ func serverListenerWithRBACHTTPFilters(t *testing.T, host string, port uint32, r
 // as normal and certain RPC's are denied by the RBAC HTTP Filter which gets
 // called by hooked xds interceptors.
 func (s) TestRBACHTTPFilter(t *testing.T) {
-	internal.RegisterRBACHTTPFilterForTesting()
-	defer internal.UnregisterRBACHTTPFilterForTesting()
 	tests := []struct {
 		name                string
 		rbacCfg             *rpb.RBAC
diff --git a/xds/internal/balancer/clustermanager/clustermanager_test.go b/xds/internal/balancer/clustermanager/clustermanager_test.go
index 91166c17..3afdc834 100644
--- a/xds/internal/balancer/clustermanager/clustermanager_test.go
+++ b/xds/internal/balancer/clustermanager/clustermanager_test.go
@@ -73,7 +73,7 @@ func testPick(t *testing.T, p balancer.Picker, info balancer.PickInfo, wantSC ba
 	}
 }
 
-func TestClusterPicks(t *testing.T) {
+func (s) TestClusterPicks(t *testing.T) {
 	cc := testutils.NewBalancerClientConn(t)
 	builder := balancer.Get(balancerName)
 	parser := builder.(balancer.ConfigParser)
@@ -154,7 +154,7 @@ func TestClusterPicks(t *testing.T) {
 
 // TestConfigUpdateAddCluster covers the cases the balancer receives config
 // update with extra clusters.
-func TestConfigUpdateAddCluster(t *testing.T) {
+func (s) TestConfigUpdateAddCluster(t *testing.T) {
 	cc := testutils.NewBalancerClientConn(t)
 	builder := balancer.Get(balancerName)
 	parser := builder.(balancer.ConfigParser)
@@ -314,7 +314,7 @@ func TestConfigUpdateAddCluster(t *testing.T) {
 
 // TestRoutingConfigUpdateDeleteAll covers the cases the balancer receives
 // config update with no clusters. Pick should fail with details in error.
-func TestRoutingConfigUpdateDeleteAll(t *testing.T) {
+func (s) TestRoutingConfigUpdateDeleteAll(t *testing.T) {
 	cc := testutils.NewBalancerClientConn(t)
 	builder := balancer.Get(balancerName)
 	parser := builder.(balancer.ConfigParser)
@@ -477,7 +477,7 @@ func TestRoutingConfigUpdateDeleteAll(t *testing.T) {
 	}
 }
 
-func TestClusterManagerForwardsBalancerBuildOptions(t *testing.T) {
+func (s) TestClusterManagerForwardsBalancerBuildOptions(t *testing.T) {
 	const (
 		userAgent          = "ua"
 		defaultTestTimeout = 1 * time.Second
@@ -561,7 +561,7 @@ func init() {
 
 // TestInitialIdle covers the case that if the child reports Idle, the overall
 // state will be Idle.
-func TestInitialIdle(t *testing.T) {
+func (s) TestInitialIdle(t *testing.T) {
 	cc := testutils.NewBalancerClientConn(t)
 	builder := balancer.Get(balancerName)
 	parser := builder.(balancer.ConfigParser)
@@ -608,7 +608,7 @@ func TestInitialIdle(t *testing.T) {
 // switches this child to a pick first load balancer. Once that balancer updates
 // it's state and completes the graceful switch process the new picker should
 // reflect this change.
-func TestClusterGracefulSwitch(t *testing.T) {
+func (s) TestClusterGracefulSwitch(t *testing.T) {
 	cc := testutils.NewBalancerClientConn(t)
 	builder := balancer.Get(balancerName)
 	parser := builder.(balancer.ConfigParser)
diff --git a/xds/internal/balancer/clustermanager/config_test.go b/xds/internal/balancer/clustermanager/config_test.go
index 23c25755..ef496a2e 100644
--- a/xds/internal/balancer/clustermanager/config_test.go
+++ b/xds/internal/balancer/clustermanager/config_test.go
@@ -96,7 +96,7 @@ var (
 	wtConfig2, _ = wtConfigParser.ParseConfig([]byte(wtConfigJSON2))
 )
 
-func Test_parseConfig(t *testing.T) {
+func (s) Test_parseConfig(t *testing.T) {
 	tests := []struct {
 		name    string
 		js      string
diff --git a/xds/internal/clients/xdsclient/helpers_test.go b/xds/internal/clients/xdsclient/helpers_test.go
index d84e0a4b..efa025ef 100644
--- a/xds/internal/clients/xdsclient/helpers_test.go
+++ b/xds/internal/clients/xdsclient/helpers_test.go
@@ -30,6 +30,7 @@ import (
 	"google.golang.org/grpc/xds/internal/clients/internal/pretty"
 	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/anypb"
 
 	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
@@ -62,9 +63,16 @@ var (
 	}
 )
 
-func unmarshalListenerResource(r []byte) (string, listenerUpdate, error) {
+func unmarshalListenerResource(rProto *anypb.Any) (string, listenerUpdate, error) {
+	rProto, err := xdsresource.UnwrapResource(rProto)
+	if err != nil {
+		return "", listenerUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
+	}
+	if !xdsresource.IsListenerResource(rProto.GetTypeUrl()) {
+		return "", listenerUpdate{}, fmt.Errorf("unexpected listener resource type: %q", rProto.GetTypeUrl())
+	}
 	lis := &v3listenerpb.Listener{}
-	if err := proto.Unmarshal(r, lis); err != nil {
+	if err := proto.Unmarshal(rProto.GetValue(), lis); err != nil {
 		return "", listenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
 	}
 
@@ -72,7 +80,7 @@ func unmarshalListenerResource(r []byte) (string, listenerUpdate, error) {
 	if err != nil {
 		return lis.GetName(), listenerUpdate{}, err
 	}
-	lu.Raw = r
+	lu.Raw = rProto.GetValue()
 	return lis.GetName(), *lu, nil
 }
 
@@ -154,8 +162,12 @@ type listenerDecoder struct{}
 
 // Decode deserializes and validates an xDS resource serialized inside the
 // provided `Any` proto, as received from the xDS management server.
-func (listenerDecoder) Decode(resource []byte, _ DecodeOptions) (*DecodeResult, error) {
-	name, listener, err := unmarshalListenerResource(resource)
+func (listenerDecoder) Decode(resource AnyProto, _ DecodeOptions) (*DecodeResult, error) {
+	rProto := &anypb.Any{
+		TypeUrl: resource.TypeURL,
+		Value:   resource.Value,
+	}
+	name, listener, err := unmarshalListenerResource(rProto)
 	switch {
 	case name == "":
 		// Name is unset only when protobuf deserialization fails.
diff --git a/xds/internal/clients/xdsclient/test/helpers_test.go b/xds/internal/clients/xdsclient/test/helpers_test.go
index 600aa8af..6e7086fb 100644
--- a/xds/internal/clients/xdsclient/test/helpers_test.go
+++ b/xds/internal/clients/xdsclient/test/helpers_test.go
@@ -34,6 +34,7 @@ import (
 	"google.golang.org/grpc/xds/internal/clients/xdsclient"
 	"google.golang.org/grpc/xds/internal/clients/xdsclient/internal/xdsresource"
 	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/anypb"
 
 	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
@@ -73,9 +74,16 @@ var (
 	}
 )
 
-func unmarshalListenerResource(r []byte) (string, listenerUpdate, error) {
+func unmarshalListenerResource(rProto *anypb.Any) (string, listenerUpdate, error) {
+	rProto, err := xdsresource.UnwrapResource(rProto)
+	if err != nil {
+		return "", listenerUpdate{}, fmt.Errorf("failed to unwrap resource: %v", err)
+	}
+	if !xdsresource.IsListenerResource(rProto.GetTypeUrl()) {
+		return "", listenerUpdate{}, fmt.Errorf("unexpected listener resource type: %q", rProto.GetTypeUrl())
+	}
 	lis := &v3listenerpb.Listener{}
-	if err := proto.Unmarshal(r, lis); err != nil {
+	if err := proto.Unmarshal(rProto.GetValue(), lis); err != nil {
 		return "", listenerUpdate{}, fmt.Errorf("failed to unmarshal resource: %v", err)
 	}
 
@@ -83,7 +91,7 @@ func unmarshalListenerResource(r []byte) (string, listenerUpdate, error) {
 	if err != nil {
 		return lis.GetName(), listenerUpdate{}, err
 	}
-	lu.Raw = r
+	lu.Raw = rProto.GetValue()
 	return lis.GetName(), *lu, nil
 }
 
@@ -165,8 +173,12 @@ type listenerDecoder struct{}
 
 // Decode deserializes and validates an xDS resource serialized inside the
 // provided `Any` proto, as received from the xDS management server.
-func (listenerDecoder) Decode(resource []byte, _ xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
-	name, listener, err := unmarshalListenerResource(resource)
+func (listenerDecoder) Decode(resource xdsclient.AnyProto, _ xdsclient.DecodeOptions) (*xdsclient.DecodeResult, error) {
+	rProto := &anypb.Any{
+		TypeUrl: resource.TypeURL,
+		Value:   resource.Value,
+	}
+	name, listener, err := unmarshalListenerResource(rProto)
 	switch {
 	case name == "":
 		// Name is unset only when protobuf deserialization fails.
diff --git a/xds/internal/xdsclient/pool/pool_test.go b/xds/internal/xdsclient/pool/pool_test.go
index b12cc737..8b8cfe4b 100644
--- a/xds/internal/xdsclient/pool/pool_test.go
+++ b/xds/internal/xdsclient/pool/pool_test.go
@@ -73,6 +73,10 @@ func (s) TestDefaultPool_LazyLoadBootstrapConfig(t *testing.T) {
 		t.Fatalf("DefaultPool.BootstrapConfigForTesting() = %v, want nil", cfg)
 	}
 
+	// The pool will attempt to read the env vars only once. Reset the pool's
+	// state to make it re-read the env vars during next client creation.
+	xdsclient.DefaultPool.UnsetBootstrapConfigForTesting()
+
 	_, closeFunc, err = xdsclient.DefaultPool.NewClient(t.Name(), &stats.NoopMetricsRecorder{})
 	if err != nil {
 		t.Fatalf("Failed to create xDS client: %v", err)