Skip to content

Commit e54f441

Browse files
authored
xds: make fallback bootstrap configuration per-process (#7401)
1 parent 9c5b31d commit e54f441

24 files changed

+318
-400
lines changed

internal/testutils/xds/e2e/bootstrap.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func DefaultBootstrapContents(t *testing.T, nodeID, serverURI string) []byte {
7676
"server_uri": "passthrough:///%s",
7777
"channel_creds": [{"type": "insecure"}]
7878
}`, serverURI))},
79-
NodeID: nodeID,
79+
Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
8080
CertificateProviders: cpc,
8181
ServerListenerResourceNameTemplate: ServerListenerResourceNameTemplate,
8282
})

internal/xds/bootstrap/bootstrap.go

Lines changed: 86 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"os"
3030
"slices"
3131
"strings"
32+
"sync"
3233

3334
"google.golang.org/grpc"
3435
"google.golang.org/grpc/credentials/tls/certprovider"
@@ -509,54 +510,48 @@ func (c *Config) UnmarshalJSON(data []byte) error {
509510
return nil
510511
}
511512

512-
// Returns the bootstrap configuration from env vars ${GRPC_XDS_BOOTSTRAP} or
513-
// ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the former is
514-
// preferred. And if none of the env vars are set, an error is returned.
515-
func bootstrapConfigFromEnvVariable() ([]byte, error) {
513+
// GetConfiguration returns the bootstrap configuration initialized by reading
514+
// the bootstrap file found at ${GRPC_XDS_BOOTSTRAP} or bootstrap contents
515+
// specified at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the
516+
// former is preferred.
517+
//
518+
// If none of the env vars are set, this function returns the fallback
519+
// configuration if it is not nil. Else, it returns an error.
520+
//
521+
// This function tries to process as much of the bootstrap file as possible (in
522+
// the presence of the errors) and may return a Config object with certain
523+
// fields left unspecified, in which case the caller should use some sane
524+
// defaults.
525+
func GetConfiguration() (*Config, error) {
516526
fName := envconfig.XDSBootstrapFileName
517527
fContent := envconfig.XDSBootstrapFileContent
518528

519529
if fName != "" {
520530
if logger.V(2) {
521-
logger.Infof("Using bootstrap file with name %q", fName)
531+
logger.Infof("Using bootstrap file with name %q from GRPC_XDS_BOOTSTRAP environment variable", fName)
522532
}
523-
return bootstrapFileReadFunc(fName)
533+
cfg, err := bootstrapFileReadFunc(fName)
534+
if err != nil {
535+
return nil, fmt.Errorf("xds: failed to read bootstrap config from file %q: %v", fName, err)
536+
}
537+
return newConfigFromContents(cfg)
524538
}
525539

526540
if fContent != "" {
527-
return []byte(fContent), nil
541+
if logger.V(2) {
542+
logger.Infof("Using bootstrap contents from GRPC_XDS_BOOTSTRAP_CONFIG environment variable")
543+
}
544+
return newConfigFromContents([]byte(fContent))
528545
}
529546

530-
return nil, fmt.Errorf("none of the bootstrap environment variables (%q or %q) defined", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
531-
}
532-
533-
// NewConfig returns a new instance of Config initialized by reading the
534-
// bootstrap file found at ${GRPC_XDS_BOOTSTRAP} or bootstrap contents specified
535-
// at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the former is
536-
// preferred.
537-
//
538-
// We support a credential registration mechanism and only credentials
539-
// registered through that mechanism will be accepted here. See package
540-
// `xds/bootstrap` for details.
541-
//
542-
// This function tries to process as much of the bootstrap file as possible (in
543-
// the presence of the errors) and may return a Config object with certain
544-
// fields left unspecified, in which case the caller should use some sane
545-
// defaults.
546-
func NewConfig() (*Config, error) {
547-
// Examples of the bootstrap json can be found in the generator tests
548-
// https://github.com/GoogleCloudPlatform/traffic-director-grpc-bootstrap/blob/master/main_test.go.
549-
data, err := bootstrapConfigFromEnvVariable()
550-
if err != nil {
551-
return nil, fmt.Errorf("xds: Failed to read bootstrap config: %v", err)
547+
if cfg := fallbackBootstrapConfig(); cfg != nil {
548+
if logger.V(2) {
549+
logger.Infof("Using bootstrap contents from fallback config")
550+
}
551+
return cfg, nil
552552
}
553-
return newConfigFromContents(data)
554-
}
555553

556-
// NewConfigFromContents returns a new Config using the specified
557-
// bootstrap file contents instead of reading the environment variable.
558-
func NewConfigFromContents(data []byte) (*Config, error) {
559-
return newConfigFromContents(data)
554+
return nil, fmt.Errorf("bootstrap environment variables (%q or %q) not defined, and no fallback config set", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv)
560555
}
561556

562557
func newConfigFromContents(data []byte) (*Config, error) {
@@ -596,9 +591,9 @@ type ConfigOptionsForTesting struct {
596591
ClientDefaultListenerResourceNameTemplate string
597592
// Authorities is a list of non-default authorities.
598593
Authorities map[string]json.RawMessage
599-
// NodeID is the node identifier of the gRPC client/server node in the
594+
// Node identifies the gRPC client/server node in the
600595
// proxyless service mesh.
601-
NodeID string
596+
Node json.RawMessage
602597
}
603598

604599
// NewContentsForTesting creates a new bootstrap configuration from the passed in
@@ -630,13 +625,17 @@ func NewContentsForTesting(opts ConfigOptionsForTesting) ([]byte, error) {
630625
}
631626
authorities[k] = a
632627
}
628+
node := newNode()
629+
if err := json.Unmarshal(opts.Node, &node); err != nil {
630+
return nil, fmt.Errorf("failed to unmarshal node configuration %s: %v", string(opts.Node), err)
631+
}
633632
cfgJSON := configJSON{
634633
XDSServers: servers,
635634
CertificateProviders: certProviders,
636635
ServerListenerResourceNameTemplate: opts.ServerListenerResourceNameTemplate,
637636
ClientDefaultListenerResourceNameTemplate: opts.ClientDefaultListenerResourceNameTemplate,
638637
Authorities: authorities,
639-
Node: node{ID: opts.NodeID},
638+
Node: node,
640639
}
641640
contents, err := json.MarshalIndent(cfgJSON, " ", " ")
642641
if err != nil {
@@ -645,6 +644,14 @@ func NewContentsForTesting(opts ConfigOptionsForTesting) ([]byte, error) {
645644
return contents, nil
646645
}
647646

647+
// NewConfigForTesting creates a new bootstrap configuration from the provided
648+
// contents, for testing purposes.
649+
//
650+
// # Testing-Only
651+
func NewConfigForTesting(contents []byte) (*Config, error) {
652+
return newConfigFromContents(contents)
653+
}
654+
648655
// certproviderNameAndConfig is the internal representation of
649656
// the`certificate_providers` field in the bootstrap configuration.
650657
type certproviderNameAndConfig struct {
@@ -747,3 +754,44 @@ func (n node) toProto() *v3corepb.Node {
747754
ClientFeatures: slices.Clone(n.clientFeatures),
748755
}
749756
}
757+
758+
// SetFallbackBootstrapConfig sets the fallback bootstrap configuration to be
759+
// used when the bootstrap environment variables are unset.
760+
//
761+
// The provided configuration must be valid JSON. Returns a non-nil error if
762+
// parsing the provided configuration fails.
763+
func SetFallbackBootstrapConfig(cfgJSON []byte) error {
764+
config, err := newConfigFromContents(cfgJSON)
765+
if err != nil {
766+
return err
767+
}
768+
769+
configMu.Lock()
770+
defer configMu.Unlock()
771+
fallbackBootstrapCfg = config
772+
return nil
773+
}
774+
775+
// UnsetFallbackBootstrapConfigForTesting unsets the fallback bootstrap
776+
// configuration to be used when the bootstrap environment variables are unset.
777+
//
778+
// # Testing-Only
779+
func UnsetFallbackBootstrapConfigForTesting() {
780+
configMu.Lock()
781+
defer configMu.Unlock()
782+
fallbackBootstrapCfg = nil
783+
}
784+
785+
// fallbackBootstrapConfig returns the fallback bootstrap configuration
786+
// that will be used by the xDS client when the bootstrap environment
787+
// variables are unset.
788+
func fallbackBootstrapConfig() *Config {
789+
configMu.Lock()
790+
defer configMu.Unlock()
791+
return fallbackBootstrapCfg
792+
}
793+
794+
var (
795+
configMu sync.Mutex
796+
fallbackBootstrapCfg *Config
797+
)

internal/xds/bootstrap/bootstrap_test.go

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -293,19 +293,16 @@ func setupBootstrapOverride(bootstrapFileMap map[string]string) func() {
293293
return func() { bootstrapFileReadFunc = oldFileReadFunc }
294294
}
295295

296-
// TODO: enable leak check for this package when
297-
// https://github.com/googleapis/google-cloud-go/issues/2417 is fixed.
298-
299296
// This function overrides the bootstrap file NAME env variable, to test the
300297
// code that reads file with the given fileName.
301-
func testNewConfigWithFileNameEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
298+
func testGetConfigurationWithFileNameEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
302299
origBootstrapFileName := envconfig.XDSBootstrapFileName
303300
envconfig.XDSBootstrapFileName = fileName
304301
defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }()
305302

306-
c, err := NewConfig()
303+
c, err := GetConfiguration()
307304
if (err != nil) != wantError {
308-
t.Fatalf("NewConfig() returned error %v, wantError: %v", err, wantError)
305+
t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError)
309306
}
310307
if wantError {
311308
return
@@ -317,7 +314,7 @@ func testNewConfigWithFileNameEnv(t *testing.T, fileName string, wantError bool,
317314

318315
// This function overrides the bootstrap file CONTENT env variable, to test the
319316
// code that uses the content from env directly.
320-
func testNewConfigWithFileContentEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
317+
func testGetConfigurationWithFileContentEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) {
321318
t.Helper()
322319
b, err := bootstrapFileReadFunc(fileName)
323320
if err != nil {
@@ -327,9 +324,9 @@ func testNewConfigWithFileContentEnv(t *testing.T, fileName string, wantError bo
327324
envconfig.XDSBootstrapFileContent = string(b)
328325
defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
329326

330-
c, err := NewConfig()
327+
c, err := GetConfiguration()
331328
if (err != nil) != wantError {
332-
t.Fatalf("NewConfig() returned error %v, wantError: %v", err, wantError)
329+
t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError)
333330
}
334331
if wantError {
335332
return
@@ -339,8 +336,9 @@ func testNewConfigWithFileContentEnv(t *testing.T, fileName string, wantError bo
339336
}
340337
}
341338

342-
// Tests NewConfig with bootstrap file contents that are expected to fail.
343-
func (s) TestNewConfig_Failure(t *testing.T) {
339+
// Tests GetConfiguration with bootstrap file contents that are expected to
340+
// fail.
341+
func (s) TestGetConfiguration_Failure(t *testing.T) {
344342
bootstrapFileMap := map[string]string{
345343
"empty": "",
346344
"badJSON": `["test": 123]`,
@@ -387,16 +385,16 @@ func (s) TestNewConfig_Failure(t *testing.T) {
387385

388386
for _, name := range []string{"nonExistentBootstrapFile", "empty", "badJSON", "noBalancerName", "emptyXdsServer"} {
389387
t.Run(name, func(t *testing.T) {
390-
testNewConfigWithFileNameEnv(t, name, true, nil)
391-
testNewConfigWithFileContentEnv(t, name, true, nil)
388+
testGetConfigurationWithFileNameEnv(t, name, true, nil)
389+
testGetConfigurationWithFileContentEnv(t, name, true, nil)
392390
})
393391
}
394392
}
395393

396-
// TestNewConfigV3ProtoSuccess exercises the functionality in NewConfig with
397-
// different bootstrap file contents. It overrides the fileReadFunc by returning
398-
// bootstrap file contents defined in this test, instead of reading from a file.
399-
func (s) TestNewConfig_Success(t *testing.T) {
394+
// Tests the functionality in GetConfiguration with different bootstrap file
395+
// contents. It overrides the fileReadFunc by returning bootstrap file contents
396+
// defined in this test, instead of reading from a file.
397+
func (s) TestGetConfiguration_Success(t *testing.T) {
400398
cancel := setupBootstrapOverride(v3BootstrapFileMap)
401399
defer cancel()
402400

@@ -431,19 +429,18 @@ func (s) TestNewConfig_Success(t *testing.T) {
431429

432430
for _, test := range tests {
433431
t.Run(test.name, func(t *testing.T) {
434-
testNewConfigWithFileNameEnv(t, test.name, false, test.wantConfig)
435-
testNewConfigWithFileContentEnv(t, test.name, false, test.wantConfig)
432+
testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig)
433+
testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig)
436434
})
437435
}
438436
}
439437

440-
// TestNewConfigBootstrapEnvPriority tests that the two env variables are read
441-
// in correct priority.
438+
// Tests that the two bootstrap env variables are read in correct priority.
442439
//
443440
// "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap
444441
// configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which
445442
// directly specifies the bootstrap configuration in itself.
446-
func (s) TestNewConfigBootstrapEnvPriority(t *testing.T) {
443+
func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) {
447444
oldFileReadFunc := bootstrapFileReadFunc
448445
bootstrapFileReadFunc = func(filename string) ([]byte, error) {
449446
return fileReadFromFileMap(v3BootstrapFileMap, filename)
@@ -465,27 +462,27 @@ func (s) TestNewConfigBootstrapEnvPriority(t *testing.T) {
465462
envconfig.XDSBootstrapFileContent = ""
466463
defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }()
467464

468-
// When both env variables are empty, NewConfig should fail.
469-
if _, err := NewConfig(); err == nil {
470-
t.Errorf("NewConfig() returned nil error, expected to fail")
465+
// When both env variables are empty, GetConfiguration should fail.
466+
if _, err := GetConfiguration(); err == nil {
467+
t.Errorf("GetConfiguration() returned nil error, expected to fail")
471468
}
472469

473470
// When one of them is set, it should be used.
474471
envconfig.XDSBootstrapFileName = goodFileName1
475472
envconfig.XDSBootstrapFileContent = ""
476-
c, err := NewConfig()
473+
c, err := GetConfiguration()
477474
if err != nil {
478-
t.Errorf("NewConfig() failed: %v", err)
475+
t.Errorf("GetConfiguration() failed: %v", err)
479476
}
480477
if diff := cmp.Diff(goodConfig1, c); diff != "" {
481478
t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
482479
}
483480

484481
envconfig.XDSBootstrapFileName = ""
485482
envconfig.XDSBootstrapFileContent = goodFileContent2
486-
c, err = NewConfig()
483+
c, err = GetConfiguration()
487484
if err != nil {
488-
t.Errorf("NewConfig() failed: %v", err)
485+
t.Errorf("GetConfiguration() failed: %v", err)
489486
}
490487
if diff := cmp.Diff(goodConfig2, c); diff != "" {
491488
t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
@@ -494,9 +491,9 @@ func (s) TestNewConfigBootstrapEnvPriority(t *testing.T) {
494491
// Set both, file name should be read.
495492
envconfig.XDSBootstrapFileName = goodFileName1
496493
envconfig.XDSBootstrapFileContent = goodFileContent2
497-
c, err = NewConfig()
494+
c, err = GetConfiguration()
498495
if err != nil {
499-
t.Errorf("NewConfig() failed: %v", err)
496+
t.Errorf("GetConfiguration() failed: %v", err)
500497
}
501498
if diff := cmp.Diff(goodConfig1, c); diff != "" {
502499
t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff)
@@ -554,7 +551,7 @@ type fakeCertProvider struct {
554551
certprovider.Provider
555552
}
556553

557-
func (s) TestNewConfigWithCertificateProviders(t *testing.T) {
554+
func (s) TestGetConfiguration_CertificateProviders(t *testing.T) {
558555
bootstrapFileMap := map[string]string{
559556
"badJSONCertProviderConfig": `
560557
{
@@ -706,13 +703,13 @@ func (s) TestNewConfigWithCertificateProviders(t *testing.T) {
706703

707704
for _, test := range tests {
708705
t.Run(test.name, func(t *testing.T) {
709-
testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
710-
testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
706+
testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
707+
testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
711708
})
712709
}
713710
}
714711

715-
func (s) TestNewConfigWithServerListenerResourceNameTemplate(t *testing.T) {
712+
func (s) TestGetConfiguration_ServerListenerResourceNameTemplate(t *testing.T) {
716713
cancel := setupBootstrapOverride(map[string]string{
717714
"badServerListenerResourceNameTemplate:": `
718715
{
@@ -775,13 +772,13 @@ func (s) TestNewConfigWithServerListenerResourceNameTemplate(t *testing.T) {
775772

776773
for _, test := range tests {
777774
t.Run(test.name, func(t *testing.T) {
778-
testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
779-
testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
775+
testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
776+
testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
780777
})
781778
}
782779
}
783780

784-
func (s) TestNewConfigWithFederation(t *testing.T) {
781+
func (s) TestGetConfiguration_Federation(t *testing.T) {
785782
cancel := setupBootstrapOverride(map[string]string{
786783
"badclientListenerResourceNameTemplate": `
787784
{
@@ -984,8 +981,8 @@ func (s) TestNewConfigWithFederation(t *testing.T) {
984981

985982
for _, test := range tests {
986983
t.Run(test.name, func(t *testing.T) {
987-
testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
988-
testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
984+
testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig)
985+
testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig)
989986
})
990987
}
991988
}

0 commit comments

Comments
 (0)