google.golang.org/grpc@v1.72.2/internal/xds/bootstrap/bootstrap.go (about) 1 /* 2 * 3 * Copyright 2019 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19 // Package bootstrap provides the functionality to initialize certain aspects 20 // of an xDS client by reading a bootstrap file. 21 package bootstrap 22 23 import ( 24 "bytes" 25 "encoding/json" 26 "fmt" 27 "maps" 28 "net/url" 29 "os" 30 "slices" 31 "strings" 32 33 "google.golang.org/grpc" 34 "google.golang.org/grpc/credentials/tls/certprovider" 35 "google.golang.org/grpc/internal" 36 "google.golang.org/grpc/internal/envconfig" 37 "google.golang.org/grpc/xds/bootstrap" 38 "google.golang.org/protobuf/proto" 39 "google.golang.org/protobuf/types/known/structpb" 40 41 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 42 ) 43 44 const ( 45 serverFeaturesIgnoreResourceDeletion = "ignore_resource_deletion" 46 gRPCUserAgentName = "gRPC Go" 47 clientFeatureNoOverprovisioning = "envoy.lb.does_not_support_overprovisioning" 48 clientFeatureResourceWrapper = "xds.config.resource-in-sotw" 49 ) 50 51 // For overriding in unit tests. 52 var bootstrapFileReadFunc = os.ReadFile 53 54 // ChannelCreds contains the credentials to be used while communicating with an 55 // xDS server. It is also used to dedup servers with the same server URI. 56 // 57 // This type does not implement custom JSON marshal/unmarshal logic because it 58 // is straightforward to accomplish the same with json struct tags. 59 type ChannelCreds struct { 60 // Type contains a unique name identifying the credentials type. The only 61 // supported types currently are "google_default" and "insecure". 62 Type string `json:"type,omitempty"` 63 // Config contains the JSON configuration associated with the credentials. 64 Config json.RawMessage `json:"config,omitempty"` 65 } 66 67 // Equal reports whether cc and other are considered equal. 68 func (cc ChannelCreds) Equal(other ChannelCreds) bool { 69 return cc.Type == other.Type && bytes.Equal(cc.Config, other.Config) 70 } 71 72 // String returns a string representation of the credentials. It contains the 73 // type and the config (if non-nil) separated by a "-". 74 func (cc ChannelCreds) String() string { 75 if cc.Config == nil { 76 return cc.Type 77 } 78 79 // We do not expect the Marshal call to fail since we wrote to cc.Config 80 // after a successful unmarshalling from JSON configuration. Therefore, 81 // it is safe to ignore the error here. 82 b, _ := json.Marshal(cc.Config) 83 return cc.Type + "-" + string(b) 84 } 85 86 // ServerConfigs represents a collection of server configurations. 87 type ServerConfigs []*ServerConfig 88 89 // Equal returns true if scs equals other. 90 func (scs *ServerConfigs) Equal(other *ServerConfigs) bool { 91 if len(*scs) != len(*other) { 92 return false 93 } 94 for i := range *scs { 95 if !(*scs)[i].Equal((*other)[i]) { 96 return false 97 } 98 } 99 return true 100 } 101 102 // UnmarshalJSON takes the json data (a list of server configurations) and 103 // unmarshals it to the struct. 104 func (scs *ServerConfigs) UnmarshalJSON(data []byte) error { 105 servers := []*ServerConfig{} 106 if err := json.Unmarshal(data, &servers); err != nil { 107 return fmt.Errorf("xds: failed to JSON unmarshal server configurations during bootstrap: %v, config:\n%s", err, string(data)) 108 } 109 // Only use the first server config if fallback support is disabled. 110 if !envconfig.XDSFallbackSupport { 111 if len(servers) > 1 { 112 servers = servers[:1] 113 } 114 } 115 *scs = servers 116 return nil 117 } 118 119 // String returns a string representation of the ServerConfigs, by concatenating 120 // the string representations of the underlying server configs. 121 func (scs *ServerConfigs) String() string { 122 ret := "" 123 for i, sc := range *scs { 124 if i > 0 { 125 ret += ", " 126 } 127 ret += sc.String() 128 } 129 return ret 130 } 131 132 // Authority contains configuration for an xDS control plane authority. 133 // 134 // This type does not implement custom JSON marshal/unmarshal logic because it 135 // is straightforward to accomplish the same with json struct tags. 136 type Authority struct { 137 // ClientListenerResourceNameTemplate is template for the name of the 138 // Listener resource to subscribe to for a gRPC client channel. Used only 139 // when the channel is created using an "xds:" URI with this authority name. 140 // 141 // The token "%s", if present in this string, will be replaced 142 // with %-encoded service authority (i.e., the path part of the target 143 // URI used to create the gRPC channel). 144 // 145 // Must start with "xdstp://<authority_name>/". If it does not, 146 // that is considered a bootstrap file parsing error. 147 // 148 // If not present in the bootstrap file, defaults to 149 // "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s". 150 ClientListenerResourceNameTemplate string `json:"client_listener_resource_name_template,omitempty"` 151 // XDSServers contains the list of server configurations for this authority. 152 XDSServers ServerConfigs `json:"xds_servers,omitempty"` 153 } 154 155 // Equal returns true if a equals other. 156 func (a *Authority) Equal(other *Authority) bool { 157 switch { 158 case a == nil && other == nil: 159 return true 160 case (a != nil) != (other != nil): 161 return false 162 case a.ClientListenerResourceNameTemplate != other.ClientListenerResourceNameTemplate: 163 return false 164 case !a.XDSServers.Equal(&other.XDSServers): 165 return false 166 } 167 return true 168 } 169 170 // ServerConfig contains the configuration to connect to a server. 171 type ServerConfig struct { 172 serverURI string 173 channelCreds []ChannelCreds 174 serverFeatures []string 175 176 // As part of unmarshalling the JSON config into this struct, we ensure that 177 // the credentials config is valid by building an instance of the specified 178 // credentials and store it here for easy access. 179 selectedCreds ChannelCreds 180 credsDialOption grpc.DialOption 181 extraDialOptions []grpc.DialOption 182 183 cleanups []func() 184 } 185 186 // ServerURI returns the URI of the management server to connect to. 187 func (sc *ServerConfig) ServerURI() string { 188 return sc.serverURI 189 } 190 191 // ChannelCreds returns the credentials configuration to use when communicating 192 // with this server. Also used to dedup servers with the same server URI. 193 func (sc *ServerConfig) ChannelCreds() []ChannelCreds { 194 return sc.channelCreds 195 } 196 197 // ServerFeatures returns the list of features supported by this server. Also 198 // used to dedup servers with the same server URI and channel creds. 199 func (sc *ServerConfig) ServerFeatures() []string { 200 return sc.serverFeatures 201 } 202 203 // ServerFeaturesIgnoreResourceDeletion returns true if this server supports a 204 // feature where the xDS client can ignore resource deletions from this server, 205 // as described in gRFC A53. 206 // 207 // This feature controls the behavior of the xDS client when the server deletes 208 // a previously sent Listener or Cluster resource. If set, the xDS client will 209 // not invoke the watchers' OnResourceDoesNotExist() method when a resource is 210 // deleted, nor will it remove the existing resource value from its cache. 211 func (sc *ServerConfig) ServerFeaturesIgnoreResourceDeletion() bool { 212 for _, sf := range sc.serverFeatures { 213 if sf == serverFeaturesIgnoreResourceDeletion { 214 return true 215 } 216 } 217 return false 218 } 219 220 // DialOptions returns a slice of all the configured dial options for this 221 // server. 222 func (sc *ServerConfig) DialOptions() []grpc.DialOption { 223 dopts := []grpc.DialOption{sc.credsDialOption} 224 if sc.extraDialOptions != nil { 225 dopts = append(dopts, sc.extraDialOptions...) 226 } 227 return dopts 228 } 229 230 // Cleanups returns a collection of functions to be called when the xDS client 231 // for this server is closed. Allows cleaning up resources created specifically 232 // for this server. 233 func (sc *ServerConfig) Cleanups() []func() { 234 return sc.cleanups 235 } 236 237 // Equal reports whether sc and other are considered equal. 238 func (sc *ServerConfig) Equal(other *ServerConfig) bool { 239 switch { 240 case sc == nil && other == nil: 241 return true 242 case (sc != nil) != (other != nil): 243 return false 244 case sc.serverURI != other.serverURI: 245 return false 246 case !slices.EqualFunc(sc.channelCreds, other.channelCreds, func(a, b ChannelCreds) bool { return a.Equal(b) }): 247 return false 248 case !slices.Equal(sc.serverFeatures, other.serverFeatures): 249 return false 250 case !sc.selectedCreds.Equal(other.selectedCreds): 251 return false 252 } 253 return true 254 } 255 256 // String returns the string representation of the ServerConfig. 257 func (sc *ServerConfig) String() string { 258 if len(sc.serverFeatures) == 0 { 259 return fmt.Sprintf("%s-%s", sc.serverURI, sc.selectedCreds.String()) 260 } 261 features := strings.Join(sc.serverFeatures, "-") 262 return strings.Join([]string{sc.serverURI, sc.selectedCreds.String(), features}, "-") 263 } 264 265 // The following fields correspond 1:1 with the JSON schema for ServerConfig. 266 type serverConfigJSON struct { 267 ServerURI string `json:"server_uri,omitempty"` 268 ChannelCreds []ChannelCreds `json:"channel_creds,omitempty"` 269 ServerFeatures []string `json:"server_features,omitempty"` 270 } 271 272 // MarshalJSON returns marshaled JSON bytes corresponding to this server config. 273 func (sc *ServerConfig) MarshalJSON() ([]byte, error) { 274 server := &serverConfigJSON{ 275 ServerURI: sc.serverURI, 276 ChannelCreds: sc.channelCreds, 277 ServerFeatures: sc.serverFeatures, 278 } 279 return json.Marshal(server) 280 } 281 282 // extraDialOptions captures custom dial options specified via 283 // credentials.Bundle. 284 type extraDialOptions interface { 285 DialOptions() []grpc.DialOption 286 } 287 288 // UnmarshalJSON takes the json data (a server) and unmarshals it to the struct. 289 func (sc *ServerConfig) UnmarshalJSON(data []byte) error { 290 server := serverConfigJSON{} 291 if err := json.Unmarshal(data, &server); err != nil { 292 return fmt.Errorf("xds: failed to JSON unmarshal server configuration during bootstrap: %v, config:\n%s", err, string(data)) 293 } 294 295 sc.serverURI = server.ServerURI 296 sc.channelCreds = server.ChannelCreds 297 sc.serverFeatures = server.ServerFeatures 298 299 for _, cc := range server.ChannelCreds { 300 // We stop at the first credential type that we support. 301 c := bootstrap.GetCredentials(cc.Type) 302 if c == nil { 303 continue 304 } 305 bundle, cancel, err := c.Build(cc.Config) 306 if err != nil { 307 return fmt.Errorf("failed to build credentials bundle from bootstrap for %q: %v", cc.Type, err) 308 } 309 sc.selectedCreds = cc 310 sc.credsDialOption = grpc.WithCredentialsBundle(bundle) 311 if d, ok := bundle.(extraDialOptions); ok { 312 sc.extraDialOptions = d.DialOptions() 313 } 314 sc.cleanups = append(sc.cleanups, cancel) 315 break 316 } 317 if sc.serverURI == "" { 318 return fmt.Errorf("xds: `server_uri` field in server config cannot be empty: %s", string(data)) 319 } 320 if sc.credsDialOption == nil { 321 return fmt.Errorf("xds: `channel_creds` field in server config cannot be empty: %s", string(data)) 322 } 323 return nil 324 } 325 326 // ServerConfigTestingOptions specifies options for creating a new ServerConfig 327 // for testing purposes. 328 // 329 // # Testing-Only 330 type ServerConfigTestingOptions struct { 331 // URI is the name of the server corresponding to this server config. 332 URI string 333 // ChannelCreds contains a list of channel credentials to use when talking 334 // to this server. If unspecified, `insecure` credentials will be used. 335 ChannelCreds []ChannelCreds 336 // ServerFeatures represents the list of features supported by this server. 337 ServerFeatures []string 338 } 339 340 // ServerConfigForTesting creates a new ServerConfig from the passed in options, 341 // for testing purposes. 342 // 343 // # Testing-Only 344 func ServerConfigForTesting(opts ServerConfigTestingOptions) (*ServerConfig, error) { 345 cc := opts.ChannelCreds 346 if cc == nil { 347 cc = []ChannelCreds{{Type: "insecure"}} 348 } 349 scInternal := &serverConfigJSON{ 350 ServerURI: opts.URI, 351 ChannelCreds: cc, 352 ServerFeatures: opts.ServerFeatures, 353 } 354 scJSON, err := json.Marshal(scInternal) 355 if err != nil { 356 return nil, err 357 } 358 359 sc := new(ServerConfig) 360 if err := sc.UnmarshalJSON(scJSON); err != nil { 361 return nil, err 362 } 363 return sc, nil 364 } 365 366 // Config is the internal representation of the bootstrap configuration provided 367 // to the xDS client. 368 type Config struct { 369 xDSServers ServerConfigs 370 cpcs map[string]certproviderNameAndConfig 371 serverListenerResourceNameTemplate string 372 clientDefaultListenerResourceNameTemplate string 373 authorities map[string]*Authority 374 node node 375 376 // A map from certprovider instance names to parsed buildable configs. 377 certProviderConfigs map[string]*certprovider.BuildableConfig 378 } 379 380 // XDSServers returns the top-level list of management servers to connect to, 381 // ordered by priority. 382 func (c *Config) XDSServers() ServerConfigs { 383 return c.xDSServers 384 } 385 386 // CertProviderConfigs returns a map from certificate provider plugin instance 387 // name to their configuration. Callers must not modify the returned map. 388 func (c *Config) CertProviderConfigs() map[string]*certprovider.BuildableConfig { 389 return c.certProviderConfigs 390 } 391 392 // ServerListenerResourceNameTemplate returns template for the name of the 393 // Listener resource to subscribe to for a gRPC server. 394 // 395 // If starts with "xdstp:", will be interpreted as a new-style name, 396 // in which case the authority of the URI will be used to select the 397 // relevant configuration in the "authorities" map. 398 // 399 // The token "%s", if present in this string, will be replaced with the IP 400 // and port on which the server is listening. (e.g., "0.0.0.0:8080", 401 // "[::]:8080"). For example, a value of "example/resource/%s" could become 402 // "example/resource/0.0.0.0:8080". If the template starts with "xdstp:", 403 // the replaced string will be %-encoded. 404 // 405 // There is no default; if unset, xDS-based server creation fails. 406 func (c *Config) ServerListenerResourceNameTemplate() string { 407 return c.serverListenerResourceNameTemplate 408 } 409 410 // ClientDefaultListenerResourceNameTemplate returns a template for the name of 411 // the Listener resource to subscribe to for a gRPC client channel. Used only 412 // when the channel is created with an "xds:" URI with no authority. 413 // 414 // If starts with "xdstp:", will be interpreted as a new-style name, 415 // in which case the authority of the URI will be used to select the 416 // relevant configuration in the "authorities" map. 417 // 418 // The token "%s", if present in this string, will be replaced with 419 // the service authority (i.e., the path part of the target URI 420 // used to create the gRPC channel). If the template starts with 421 // "xdstp:", the replaced string will be %-encoded. 422 // 423 // Defaults to "%s". 424 func (c *Config) ClientDefaultListenerResourceNameTemplate() string { 425 return c.clientDefaultListenerResourceNameTemplate 426 } 427 428 // Authorities returns a map of authority name to corresponding configuration. 429 // Callers must not modify the returned map. 430 // 431 // This is used in the following cases: 432 // - A gRPC client channel is created using an "xds:" URI that includes 433 // an authority. 434 // - A gRPC client channel is created using an "xds:" URI with no 435 // authority, but the "client_default_listener_resource_name_template" 436 // field above turns it into an "xdstp:" URI. 437 // - A gRPC server is created and the 438 // "server_listener_resource_name_template" field is an "xdstp:" URI. 439 // 440 // In any of those cases, it is an error if the specified authority is 441 // not present in this map. 442 func (c *Config) Authorities() map[string]*Authority { 443 return c.authorities 444 } 445 446 // Node returns xDS a v3 Node proto corresponding to the node field in the 447 // bootstrap configuration, which identifies a specific gRPC instance. 448 func (c *Config) Node() *v3corepb.Node { 449 return c.node.toProto() 450 } 451 452 // Equal returns true if c equals other. 453 func (c *Config) Equal(other *Config) bool { 454 switch { 455 case c == nil && other == nil: 456 return true 457 case (c != nil) != (other != nil): 458 return false 459 case !c.xDSServers.Equal(&other.xDSServers): 460 return false 461 case !maps.EqualFunc(c.certProviderConfigs, other.certProviderConfigs, func(a, b *certprovider.BuildableConfig) bool { return a.String() == b.String() }): 462 return false 463 case c.serverListenerResourceNameTemplate != other.serverListenerResourceNameTemplate: 464 return false 465 case c.clientDefaultListenerResourceNameTemplate != other.clientDefaultListenerResourceNameTemplate: 466 return false 467 case !maps.EqualFunc(c.authorities, other.authorities, func(a, b *Authority) bool { return a.Equal(b) }): 468 return false 469 case !c.node.Equal(other.node): 470 return false 471 } 472 return true 473 } 474 475 // String returns a string representation of the Config. 476 func (c *Config) String() string { 477 s, _ := c.MarshalJSON() 478 return string(s) 479 } 480 481 // The following fields correspond 1:1 with the JSON schema for Config. 482 type configJSON struct { 483 XDSServers ServerConfigs `json:"xds_servers,omitempty"` 484 CertificateProviders map[string]certproviderNameAndConfig `json:"certificate_providers,omitempty"` 485 ServerListenerResourceNameTemplate string `json:"server_listener_resource_name_template,omitempty"` 486 ClientDefaultListenerResourceNameTemplate string `json:"client_default_listener_resource_name_template,omitempty"` 487 Authorities map[string]*Authority `json:"authorities,omitempty"` 488 Node node `json:"node,omitempty"` 489 } 490 491 // MarshalJSON returns marshaled JSON bytes corresponding to this config. 492 func (c *Config) MarshalJSON() ([]byte, error) { 493 config := &configJSON{ 494 XDSServers: c.xDSServers, 495 CertificateProviders: c.cpcs, 496 ServerListenerResourceNameTemplate: c.serverListenerResourceNameTemplate, 497 ClientDefaultListenerResourceNameTemplate: c.clientDefaultListenerResourceNameTemplate, 498 Authorities: c.authorities, 499 Node: c.node, 500 } 501 return json.MarshalIndent(config, " ", " ") 502 } 503 504 // UnmarshalJSON takes the json data (the complete bootstrap configuration) and 505 // unmarshals it to the struct. 506 func (c *Config) UnmarshalJSON(data []byte) error { 507 // Initialize the node field with client controlled values. This ensures 508 // even if the bootstrap configuration did not contain the node field, we 509 // will have a node field with client controlled fields alone. 510 config := configJSON{Node: newNode()} 511 if err := json.Unmarshal(data, &config); err != nil { 512 return fmt.Errorf("xds: json.Unmarshal(%s) failed during bootstrap: %v", string(data), err) 513 } 514 515 c.xDSServers = config.XDSServers 516 c.cpcs = config.CertificateProviders 517 c.serverListenerResourceNameTemplate = config.ServerListenerResourceNameTemplate 518 c.clientDefaultListenerResourceNameTemplate = config.ClientDefaultListenerResourceNameTemplate 519 c.authorities = config.Authorities 520 c.node = config.Node 521 522 // Build the certificate providers configuration to ensure that it is valid. 523 cpcCfgs := make(map[string]*certprovider.BuildableConfig) 524 getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder) 525 for instance, nameAndConfig := range c.cpcs { 526 name := nameAndConfig.PluginName 527 parser := getBuilder(nameAndConfig.PluginName) 528 if parser == nil { 529 // We ignore plugins that we do not know about. 530 continue 531 } 532 bc, err := parser.ParseConfig(nameAndConfig.Config) 533 if err != nil { 534 return fmt.Errorf("xds: config parsing for certificate provider plugin %q failed during bootstrap: %v", name, err) 535 } 536 cpcCfgs[instance] = bc 537 } 538 c.certProviderConfigs = cpcCfgs 539 540 // Default value of the default client listener name template is "%s". 541 if c.clientDefaultListenerResourceNameTemplate == "" { 542 c.clientDefaultListenerResourceNameTemplate = "%s" 543 } 544 if len(c.xDSServers) == 0 { 545 return fmt.Errorf("xds: required field `xds_servers` not found in bootstrap configuration: %s", string(data)) 546 } 547 548 // Post-process the authorities' client listener resource template field: 549 // - if set, it must start with "xdstp://<authority_name>/" 550 // - if not set, it defaults to "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s" 551 for name, authority := range c.authorities { 552 prefix := fmt.Sprintf("xdstp://%s", url.PathEscape(name)) 553 if authority.ClientListenerResourceNameTemplate == "" { 554 authority.ClientListenerResourceNameTemplate = prefix + "/envoy.config.listener.v3.Listener/%s" 555 continue 556 } 557 if !strings.HasPrefix(authority.ClientListenerResourceNameTemplate, prefix) { 558 return fmt.Errorf("xds: field clientListenerResourceNameTemplate %q of authority %q doesn't start with prefix %q", authority.ClientListenerResourceNameTemplate, name, prefix) 559 } 560 } 561 return nil 562 } 563 564 // GetConfiguration returns the bootstrap configuration initialized by reading 565 // the bootstrap file found at ${GRPC_XDS_BOOTSTRAP} or bootstrap contents 566 // specified at ${GRPC_XDS_BOOTSTRAP_CONFIG}. If both env vars are set, the 567 // former is preferred. 568 // 569 // This function tries to process as much of the bootstrap file as possible (in 570 // the presence of the errors) and may return a Config object with certain 571 // fields left unspecified, in which case the caller should use some sane 572 // defaults. 573 func GetConfiguration() (*Config, error) { 574 fName := envconfig.XDSBootstrapFileName 575 fContent := envconfig.XDSBootstrapFileContent 576 577 if fName != "" { 578 if logger.V(2) { 579 logger.Infof("Using bootstrap file with name %q from GRPC_XDS_BOOTSTRAP environment variable", fName) 580 } 581 cfg, err := bootstrapFileReadFunc(fName) 582 if err != nil { 583 return nil, fmt.Errorf("xds: failed to read bootstrap config from file %q: %v", fName, err) 584 } 585 return NewConfigFromContents(cfg) 586 } 587 588 if fContent != "" { 589 if logger.V(2) { 590 logger.Infof("Using bootstrap contents from GRPC_XDS_BOOTSTRAP_CONFIG environment variable") 591 } 592 return NewConfigFromContents([]byte(fContent)) 593 } 594 595 return nil, fmt.Errorf("bootstrap environment variables (%q or %q) not defined", envconfig.XDSBootstrapFileNameEnv, envconfig.XDSBootstrapFileContentEnv) 596 } 597 598 // NewConfigFromContents creates a new bootstrap configuration from the provided 599 // contents. 600 func NewConfigFromContents(data []byte) (*Config, error) { 601 // Normalize the input configuration. 602 buf := bytes.Buffer{} 603 err := json.Indent(&buf, data, "", "") 604 if err != nil { 605 return nil, fmt.Errorf("xds: error normalizing JSON bootstrap configuration: %v", err) 606 } 607 data = bytes.TrimSpace(buf.Bytes()) 608 609 config := &Config{} 610 if err := config.UnmarshalJSON(data); err != nil { 611 return nil, err 612 } 613 return config, nil 614 } 615 616 // ConfigOptionsForTesting specifies options for creating a new bootstrap 617 // configuration for testing purposes. 618 // 619 // # Testing-Only 620 type ConfigOptionsForTesting struct { 621 // Servers is the top-level xDS server configuration. It contains a list of 622 // server configurations. 623 Servers json.RawMessage 624 // CertificateProviders is the certificate providers configuration. 625 CertificateProviders map[string]json.RawMessage 626 // ServerListenerResourceNameTemplate is the listener resource name template 627 // to be used on the gRPC server. 628 ServerListenerResourceNameTemplate string 629 // ClientDefaultListenerResourceNameTemplate is the default listener 630 // resource name template to be used on the gRPC client. 631 ClientDefaultListenerResourceNameTemplate string 632 // Authorities is a list of non-default authorities. 633 Authorities map[string]json.RawMessage 634 // Node identifies the gRPC client/server node in the 635 // proxyless service mesh. 636 Node json.RawMessage 637 } 638 639 // NewContentsForTesting creates a new bootstrap configuration from the passed in 640 // options, for testing purposes. 641 // 642 // # Testing-Only 643 func NewContentsForTesting(opts ConfigOptionsForTesting) ([]byte, error) { 644 var servers ServerConfigs 645 if err := json.Unmarshal(opts.Servers, &servers); err != nil { 646 return nil, err 647 } 648 certProviders := make(map[string]certproviderNameAndConfig) 649 for k, v := range opts.CertificateProviders { 650 cp := certproviderNameAndConfig{} 651 if err := json.Unmarshal(v, &cp); err != nil { 652 return nil, fmt.Errorf("failed to unmarshal certificate provider configuration for %s: %s", k, string(v)) 653 } 654 certProviders[k] = cp 655 } 656 authorities := make(map[string]*Authority) 657 for k, v := range opts.Authorities { 658 a := &Authority{} 659 if err := json.Unmarshal(v, a); err != nil { 660 return nil, fmt.Errorf("failed to unmarshal authority configuration for %s: %s", k, string(v)) 661 } 662 authorities[k] = a 663 } 664 node := newNode() 665 if err := json.Unmarshal(opts.Node, &node); err != nil { 666 return nil, fmt.Errorf("failed to unmarshal node configuration %s: %v", string(opts.Node), err) 667 } 668 cfgJSON := configJSON{ 669 XDSServers: servers, 670 CertificateProviders: certProviders, 671 ServerListenerResourceNameTemplate: opts.ServerListenerResourceNameTemplate, 672 ClientDefaultListenerResourceNameTemplate: opts.ClientDefaultListenerResourceNameTemplate, 673 Authorities: authorities, 674 Node: node, 675 } 676 contents, err := json.MarshalIndent(cfgJSON, " ", " ") 677 if err != nil { 678 return nil, fmt.Errorf("failed to marshal bootstrap configuration for provided options %+v: %v", opts, err) 679 } 680 return contents, nil 681 } 682 683 // certproviderNameAndConfig is the internal representation of 684 // the`certificate_providers` field in the bootstrap configuration. 685 type certproviderNameAndConfig struct { 686 PluginName string `json:"plugin_name"` 687 Config json.RawMessage `json:"config"` 688 } 689 690 // locality is the internal representation of the locality field within node. 691 type locality struct { 692 Region string `json:"region,omitempty"` 693 Zone string `json:"zone,omitempty"` 694 SubZone string `json:"sub_zone,omitempty"` 695 } 696 697 func (l locality) Equal(other locality) bool { 698 return l.Region == other.Region && l.Zone == other.Zone && l.SubZone == other.SubZone 699 } 700 701 func (l locality) isEmpty() bool { 702 return l.Equal(locality{}) 703 } 704 705 type userAgentVersion struct { 706 UserAgentVersion string `json:"user_agent_version,omitempty"` 707 } 708 709 // node is the internal representation of the node field in the bootstrap 710 // configuration. 711 type node struct { 712 ID string `json:"id,omitempty"` 713 Cluster string `json:"cluster,omitempty"` 714 Locality locality `json:"locality,omitempty"` 715 Metadata *structpb.Struct `json:"metadata,omitempty"` 716 717 // The following fields are controlled by the client implementation and 718 // should not unmarshaled from JSON. 719 userAgentName string 720 userAgentVersionType userAgentVersion 721 clientFeatures []string 722 } 723 724 // newNode is a convenience function to create a new node instance with fields 725 // controlled by the client implementation set to the desired values. 726 func newNode() node { 727 return node{ 728 userAgentName: gRPCUserAgentName, 729 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 730 clientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 731 } 732 } 733 734 func (n node) Equal(other node) bool { 735 switch { 736 case n.ID != other.ID: 737 return false 738 case n.Cluster != other.Cluster: 739 return false 740 case !n.Locality.Equal(other.Locality): 741 return false 742 case n.userAgentName != other.userAgentName: 743 return false 744 case n.userAgentVersionType != other.userAgentVersionType: 745 return false 746 } 747 748 // Consider failures in JSON marshaling as being unable to perform the 749 // comparison, and hence return false. 750 nMetadata, err := n.Metadata.MarshalJSON() 751 if err != nil { 752 return false 753 } 754 otherMetadata, err := other.Metadata.MarshalJSON() 755 if err != nil { 756 return false 757 } 758 if !bytes.Equal(nMetadata, otherMetadata) { 759 return false 760 } 761 762 return slices.Equal(n.clientFeatures, other.clientFeatures) 763 } 764 765 func (n node) toProto() *v3corepb.Node { 766 return &v3corepb.Node{ 767 Id: n.ID, 768 Cluster: n.Cluster, 769 Locality: func() *v3corepb.Locality { 770 if n.Locality.isEmpty() { 771 return nil 772 } 773 return &v3corepb.Locality{ 774 Region: n.Locality.Region, 775 Zone: n.Locality.Zone, 776 SubZone: n.Locality.SubZone, 777 } 778 }(), 779 Metadata: proto.Clone(n.Metadata).(*structpb.Struct), 780 UserAgentName: n.userAgentName, 781 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: n.userAgentVersionType.UserAgentVersion}, 782 ClientFeatures: slices.Clone(n.clientFeatures), 783 } 784 }