google.golang.org/grpc@v1.74.2/internal/xds/bootstrap/bootstrap_test.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 20 21 import ( 22 "encoding/json" 23 "errors" 24 "fmt" 25 "os" 26 "testing" 27 28 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 29 "github.com/google/go-cmp/cmp" 30 "google.golang.org/grpc" 31 "google.golang.org/grpc/credentials/tls/certprovider" 32 "google.golang.org/grpc/internal" 33 "google.golang.org/grpc/internal/envconfig" 34 "google.golang.org/grpc/internal/grpctest" 35 "google.golang.org/grpc/xds/bootstrap" 36 "google.golang.org/protobuf/testing/protocmp" 37 "google.golang.org/protobuf/types/known/structpb" 38 ) 39 40 var ( 41 v3BootstrapFileMap = map[string]string{ 42 "serverFeaturesIncludesXDSV3": ` 43 { 44 "node": { 45 "id": "ENVOY_NODE_ID", 46 "metadata": { 47 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 48 } 49 }, 50 "xds_servers" : [{ 51 "server_uri": "trafficdirector.googleapis.com:443", 52 "channel_creds": [ 53 { "type": "google_default" } 54 ], 55 "server_features" : ["xds_v3"] 56 }] 57 }`, 58 "serverFeaturesExcludesXDSV3": ` 59 { 60 "node": { 61 "id": "ENVOY_NODE_ID", 62 "metadata": { 63 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 64 } 65 }, 66 "xds_servers" : [{ 67 "server_uri": "trafficdirector.googleapis.com:443", 68 "channel_creds": [ 69 { "type": "google_default" } 70 ] 71 }] 72 }`, 73 "emptyNodeProto": ` 74 { 75 "xds_servers" : [{ 76 "server_uri": "trafficdirector.googleapis.com:443", 77 "channel_creds": [ 78 { "type": "insecure" } 79 ] 80 }] 81 }`, 82 "unknownTopLevelFieldInFile": ` 83 { 84 "node": { 85 "id": "ENVOY_NODE_ID", 86 "metadata": { 87 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 88 } 89 }, 90 "xds_servers" : [{ 91 "server_uri": "trafficdirector.googleapis.com:443", 92 "channel_creds": [ 93 { "type": "insecure" } 94 ] 95 }], 96 "unknownField": "foobar" 97 }`, 98 "unknownFieldInNodeProto": ` 99 { 100 "node": { 101 "id": "ENVOY_NODE_ID", 102 "unknownField": "foobar", 103 "metadata": { 104 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 105 } 106 }, 107 "xds_servers" : [{ 108 "server_uri": "trafficdirector.googleapis.com:443", 109 "channel_creds": [ 110 { "type": "insecure" } 111 ] 112 }] 113 }`, 114 "unknownFieldInXdsServer": ` 115 { 116 "node": { 117 "id": "ENVOY_NODE_ID", 118 "metadata": { 119 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 120 } 121 }, 122 "xds_servers" : [{ 123 "server_uri": "trafficdirector.googleapis.com:443", 124 "channel_creds": [ 125 { "type": "insecure" } 126 ], 127 "unknownField": "foobar" 128 }] 129 }`, 130 "multipleChannelCreds": ` 131 { 132 "node": { 133 "id": "ENVOY_NODE_ID", 134 "metadata": { 135 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 136 } 137 }, 138 "xds_servers" : [{ 139 "server_uri": "trafficdirector.googleapis.com:443", 140 "channel_creds": [ 141 { "type": "not-google-default" }, 142 { "type": "google_default" } 143 ], 144 "server_features": ["xds_v3"] 145 }] 146 }`, 147 "goodBootstrap": ` 148 { 149 "node": { 150 "id": "ENVOY_NODE_ID", 151 "metadata": { 152 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 153 } 154 }, 155 "xds_servers" : [{ 156 "server_uri": "trafficdirector.googleapis.com:443", 157 "channel_creds": [ 158 { "type": "google_default" } 159 ], 160 "server_features": ["xds_v3"] 161 }] 162 }`, 163 "multipleXDSServers": ` 164 { 165 "node": { 166 "id": "ENVOY_NODE_ID", 167 "metadata": { 168 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 169 } 170 }, 171 "xds_servers" : [ 172 { 173 "server_uri": "trafficdirector.googleapis.com:443", 174 "channel_creds": [{ "type": "google_default" }], 175 "server_features": ["xds_v3"] 176 }, 177 { 178 "server_uri": "backup.never.use.com:1234", 179 "channel_creds": [{ "type": "google_default" }] 180 } 181 ] 182 }`, 183 "serverSupportsIgnoreResourceDeletion": ` 184 { 185 "node": { 186 "id": "ENVOY_NODE_ID", 187 "metadata": { 188 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 189 } 190 }, 191 "xds_servers" : [{ 192 "server_uri": "trafficdirector.googleapis.com:443", 193 "channel_creds": [ 194 { "type": "google_default" } 195 ], 196 "server_features" : ["ignore_resource_deletion", "xds_v3"] 197 }] 198 }`, 199 } 200 metadata = &structpb.Struct{ 201 Fields: map[string]*structpb.Value{ 202 "TRAFFICDIRECTOR_GRPC_HOSTNAME": { 203 Kind: &structpb.Value_StringValue{StringValue: "trafficdirector"}, 204 }, 205 }, 206 } 207 v3Node = node{ 208 ID: "ENVOY_NODE_ID", 209 Metadata: metadata, 210 userAgentName: gRPCUserAgentName, 211 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 212 clientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 213 } 214 configWithInsecureCreds = &Config{ 215 xDSServers: []*ServerConfig{{ 216 serverURI: "trafficdirector.googleapis.com:443", 217 channelCreds: []ChannelCreds{{Type: "insecure"}}, 218 selectedCreds: ChannelCreds{Type: "insecure"}, 219 }}, 220 node: v3Node, 221 clientDefaultListenerResourceNameTemplate: "%s", 222 } 223 configWithMultipleChannelCredsAndV3 = &Config{ 224 xDSServers: []*ServerConfig{{ 225 serverURI: "trafficdirector.googleapis.com:443", 226 channelCreds: []ChannelCreds{{Type: "not-google-default"}, {Type: "google_default"}}, 227 serverFeatures: []string{"xds_v3"}, 228 selectedCreds: ChannelCreds{Type: "google_default"}, 229 }}, 230 node: v3Node, 231 clientDefaultListenerResourceNameTemplate: "%s", 232 } 233 configWithGoogleDefaultCredsAndV3 = &Config{ 234 xDSServers: []*ServerConfig{{ 235 serverURI: "trafficdirector.googleapis.com:443", 236 channelCreds: []ChannelCreds{{Type: "google_default"}}, 237 serverFeatures: []string{"xds_v3"}, 238 selectedCreds: ChannelCreds{Type: "google_default"}, 239 }}, 240 node: v3Node, 241 clientDefaultListenerResourceNameTemplate: "%s", 242 } 243 configWithMultipleServers = &Config{ 244 xDSServers: []*ServerConfig{ 245 { 246 serverURI: "trafficdirector.googleapis.com:443", 247 channelCreds: []ChannelCreds{{Type: "google_default"}}, 248 serverFeatures: []string{"xds_v3"}, 249 selectedCreds: ChannelCreds{Type: "google_default"}, 250 }, 251 { 252 serverURI: "backup.never.use.com:1234", 253 channelCreds: []ChannelCreds{{Type: "google_default"}}, 254 selectedCreds: ChannelCreds{Type: "google_default"}, 255 }, 256 }, 257 node: v3Node, 258 clientDefaultListenerResourceNameTemplate: "%s", 259 } 260 configWithGoogleDefaultCredsAndIgnoreResourceDeletion = &Config{ 261 xDSServers: []*ServerConfig{{ 262 serverURI: "trafficdirector.googleapis.com:443", 263 channelCreds: []ChannelCreds{{Type: "google_default"}}, 264 serverFeatures: []string{"ignore_resource_deletion", "xds_v3"}, 265 selectedCreds: ChannelCreds{Type: "google_default"}, 266 }}, 267 node: v3Node, 268 clientDefaultListenerResourceNameTemplate: "%s", 269 } 270 configWithGoogleDefaultCredsAndNoServerFeatures = &Config{ 271 xDSServers: []*ServerConfig{{ 272 serverURI: "trafficdirector.googleapis.com:443", 273 channelCreds: []ChannelCreds{{Type: "google_default"}}, 274 selectedCreds: ChannelCreds{Type: "google_default"}, 275 }}, 276 node: v3Node, 277 clientDefaultListenerResourceNameTemplate: "%s", 278 } 279 ) 280 281 func fileReadFromFileMap(bootstrapFileMap map[string]string, name string) ([]byte, error) { 282 if b, ok := bootstrapFileMap[name]; ok { 283 return []byte(b), nil 284 } 285 return nil, os.ErrNotExist 286 } 287 288 func setupBootstrapOverride(bootstrapFileMap map[string]string) func() { 289 oldFileReadFunc := bootstrapFileReadFunc 290 bootstrapFileReadFunc = func(filename string) ([]byte, error) { 291 return fileReadFromFileMap(bootstrapFileMap, filename) 292 } 293 return func() { bootstrapFileReadFunc = oldFileReadFunc } 294 } 295 296 // This function overrides the bootstrap file NAME env variable, to test the 297 // code that reads file with the given fileName. 298 func testGetConfigurationWithFileNameEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) { 299 origBootstrapFileName := envconfig.XDSBootstrapFileName 300 envconfig.XDSBootstrapFileName = fileName 301 defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }() 302 303 c, err := GetConfiguration() 304 if (err != nil) != wantError { 305 t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError) 306 } 307 if wantError { 308 return 309 } 310 if diff := cmp.Diff(wantConfig, c); diff != "" { 311 t.Fatalf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 312 } 313 } 314 315 // This function overrides the bootstrap file CONTENT env variable, to test the 316 // code that uses the content from env directly. 317 func testGetConfigurationWithFileContentEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) { 318 t.Helper() 319 b, err := bootstrapFileReadFunc(fileName) 320 if err != nil { 321 t.Skip(err) 322 } 323 origBootstrapContent := envconfig.XDSBootstrapFileContent 324 envconfig.XDSBootstrapFileContent = string(b) 325 defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }() 326 327 c, err := GetConfiguration() 328 if (err != nil) != wantError { 329 t.Fatalf("GetConfiguration() returned error %v, wantError: %v", err, wantError) 330 } 331 if wantError { 332 return 333 } 334 if diff := cmp.Diff(wantConfig, c); diff != "" { 335 t.Fatalf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 336 } 337 } 338 339 // Tests GetConfiguration with bootstrap file contents that are expected to 340 // fail. 341 func (s) TestGetConfiguration_Failure(t *testing.T) { 342 bootstrapFileMap := map[string]string{ 343 "empty": "", 344 "badJSON": `["test": 123]`, 345 "noBalancerName": `{"node": {"id": "ENVOY_NODE_ID"}}`, 346 "emptyXdsServer": ` 347 { 348 "node": { 349 "id": "ENVOY_NODE_ID", 350 "metadata": { 351 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 352 } 353 } 354 }`, 355 "emptyChannelCreds": ` 356 { 357 "node": { 358 "id": "ENVOY_NODE_ID", 359 "metadata": { 360 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 361 } 362 }, 363 "xds_servers" : [{ 364 "server_uri": "trafficdirector.googleapis.com:443" 365 }] 366 }`, 367 "nonGoogleDefaultCreds": ` 368 { 369 "node": { 370 "id": "ENVOY_NODE_ID", 371 "metadata": { 372 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 373 } 374 }, 375 "xds_servers" : [{ 376 "server_uri": "trafficdirector.googleapis.com:443", 377 "channel_creds": [ 378 { "type": "not-google-default" } 379 ] 380 }] 381 }`, 382 } 383 cancel := setupBootstrapOverride(bootstrapFileMap) 384 defer cancel() 385 386 for _, name := range []string{"nonExistentBootstrapFile", "badJSON", "noBalancerName", "emptyXdsServer"} { 387 t.Run(name, func(t *testing.T) { 388 testGetConfigurationWithFileNameEnv(t, name, true, nil) 389 testGetConfigurationWithFileContentEnv(t, name, true, nil) 390 }) 391 } 392 const name = "empty" 393 t.Run(name, func(t *testing.T) { 394 testGetConfigurationWithFileNameEnv(t, name, true, nil) 395 // If both the env vars are empty, a nil config with a nil error must be 396 // returned. 397 testGetConfigurationWithFileContentEnv(t, name, false, nil) 398 }) 399 } 400 401 // Tests the functionality in GetConfiguration with different bootstrap file 402 // contents. It overrides the fileReadFunc by returning bootstrap file contents 403 // defined in this test, instead of reading from a file. 404 func (s) TestGetConfiguration_Success(t *testing.T) { 405 cancel := setupBootstrapOverride(v3BootstrapFileMap) 406 defer cancel() 407 408 tests := []struct { 409 name string 410 wantConfig *Config 411 }{ 412 { 413 name: "emptyNodeProto", 414 wantConfig: &Config{ 415 xDSServers: []*ServerConfig{{ 416 serverURI: "trafficdirector.googleapis.com:443", 417 channelCreds: []ChannelCreds{{Type: "insecure"}}, 418 selectedCreds: ChannelCreds{Type: "insecure"}, 419 }}, 420 node: node{ 421 userAgentName: gRPCUserAgentName, 422 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 423 clientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 424 }, 425 clientDefaultListenerResourceNameTemplate: "%s", 426 }, 427 }, 428 {"unknownTopLevelFieldInFile", configWithInsecureCreds}, 429 {"unknownFieldInNodeProto", configWithInsecureCreds}, 430 {"unknownFieldInXdsServer", configWithInsecureCreds}, 431 {"multipleChannelCreds", configWithMultipleChannelCredsAndV3}, 432 {"goodBootstrap", configWithGoogleDefaultCredsAndV3}, 433 {"multipleXDSServers", configWithMultipleServers}, 434 {"serverSupportsIgnoreResourceDeletion", configWithGoogleDefaultCredsAndIgnoreResourceDeletion}, 435 } 436 437 for _, test := range tests { 438 t.Run(test.name, func(t *testing.T) { 439 testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig) 440 testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig) 441 }) 442 } 443 } 444 445 // Tests that the two bootstrap env variables are read in correct priority. 446 // 447 // "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap 448 // configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which 449 // directly specifies the bootstrap configuration in itself. 450 func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) { 451 oldFileReadFunc := bootstrapFileReadFunc 452 bootstrapFileReadFunc = func(filename string) ([]byte, error) { 453 return fileReadFromFileMap(v3BootstrapFileMap, filename) 454 } 455 defer func() { bootstrapFileReadFunc = oldFileReadFunc }() 456 457 goodFileName1 := "serverFeaturesIncludesXDSV3" 458 goodConfig1 := configWithGoogleDefaultCredsAndV3 459 460 goodFileName2 := "serverFeaturesExcludesXDSV3" 461 goodFileContent2 := v3BootstrapFileMap[goodFileName2] 462 goodConfig2 := configWithGoogleDefaultCredsAndNoServerFeatures 463 464 origBootstrapFileName := envconfig.XDSBootstrapFileName 465 envconfig.XDSBootstrapFileName = "" 466 defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }() 467 468 origBootstrapContent := envconfig.XDSBootstrapFileContent 469 envconfig.XDSBootstrapFileContent = "" 470 defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }() 471 472 // When both env variables are empty, GetConfiguration should return nil. 473 if cfg, err := GetConfiguration(); err != nil || cfg != nil { 474 t.Errorf("GetConfiguration() returned (%v, %v), want (<nil>, <nil>)", cfg, err) 475 } 476 477 // When one of them is set, it should be used. 478 envconfig.XDSBootstrapFileName = goodFileName1 479 envconfig.XDSBootstrapFileContent = "" 480 c, err := GetConfiguration() 481 if err != nil { 482 t.Errorf("GetConfiguration() failed: %v", err) 483 } 484 if diff := cmp.Diff(goodConfig1, c); diff != "" { 485 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 486 } 487 488 envconfig.XDSBootstrapFileName = "" 489 envconfig.XDSBootstrapFileContent = goodFileContent2 490 c, err = GetConfiguration() 491 if err != nil { 492 t.Errorf("GetConfiguration() failed: %v", err) 493 } 494 if diff := cmp.Diff(goodConfig2, c); diff != "" { 495 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 496 } 497 498 // Set both, file name should be read. 499 envconfig.XDSBootstrapFileName = goodFileName1 500 envconfig.XDSBootstrapFileContent = goodFileContent2 501 c, err = GetConfiguration() 502 if err != nil { 503 t.Errorf("GetConfiguration() failed: %v", err) 504 } 505 if diff := cmp.Diff(goodConfig1, c); diff != "" { 506 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 507 } 508 } 509 510 func init() { 511 certprovider.Register(&fakeCertProviderBuilder{}) 512 } 513 514 const fakeCertProviderName = "fake-certificate-provider" 515 516 // fakeCertProviderBuilder builds new instances of fakeCertProvider and 517 // interprets the config provided to it as JSON with a single key and value. 518 type fakeCertProviderBuilder struct{} 519 520 // ParseConfig expects input in JSON format containing a map from string to 521 // string, with a single entry and mapKey being "configKey". 522 func (b *fakeCertProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) { 523 config, ok := cfg.(json.RawMessage) 524 if !ok { 525 return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config) 526 } 527 var cfgData map[string]string 528 if err := json.Unmarshal(config, &cfgData); err != nil { 529 return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err) 530 } 531 if len(cfgData) != 1 || cfgData["configKey"] == "" { 532 return nil, errors.New("fakeCertProviderBuilder received invalid config") 533 } 534 fc := &fakeStableConfig{config: cfgData} 535 return certprovider.NewBuildableConfig(fakeCertProviderName, fc.canonical(), func(certprovider.BuildOptions) certprovider.Provider { 536 return &fakeCertProvider{} 537 }), nil 538 } 539 540 func (b *fakeCertProviderBuilder) Name() string { 541 return fakeCertProviderName 542 } 543 544 type fakeStableConfig struct { 545 config map[string]string 546 } 547 548 func (c *fakeStableConfig) canonical() []byte { 549 var cfg string 550 for k, v := range c.config { 551 cfg = fmt.Sprintf("%s:%s", k, v) 552 } 553 return []byte(cfg) 554 } 555 556 // fakeCertProvider is an empty implementation of the Provider interface. 557 type fakeCertProvider struct { 558 certprovider.Provider 559 } 560 561 func (s) TestGetConfiguration_CertificateProviders(t *testing.T) { 562 bootstrapFileMap := map[string]string{ 563 "badJSONCertProviderConfig": ` 564 { 565 "node": { 566 "id": "ENVOY_NODE_ID", 567 "metadata": { 568 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 569 } 570 }, 571 "xds_servers" : [{ 572 "server_uri": "trafficdirector.googleapis.com:443", 573 "channel_creds": [ 574 { "type": "google_default" } 575 ], 576 "server_features" : ["foo", "bar", "xds_v3"], 577 }], 578 "certificate_providers": "bad JSON" 579 }`, 580 "allUnknownCertProviders": ` 581 { 582 "node": { 583 "id": "ENVOY_NODE_ID", 584 "metadata": { 585 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 586 } 587 }, 588 "xds_servers" : [{ 589 "server_uri": "trafficdirector.googleapis.com:443", 590 "channel_creds": [ 591 { "type": "google_default" } 592 ], 593 "server_features" : ["xds_v3"] 594 }], 595 "certificate_providers": { 596 "unknownProviderInstance1": { 597 "plugin_name": "foo", 598 "config": {"foo": "bar"} 599 }, 600 "unknownProviderInstance2": { 601 "plugin_name": "bar", 602 "config": {"foo": "bar"} 603 } 604 } 605 }`, 606 "badCertProviderConfig": ` 607 { 608 "node": { 609 "id": "ENVOY_NODE_ID", 610 "metadata": { 611 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 612 } 613 }, 614 "xds_servers" : [{ 615 "server_uri": "trafficdirector.googleapis.com:443", 616 "channel_creds": [ 617 { "type": "google_default" } 618 ], 619 "server_features" : ["xds_v3"], 620 }], 621 "certificate_providers": { 622 "unknownProviderInstance": { 623 "plugin_name": "foo", 624 "config": {"foo": "bar"} 625 }, 626 "fakeProviderInstanceBad": { 627 "plugin_name": "fake-certificate-provider", 628 "config": {"configKey": 666} 629 } 630 } 631 }`, 632 "goodCertProviderConfig": ` 633 { 634 "node": { 635 "id": "ENVOY_NODE_ID", 636 "metadata": { 637 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 638 } 639 }, 640 "xds_servers" : [{ 641 "server_uri": "trafficdirector.googleapis.com:443", 642 "channel_creds": [ 643 { "type": "insecure" } 644 ], 645 "server_features" : ["xds_v3"] 646 }], 647 "certificate_providers": { 648 "unknownProviderInstance": { 649 "plugin_name": "foo", 650 "config": {"foo": "bar"} 651 }, 652 "fakeProviderInstance": { 653 "plugin_name": "fake-certificate-provider", 654 "config": {"configKey": "configValue"} 655 } 656 } 657 }`, 658 } 659 660 getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder) 661 parser := getBuilder(fakeCertProviderName) 662 if parser == nil { 663 t.Fatalf("Missing certprovider plugin %q", fakeCertProviderName) 664 } 665 wantCfg, err := parser.ParseConfig(json.RawMessage(`{"configKey": "configValue"}`)) 666 if err != nil { 667 t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err) 668 } 669 670 cancel := setupBootstrapOverride(bootstrapFileMap) 671 defer cancel() 672 673 goodConfig := &Config{ 674 xDSServers: []*ServerConfig{{ 675 serverURI: "trafficdirector.googleapis.com:443", 676 channelCreds: []ChannelCreds{{Type: "insecure"}}, 677 serverFeatures: []string{"xds_v3"}, 678 selectedCreds: ChannelCreds{Type: "insecure"}, 679 }}, 680 certProviderConfigs: map[string]*certprovider.BuildableConfig{ 681 "fakeProviderInstance": wantCfg, 682 }, 683 clientDefaultListenerResourceNameTemplate: "%s", 684 node: v3Node, 685 } 686 tests := []struct { 687 name string 688 wantConfig *Config 689 wantErr bool 690 }{ 691 { 692 name: "badJSONCertProviderConfig", 693 wantErr: true, 694 }, 695 { 696 697 name: "badCertProviderConfig", 698 wantErr: true, 699 }, 700 { 701 702 name: "allUnknownCertProviders", 703 wantConfig: configWithGoogleDefaultCredsAndV3, 704 }, 705 { 706 name: "goodCertProviderConfig", 707 wantConfig: goodConfig, 708 }, 709 } 710 711 for _, test := range tests { 712 t.Run(test.name, func(t *testing.T) { 713 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 714 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 715 }) 716 } 717 } 718 719 func (s) TestGetConfiguration_ServerListenerResourceNameTemplate(t *testing.T) { 720 cancel := setupBootstrapOverride(map[string]string{ 721 "badServerListenerResourceNameTemplate:": ` 722 { 723 "node": { 724 "id": "ENVOY_NODE_ID", 725 "metadata": { 726 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 727 } 728 }, 729 "xds_servers" : [{ 730 "server_uri": "trafficdirector.googleapis.com:443", 731 "channel_creds": [ 732 { "type": "google_default" } 733 ] 734 }], 735 "server_listener_resource_name_template": 123456789 736 }`, 737 "goodServerListenerResourceNameTemplate": ` 738 { 739 "node": { 740 "id": "ENVOY_NODE_ID", 741 "metadata": { 742 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 743 } 744 }, 745 "xds_servers" : [{ 746 "server_uri": "trafficdirector.googleapis.com:443", 747 "channel_creds": [ 748 { "type": "google_default" } 749 ] 750 }], 751 "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s" 752 }`, 753 }) 754 defer cancel() 755 756 tests := []struct { 757 name string 758 wantConfig *Config 759 wantErr bool 760 }{ 761 { 762 name: "badServerListenerResourceNameTemplate", 763 wantErr: true, 764 }, 765 { 766 name: "goodServerListenerResourceNameTemplate", 767 wantConfig: &Config{ 768 xDSServers: []*ServerConfig{{ 769 serverURI: "trafficdirector.googleapis.com:443", 770 channelCreds: []ChannelCreds{{Type: "google_default"}}, 771 selectedCreds: ChannelCreds{Type: "google_default"}, 772 }}, 773 node: v3Node, 774 serverListenerResourceNameTemplate: "grpc/server?xds.resource.listening_address=%s", 775 clientDefaultListenerResourceNameTemplate: "%s", 776 }, 777 }, 778 } 779 780 for _, test := range tests { 781 t.Run(test.name, func(t *testing.T) { 782 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 783 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 784 }) 785 } 786 } 787 788 func (s) TestGetConfiguration_Federation(t *testing.T) { 789 cancel := setupBootstrapOverride(map[string]string{ 790 "badclientListenerResourceNameTemplate": ` 791 { 792 "node": { "id": "ENVOY_NODE_ID" }, 793 "xds_servers" : [{ 794 "server_uri": "trafficdirector.googleapis.com:443" 795 }], 796 "client_default_listener_resource_name_template": 123456789 797 }`, 798 "badclientListenerResourceNameTemplatePerAuthority": ` 799 { 800 "node": { "id": "ENVOY_NODE_ID" }, 801 "xds_servers" : [{ 802 "server_uri": "trafficdirector.googleapis.com:443", 803 "channel_creds": [ { "type": "google_default" } ] 804 }], 805 "authorities": { 806 "xds.td.com": { 807 "client_listener_resource_name_template": "some/template/%s", 808 "xds_servers": [{ 809 "server_uri": "td.com", 810 "channel_creds": [ { "type": "google_default" } ], 811 "server_features" : ["foo", "bar", "xds_v3"] 812 }] 813 } 814 } 815 }`, 816 "good": ` 817 { 818 "node": { 819 "id": "ENVOY_NODE_ID", 820 "metadata": { 821 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 822 } 823 }, 824 "xds_servers" : [{ 825 "server_uri": "trafficdirector.googleapis.com:443", 826 "channel_creds": [ { "type": "google_default" } ] 827 }], 828 "server_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s", 829 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 830 "authorities": { 831 "xds.td.com": { 832 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 833 "xds_servers": [{ 834 "server_uri": "td.com", 835 "channel_creds": [ { "type": "google_default" } ], 836 "server_features" : ["xds_v3"] 837 }] 838 } 839 } 840 }`, 841 // If client_default_listener_resource_name_template is not set, it 842 // defaults to "%s". 843 "goodWithDefaultDefaultClientListenerTemplate": ` 844 { 845 "node": { 846 "id": "ENVOY_NODE_ID", 847 "metadata": { 848 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 849 } 850 }, 851 "xds_servers" : [{ 852 "server_uri": "trafficdirector.googleapis.com:443", 853 "channel_creds": [ { "type": "google_default" } ] 854 }] 855 }`, 856 // If client_listener_resource_name_template in authority is not set, it 857 // defaults to 858 // "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s". 859 "goodWithDefaultClientListenerTemplatePerAuthority": ` 860 { 861 "node": { 862 "id": "ENVOY_NODE_ID", 863 "metadata": { 864 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 865 } 866 }, 867 "xds_servers" : [{ 868 "server_uri": "trafficdirector.googleapis.com:443", 869 "channel_creds": [ { "type": "google_default" } ] 870 }], 871 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 872 "authorities": { 873 "xds.td.com": { }, 874 "#.com": { } 875 } 876 }`, 877 // It's OK for an authority to not have servers. The top-level server 878 // will be used. 879 "goodWithNoServerPerAuthority": ` 880 { 881 "node": { 882 "id": "ENVOY_NODE_ID", 883 "metadata": { 884 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 885 } 886 }, 887 "xds_servers" : [{ 888 "server_uri": "trafficdirector.googleapis.com:443", 889 "channel_creds": [ { "type": "google_default" } ] 890 }], 891 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 892 "authorities": { 893 "xds.td.com": { 894 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s" 895 } 896 } 897 }`, 898 }) 899 defer cancel() 900 901 tests := []struct { 902 name string 903 wantConfig *Config 904 wantErr bool 905 }{ 906 { 907 name: "badclientListenerResourceNameTemplate", 908 wantErr: true, 909 }, 910 { 911 name: "badclientListenerResourceNameTemplatePerAuthority", 912 wantErr: true, 913 }, 914 { 915 name: "good", 916 wantConfig: &Config{ 917 xDSServers: []*ServerConfig{{ 918 serverURI: "trafficdirector.googleapis.com:443", 919 channelCreds: []ChannelCreds{{Type: "google_default"}}, 920 selectedCreds: ChannelCreds{Type: "google_default"}, 921 }}, 922 node: v3Node, 923 serverListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s", 924 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 925 authorities: map[string]*Authority{ 926 "xds.td.com": { 927 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 928 XDSServers: []*ServerConfig{{ 929 serverURI: "td.com", 930 channelCreds: []ChannelCreds{{Type: "google_default"}}, 931 serverFeatures: []string{"xds_v3"}, 932 selectedCreds: ChannelCreds{Type: "google_default"}, 933 }}, 934 }, 935 }, 936 }, 937 }, 938 { 939 name: "goodWithDefaultDefaultClientListenerTemplate", 940 wantConfig: &Config{ 941 xDSServers: []*ServerConfig{{ 942 serverURI: "trafficdirector.googleapis.com:443", 943 channelCreds: []ChannelCreds{{Type: "google_default"}}, 944 selectedCreds: ChannelCreds{Type: "google_default"}, 945 }}, 946 node: v3Node, 947 clientDefaultListenerResourceNameTemplate: "%s", 948 }, 949 }, 950 { 951 name: "goodWithDefaultClientListenerTemplatePerAuthority", 952 wantConfig: &Config{ 953 xDSServers: []*ServerConfig{{ 954 serverURI: "trafficdirector.googleapis.com:443", 955 channelCreds: []ChannelCreds{{Type: "google_default"}}, 956 selectedCreds: ChannelCreds{Type: "google_default"}, 957 }}, 958 node: v3Node, 959 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 960 authorities: map[string]*Authority{ 961 "xds.td.com": { 962 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 963 }, 964 "#.com": { 965 ClientListenerResourceNameTemplate: "xdstp://%23.com/envoy.config.listener.v3.Listener/%s", 966 }, 967 }, 968 }, 969 }, 970 { 971 name: "goodWithNoServerPerAuthority", 972 wantConfig: &Config{ 973 xDSServers: []*ServerConfig{{ 974 serverURI: "trafficdirector.googleapis.com:443", 975 channelCreds: []ChannelCreds{{Type: "google_default"}}, 976 selectedCreds: ChannelCreds{Type: "google_default"}, 977 }}, 978 node: v3Node, 979 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 980 authorities: map[string]*Authority{ 981 "xds.td.com": { 982 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 983 }, 984 }, 985 }, 986 }, 987 } 988 989 for _, test := range tests { 990 t.Run(test.name, func(t *testing.T) { 991 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 992 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 993 }) 994 } 995 } 996 997 func (s) TestServerConfigMarshalAndUnmarshal(t *testing.T) { 998 origConfig, err := ServerConfigForTesting(ServerConfigTestingOptions{URI: "test-server", ServerFeatures: []string{"xds_v3"}}) 999 if err != nil { 1000 t.Fatalf("Failed to create server config for testing: %v", err) 1001 } 1002 marshaledCfg, err := json.Marshal(origConfig) 1003 if err != nil { 1004 t.Fatalf("failed to marshal: %v", err) 1005 } 1006 1007 unmarshaledConfig := new(ServerConfig) 1008 if err := json.Unmarshal(marshaledCfg, unmarshaledConfig); err != nil { 1009 t.Fatalf("failed to unmarshal: %v", err) 1010 } 1011 if diff := cmp.Diff(origConfig, unmarshaledConfig); diff != "" { 1012 t.Fatalf("Unexpected diff in server config (-want, +got):\n%s", diff) 1013 } 1014 } 1015 1016 func (s) TestDefaultBundles(t *testing.T) { 1017 tests := []string{"google_default", "insecure", "tls"} 1018 1019 for _, typename := range tests { 1020 t.Run(typename, func(t *testing.T) { 1021 if c := bootstrap.GetCredentials(typename); c == nil { 1022 t.Errorf(`bootstrap.GetCredentials(%s) credential is nil, want non-nil`, typename) 1023 } 1024 }) 1025 } 1026 } 1027 1028 type s struct { 1029 grpctest.Tester 1030 } 1031 1032 func Test(t *testing.T) { 1033 grpctest.RunSubTests(t, s{}) 1034 } 1035 1036 func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct { 1037 t.Helper() 1038 1039 ret, err := structpb.NewStruct(input) 1040 if err != nil { 1041 t.Fatalf("Failed to create new struct proto from map %v: %v", input, err) 1042 } 1043 return ret 1044 } 1045 1046 func (s) TestNode_MarshalAndUnmarshal(t *testing.T) { 1047 tests := []struct { 1048 desc string 1049 inputJSON []byte 1050 wantNode node 1051 }{ 1052 { 1053 desc: "basic happy case", 1054 inputJSON: []byte(`{ 1055 "id": "id", 1056 "cluster": "cluster", 1057 "locality": { 1058 "region": "region", 1059 "zone": "zone", 1060 "sub_zone": "sub_zone" 1061 }, 1062 "metadata": { 1063 "k1": "v1", 1064 "k2": 101, 1065 "k3": 280.0 1066 } 1067 }`), 1068 wantNode: node{ 1069 ID: "id", 1070 Cluster: "cluster", 1071 Locality: locality{ 1072 Region: "region", 1073 Zone: "zone", 1074 SubZone: "sub_zone", 1075 }, 1076 Metadata: newStructProtoFromMap(t, map[string]any{ 1077 "k1": "v1", 1078 "k2": 101, 1079 "k3": 280.0, 1080 }), 1081 userAgentName: "gRPC Go", 1082 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 1083 clientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1084 }, 1085 }, 1086 { 1087 desc: "client controlled fields", 1088 inputJSON: []byte(`{ 1089 "id": "id", 1090 "cluster": "cluster", 1091 "user_agent_name": "user_agent_name", 1092 "user_agent_version_type": { 1093 "user_agent_version": "version" 1094 }, 1095 "client_features": ["feature1", "feature2"] 1096 }`), 1097 wantNode: node{ 1098 ID: "id", 1099 Cluster: "cluster", 1100 userAgentName: "gRPC Go", 1101 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 1102 clientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1103 }, 1104 }, 1105 } 1106 1107 for _, test := range tests { 1108 t.Run(test.desc, func(t *testing.T) { 1109 // Unmarshal the input JSON into a node struct and check if it 1110 // matches expectations. 1111 unmarshaledNode := newNode() 1112 if err := json.Unmarshal([]byte(test.inputJSON), &unmarshaledNode); err != nil { 1113 t.Fatal(err) 1114 } 1115 if diff := cmp.Diff(test.wantNode, unmarshaledNode); diff != "" { 1116 t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff) 1117 } 1118 1119 // Marshal the recently unmarshaled node struct into JSON and 1120 // remarshal it into another node struct, and check that it still 1121 // matches expectations. 1122 marshaledJSON, err := json.Marshal(unmarshaledNode) 1123 if err != nil { 1124 t.Fatalf("node.MarshalJSON() failed: %v", err) 1125 } 1126 reUnmarshaledNode := newNode() 1127 if err := json.Unmarshal([]byte(marshaledJSON), &reUnmarshaledNode); err != nil { 1128 t.Fatal(err) 1129 } 1130 if diff := cmp.Diff(test.wantNode, reUnmarshaledNode); diff != "" { 1131 t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff) 1132 } 1133 }) 1134 } 1135 } 1136 1137 func (s) TestNode_ToProto(t *testing.T) { 1138 tests := []struct { 1139 desc string 1140 inputNode node 1141 wantProto *v3corepb.Node 1142 }{ 1143 { 1144 desc: "all fields set", 1145 inputNode: func() node { 1146 n := newNode() 1147 n.ID = "id" 1148 n.Cluster = "cluster" 1149 n.Locality = locality{ 1150 Region: "region", 1151 Zone: "zone", 1152 SubZone: "sub_zone", 1153 } 1154 n.Metadata = newStructProtoFromMap(t, map[string]any{ 1155 "k1": "v1", 1156 "k2": 101, 1157 "k3": 280.0, 1158 }) 1159 return n 1160 }(), 1161 wantProto: &v3corepb.Node{ 1162 Id: "id", 1163 Cluster: "cluster", 1164 Locality: &v3corepb.Locality{ 1165 Region: "region", 1166 Zone: "zone", 1167 SubZone: "sub_zone", 1168 }, 1169 Metadata: newStructProtoFromMap(t, map[string]any{ 1170 "k1": "v1", 1171 "k2": 101, 1172 "k3": 280.0, 1173 }), 1174 UserAgentName: "gRPC Go", 1175 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 1176 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1177 }, 1178 }, 1179 { 1180 desc: "some fields unset", 1181 inputNode: func() node { 1182 n := newNode() 1183 n.ID = "id" 1184 return n 1185 }(), 1186 wantProto: &v3corepb.Node{ 1187 Id: "id", 1188 UserAgentName: "gRPC Go", 1189 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 1190 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1191 }, 1192 }, 1193 } 1194 1195 for _, test := range tests { 1196 t.Run(test.desc, func(t *testing.T) { 1197 gotProto := test.inputNode.toProto() 1198 if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" { 1199 t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff) 1200 } 1201 }) 1202 } 1203 } 1204 1205 // Tests the case where the xDS fallback env var is set to false, and verifies 1206 // that only the first server from the list of server configurations is used. 1207 func (s) TestGetConfiguration_FallbackDisabled(t *testing.T) { 1208 origFallbackEnv := envconfig.XDSFallbackSupport 1209 envconfig.XDSFallbackSupport = false 1210 defer func() { envconfig.XDSFallbackSupport = origFallbackEnv }() 1211 1212 cancel := setupBootstrapOverride(map[string]string{ 1213 "multipleXDSServers": ` 1214 { 1215 "node": { 1216 "id": "ENVOY_NODE_ID", 1217 "metadata": { 1218 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 1219 } 1220 }, 1221 "xds_servers" : [ 1222 { 1223 "server_uri": "trafficdirector.googleapis.com:443", 1224 "channel_creds": [{ "type": "google_default" }], 1225 "server_features": ["xds_v3"] 1226 }, 1227 { 1228 "server_uri": "backup.never.use.com:1234", 1229 "channel_creds": [{ "type": "google_default" }] 1230 } 1231 ], 1232 "authorities": { 1233 "xds.td.com": { 1234 "xds_servers": [ 1235 { 1236 "server_uri": "td.com", 1237 "channel_creds": [ { "type": "google_default" } ], 1238 "server_features" : ["xds_v3"] 1239 }, 1240 { 1241 "server_uri": "backup.never.use.com:1234", 1242 "channel_creds": [{ "type": "google_default" }] 1243 } 1244 ] 1245 } 1246 } 1247 }`, 1248 }) 1249 defer cancel() 1250 1251 wantConfig := &Config{ 1252 xDSServers: []*ServerConfig{{ 1253 serverURI: "trafficdirector.googleapis.com:443", 1254 channelCreds: []ChannelCreds{{Type: "google_default"}}, 1255 serverFeatures: []string{"xds_v3"}, 1256 selectedCreds: ChannelCreds{Type: "google_default"}, 1257 }}, 1258 node: v3Node, 1259 clientDefaultListenerResourceNameTemplate: "%s", 1260 authorities: map[string]*Authority{ 1261 "xds.td.com": { 1262 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 1263 XDSServers: []*ServerConfig{{ 1264 serverURI: "td.com", 1265 channelCreds: []ChannelCreds{{Type: "google_default"}}, 1266 serverFeatures: []string{"xds_v3"}, 1267 selectedCreds: ChannelCreds{Type: "google_default"}, 1268 }}, 1269 }, 1270 }, 1271 } 1272 t.Run("bootstrap_file_name", func(t *testing.T) { 1273 testGetConfigurationWithFileNameEnv(t, "multipleXDSServers", false, wantConfig) 1274 }) 1275 t.Run("bootstrap_file_contents", func(t *testing.T) { 1276 testGetConfigurationWithFileContentEnv(t, "multipleXDSServers", false, wantConfig) 1277 }) 1278 }