diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go index d90648713944..00abc7c2beb0 100644 --- a/internal/envconfig/envconfig.go +++ b/internal/envconfig/envconfig.go @@ -46,6 +46,10 @@ var ( // by setting the environment variable "GRPC_ENFORCE_ALPN_ENABLED" to "true" // or "false". EnforceALPNEnabled = boolFromEnv("GRPC_ENFORCE_ALPN_ENABLED", false) + // 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", false) ) func boolFromEnv(envVar string, def bool) bool { diff --git a/internal/xds/bootstrap/bootstrap.go b/internal/xds/bootstrap/bootstrap.go index af44da183e5e..1ec0a82c0217 100644 --- a/internal/xds/bootstrap/bootstrap.go +++ b/internal/xds/bootstrap/bootstrap.go @@ -84,6 +84,39 @@ func (cc ChannelCreds) String() string { return cc.Type + "-" + string(b) } +// ServerConfigs represents a collection of server configurations. +type ServerConfigs []*ServerConfig + +// Equal returns true if scs equals other. +func (scs *ServerConfigs) Equal(other *ServerConfigs) bool { + if len(*scs) != len(*other) { + return false + } + for i := range *scs { + if !(*scs)[i].Equal((*other)[i]) { + return false + } + } + return true +} + +// UnmarshalJSON takes the json data (a list of server configurations) and +// unmarshals it to the struct. +func (scs *ServerConfigs) UnmarshalJSON(data []byte) error { + servers := []*ServerConfig{} + if err := json.Unmarshal(data, &servers); err != nil { + return fmt.Errorf("xds: failed to JSON unmarshal server configurations during bootstrap: %v, config:\n%s", err, string(data)) + } + // Only use the first server config if fallback support is disabled. + if !envconfig.XDSFallbackSupport { + if len(servers) > 1 { + servers = servers[:1] + } + } + *scs = servers + return nil +} + // Authority contains configuration for an xDS control plane authority. // // This type does not implement custom JSON marshal/unmarshal logic because it @@ -104,7 +137,7 @@ type Authority struct { // "xdstp:///envoy.config.listener.v3.Listener/%s". ClientListenerResourceNameTemplate string `json:"client_listener_resource_name_template,omitempty"` // XDSServers contains the list of server configurations for this authority. - XDSServers []*ServerConfig `json:"xds_servers,omitempty"` + XDSServers ServerConfigs `json:"xds_servers,omitempty"` } // Equal returns true if a equals other. @@ -116,7 +149,7 @@ func (a *Authority) Equal(other *Authority) bool { return false case a.ClientListenerResourceNameTemplate != other.ClientListenerResourceNameTemplate: return false - case !slices.EqualFunc(a.XDSServers, other.XDSServers, func(a, b *ServerConfig) bool { return a.Equal(b) }): + case !a.XDSServers.Equal(&other.XDSServers): return false } return true @@ -315,7 +348,7 @@ func ServerConfigForTesting(opts ServerConfigTestingOptions) (*ServerConfig, err // Config is the internal representation of the bootstrap configuration provided // to the xDS client. type Config struct { - xDSServers []*ServerConfig + xDSServers ServerConfigs cpcs map[string]certproviderNameAndConfig serverListenerResourceNameTemplate string clientDefaultListenerResourceNameTemplate string @@ -405,7 +438,7 @@ func (c *Config) Equal(other *Config) bool { return true case (c != nil) != (other != nil): return false - case !slices.EqualFunc(c.xDSServers, other.xDSServers, func(a, b *ServerConfig) bool { return a.Equal(b) }): + case !c.xDSServers.Equal(&other.xDSServers): return false case !maps.EqualFunc(c.certProviderConfigs, other.certProviderConfigs, func(a, b *certprovider.BuildableConfig) bool { return a.String() == b.String() }): return false @@ -429,7 +462,7 @@ func (c *Config) String() string { // The following fields correspond 1:1 with the JSON schema for Config. type configJSON struct { - XDSServers []*ServerConfig `json:"xds_servers,omitempty"` + XDSServers ServerConfigs `json:"xds_servers,omitempty"` CertificateProviders map[string]certproviderNameAndConfig `json:"certificate_providers,omitempty"` ServerListenerResourceNameTemplate string `json:"server_listener_resource_name_template,omitempty"` ClientDefaultListenerResourceNameTemplate string `json:"client_default_listener_resource_name_template,omitempty"` diff --git a/internal/xds/bootstrap/bootstrap_test.go b/internal/xds/bootstrap/bootstrap_test.go index f7fe89335653..302e281405ee 100644 --- a/internal/xds/bootstrap/bootstrap_test.go +++ b/internal/xds/bootstrap/bootstrap_test.go @@ -429,6 +429,10 @@ func (s) TestGetConfiguration_Success(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { + origFallbackEnv := envconfig.XDSFallbackSupport + envconfig.XDSFallbackSupport = true + defer func() { envconfig.XDSFallbackSupport = origFallbackEnv }() + testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig) testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig) }) @@ -1194,3 +1198,78 @@ func (s) TestNode_ToProto(t *testing.T) { }) } } + +// Tests the case where the xDS fallback env var is set to false, and verifies +// that only the first server from the list of server configurations is used. +func (s) TestGetConfiguration_FallbackDisabled(t *testing.T) { + // TODO(easwars): Default value of "GRPC_EXPERIMENTAL_XDS_FALLBACK" + // env var is currently false. When the default is changed to true, + // explicitly set it to false here. + + cancel := setupBootstrapOverride(map[string]string{ + "multipleXDSServers": ` + { + "node": { + "id": "ENVOY_NODE_ID", + "metadata": { + "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" + } + }, + "xds_servers" : [ + { + "server_uri": "trafficdirector.googleapis.com:443", + "channel_creds": [{ "type": "google_default" }], + "server_features": ["xds_v3"] + }, + { + "server_uri": "backup.never.use.com:1234", + "channel_creds": [{ "type": "google_default" }] + } + ], + "authorities": { + "xds.td.com": { + "xds_servers": [ + { + "server_uri": "td.com", + "channel_creds": [ { "type": "google_default" } ], + "server_features" : ["xds_v3"] + }, + { + "server_uri": "backup.never.use.com:1234", + "channel_creds": [{ "type": "google_default" }] + } + ] + } + } + }`, + }) + defer cancel() + + wantConfig := &Config{ + xDSServers: []*ServerConfig{{ + serverURI: "trafficdirector.googleapis.com:443", + channelCreds: []ChannelCreds{{Type: "google_default"}}, + serverFeatures: []string{"xds_v3"}, + selectedCreds: ChannelCreds{Type: "google_default"}, + }}, + node: v3Node, + clientDefaultListenerResourceNameTemplate: "%s", + authorities: map[string]*Authority{ + "xds.td.com": { + ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", + XDSServers: []*ServerConfig{{ + serverURI: "td.com", + channelCreds: []ChannelCreds{{Type: "google_default"}}, + serverFeatures: []string{"xds_v3"}, + selectedCreds: ChannelCreds{Type: "google_default"}, + }}, + }, + }, + } + t.Run("bootstrap_file_name", func(t *testing.T) { + testGetConfigurationWithFileNameEnv(t, "multipleXDSServers", false, wantConfig) + }) + t.Run("bootstrap_file_contents", func(t *testing.T) { + testGetConfigurationWithFileContentEnv(t, "multipleXDSServers", false, wantConfig) + }) +}