google.golang.org/grpc@v1.72.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", "empty", "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 } 393 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) { 398 cancel := setupBootstrapOverride(v3BootstrapFileMap) 399 defer cancel() 400 401 tests := []struct { 402 name string 403 wantConfig *Config 404 }{ 405 { 406 name: "emptyNodeProto", 407 wantConfig: &Config{ 408 xDSServers: []*ServerConfig{{ 409 serverURI: "trafficdirector.googleapis.com:443", 410 channelCreds: []ChannelCreds{{Type: "insecure"}}, 411 selectedCreds: ChannelCreds{Type: "insecure"}, 412 }}, 413 node: node{ 414 userAgentName: gRPCUserAgentName, 415 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 416 clientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 417 }, 418 clientDefaultListenerResourceNameTemplate: "%s", 419 }, 420 }, 421 {"unknownTopLevelFieldInFile", configWithInsecureCreds}, 422 {"unknownFieldInNodeProto", configWithInsecureCreds}, 423 {"unknownFieldInXdsServer", configWithInsecureCreds}, 424 {"multipleChannelCreds", configWithMultipleChannelCredsAndV3}, 425 {"goodBootstrap", configWithGoogleDefaultCredsAndV3}, 426 {"multipleXDSServers", configWithMultipleServers}, 427 {"serverSupportsIgnoreResourceDeletion", configWithGoogleDefaultCredsAndIgnoreResourceDeletion}, 428 } 429 430 for _, test := range tests { 431 t.Run(test.name, func(t *testing.T) { 432 testGetConfigurationWithFileNameEnv(t, test.name, false, test.wantConfig) 433 testGetConfigurationWithFileContentEnv(t, test.name, false, test.wantConfig) 434 }) 435 } 436 } 437 438 // Tests that the two bootstrap env variables are read in correct priority. 439 // 440 // "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap 441 // configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which 442 // directly specifies the bootstrap configuration in itself. 443 func (s) TestGetConfiguration_BootstrapEnvPriority(t *testing.T) { 444 oldFileReadFunc := bootstrapFileReadFunc 445 bootstrapFileReadFunc = func(filename string) ([]byte, error) { 446 return fileReadFromFileMap(v3BootstrapFileMap, filename) 447 } 448 defer func() { bootstrapFileReadFunc = oldFileReadFunc }() 449 450 goodFileName1 := "serverFeaturesIncludesXDSV3" 451 goodConfig1 := configWithGoogleDefaultCredsAndV3 452 453 goodFileName2 := "serverFeaturesExcludesXDSV3" 454 goodFileContent2 := v3BootstrapFileMap[goodFileName2] 455 goodConfig2 := configWithGoogleDefaultCredsAndNoServerFeatures 456 457 origBootstrapFileName := envconfig.XDSBootstrapFileName 458 envconfig.XDSBootstrapFileName = "" 459 defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }() 460 461 origBootstrapContent := envconfig.XDSBootstrapFileContent 462 envconfig.XDSBootstrapFileContent = "" 463 defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }() 464 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") 468 } 469 470 // When one of them is set, it should be used. 471 envconfig.XDSBootstrapFileName = goodFileName1 472 envconfig.XDSBootstrapFileContent = "" 473 c, err := GetConfiguration() 474 if err != nil { 475 t.Errorf("GetConfiguration() failed: %v", err) 476 } 477 if diff := cmp.Diff(goodConfig1, c); diff != "" { 478 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 479 } 480 481 envconfig.XDSBootstrapFileName = "" 482 envconfig.XDSBootstrapFileContent = goodFileContent2 483 c, err = GetConfiguration() 484 if err != nil { 485 t.Errorf("GetConfiguration() failed: %v", err) 486 } 487 if diff := cmp.Diff(goodConfig2, c); diff != "" { 488 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 489 } 490 491 // Set both, file name should be read. 492 envconfig.XDSBootstrapFileName = goodFileName1 493 envconfig.XDSBootstrapFileContent = goodFileContent2 494 c, err = GetConfiguration() 495 if err != nil { 496 t.Errorf("GetConfiguration() failed: %v", err) 497 } 498 if diff := cmp.Diff(goodConfig1, c); diff != "" { 499 t.Errorf("Unexpected diff in bootstrap configuration (-want, +got):\n%s", diff) 500 } 501 } 502 503 func init() { 504 certprovider.Register(&fakeCertProviderBuilder{}) 505 } 506 507 const fakeCertProviderName = "fake-certificate-provider" 508 509 // fakeCertProviderBuilder builds new instances of fakeCertProvider and 510 // interprets the config provided to it as JSON with a single key and value. 511 type fakeCertProviderBuilder struct{} 512 513 // ParseConfig expects input in JSON format containing a map from string to 514 // string, with a single entry and mapKey being "configKey". 515 func (b *fakeCertProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) { 516 config, ok := cfg.(json.RawMessage) 517 if !ok { 518 return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config) 519 } 520 var cfgData map[string]string 521 if err := json.Unmarshal(config, &cfgData); err != nil { 522 return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err) 523 } 524 if len(cfgData) != 1 || cfgData["configKey"] == "" { 525 return nil, errors.New("fakeCertProviderBuilder received invalid config") 526 } 527 fc := &fakeStableConfig{config: cfgData} 528 return certprovider.NewBuildableConfig(fakeCertProviderName, fc.canonical(), func(certprovider.BuildOptions) certprovider.Provider { 529 return &fakeCertProvider{} 530 }), nil 531 } 532 533 func (b *fakeCertProviderBuilder) Name() string { 534 return fakeCertProviderName 535 } 536 537 type fakeStableConfig struct { 538 config map[string]string 539 } 540 541 func (c *fakeStableConfig) canonical() []byte { 542 var cfg string 543 for k, v := range c.config { 544 cfg = fmt.Sprintf("%s:%s", k, v) 545 } 546 return []byte(cfg) 547 } 548 549 // fakeCertProvider is an empty implementation of the Provider interface. 550 type fakeCertProvider struct { 551 certprovider.Provider 552 } 553 554 func (s) TestGetConfiguration_CertificateProviders(t *testing.T) { 555 bootstrapFileMap := map[string]string{ 556 "badJSONCertProviderConfig": ` 557 { 558 "node": { 559 "id": "ENVOY_NODE_ID", 560 "metadata": { 561 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 562 } 563 }, 564 "xds_servers" : [{ 565 "server_uri": "trafficdirector.googleapis.com:443", 566 "channel_creds": [ 567 { "type": "google_default" } 568 ], 569 "server_features" : ["foo", "bar", "xds_v3"], 570 }], 571 "certificate_providers": "bad JSON" 572 }`, 573 "allUnknownCertProviders": ` 574 { 575 "node": { 576 "id": "ENVOY_NODE_ID", 577 "metadata": { 578 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 579 } 580 }, 581 "xds_servers" : [{ 582 "server_uri": "trafficdirector.googleapis.com:443", 583 "channel_creds": [ 584 { "type": "google_default" } 585 ], 586 "server_features" : ["xds_v3"] 587 }], 588 "certificate_providers": { 589 "unknownProviderInstance1": { 590 "plugin_name": "foo", 591 "config": {"foo": "bar"} 592 }, 593 "unknownProviderInstance2": { 594 "plugin_name": "bar", 595 "config": {"foo": "bar"} 596 } 597 } 598 }`, 599 "badCertProviderConfig": ` 600 { 601 "node": { 602 "id": "ENVOY_NODE_ID", 603 "metadata": { 604 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 605 } 606 }, 607 "xds_servers" : [{ 608 "server_uri": "trafficdirector.googleapis.com:443", 609 "channel_creds": [ 610 { "type": "google_default" } 611 ], 612 "server_features" : ["xds_v3"], 613 }], 614 "certificate_providers": { 615 "unknownProviderInstance": { 616 "plugin_name": "foo", 617 "config": {"foo": "bar"} 618 }, 619 "fakeProviderInstanceBad": { 620 "plugin_name": "fake-certificate-provider", 621 "config": {"configKey": 666} 622 } 623 } 624 }`, 625 "goodCertProviderConfig": ` 626 { 627 "node": { 628 "id": "ENVOY_NODE_ID", 629 "metadata": { 630 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 631 } 632 }, 633 "xds_servers" : [{ 634 "server_uri": "trafficdirector.googleapis.com:443", 635 "channel_creds": [ 636 { "type": "insecure" } 637 ], 638 "server_features" : ["xds_v3"] 639 }], 640 "certificate_providers": { 641 "unknownProviderInstance": { 642 "plugin_name": "foo", 643 "config": {"foo": "bar"} 644 }, 645 "fakeProviderInstance": { 646 "plugin_name": "fake-certificate-provider", 647 "config": {"configKey": "configValue"} 648 } 649 } 650 }`, 651 } 652 653 getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder) 654 parser := getBuilder(fakeCertProviderName) 655 if parser == nil { 656 t.Fatalf("Missing certprovider plugin %q", fakeCertProviderName) 657 } 658 wantCfg, err := parser.ParseConfig(json.RawMessage(`{"configKey": "configValue"}`)) 659 if err != nil { 660 t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err) 661 } 662 663 cancel := setupBootstrapOverride(bootstrapFileMap) 664 defer cancel() 665 666 goodConfig := &Config{ 667 xDSServers: []*ServerConfig{{ 668 serverURI: "trafficdirector.googleapis.com:443", 669 channelCreds: []ChannelCreds{{Type: "insecure"}}, 670 serverFeatures: []string{"xds_v3"}, 671 selectedCreds: ChannelCreds{Type: "insecure"}, 672 }}, 673 certProviderConfigs: map[string]*certprovider.BuildableConfig{ 674 "fakeProviderInstance": wantCfg, 675 }, 676 clientDefaultListenerResourceNameTemplate: "%s", 677 node: v3Node, 678 } 679 tests := []struct { 680 name string 681 wantConfig *Config 682 wantErr bool 683 }{ 684 { 685 name: "badJSONCertProviderConfig", 686 wantErr: true, 687 }, 688 { 689 690 name: "badCertProviderConfig", 691 wantErr: true, 692 }, 693 { 694 695 name: "allUnknownCertProviders", 696 wantConfig: configWithGoogleDefaultCredsAndV3, 697 }, 698 { 699 name: "goodCertProviderConfig", 700 wantConfig: goodConfig, 701 }, 702 } 703 704 for _, test := range tests { 705 t.Run(test.name, func(t *testing.T) { 706 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 707 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 708 }) 709 } 710 } 711 712 func (s) TestGetConfiguration_ServerListenerResourceNameTemplate(t *testing.T) { 713 cancel := setupBootstrapOverride(map[string]string{ 714 "badServerListenerResourceNameTemplate:": ` 715 { 716 "node": { 717 "id": "ENVOY_NODE_ID", 718 "metadata": { 719 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 720 } 721 }, 722 "xds_servers" : [{ 723 "server_uri": "trafficdirector.googleapis.com:443", 724 "channel_creds": [ 725 { "type": "google_default" } 726 ] 727 }], 728 "server_listener_resource_name_template": 123456789 729 }`, 730 "goodServerListenerResourceNameTemplate": ` 731 { 732 "node": { 733 "id": "ENVOY_NODE_ID", 734 "metadata": { 735 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 736 } 737 }, 738 "xds_servers" : [{ 739 "server_uri": "trafficdirector.googleapis.com:443", 740 "channel_creds": [ 741 { "type": "google_default" } 742 ] 743 }], 744 "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s" 745 }`, 746 }) 747 defer cancel() 748 749 tests := []struct { 750 name string 751 wantConfig *Config 752 wantErr bool 753 }{ 754 { 755 name: "badServerListenerResourceNameTemplate", 756 wantErr: true, 757 }, 758 { 759 name: "goodServerListenerResourceNameTemplate", 760 wantConfig: &Config{ 761 xDSServers: []*ServerConfig{{ 762 serverURI: "trafficdirector.googleapis.com:443", 763 channelCreds: []ChannelCreds{{Type: "google_default"}}, 764 selectedCreds: ChannelCreds{Type: "google_default"}, 765 }}, 766 node: v3Node, 767 serverListenerResourceNameTemplate: "grpc/server?xds.resource.listening_address=%s", 768 clientDefaultListenerResourceNameTemplate: "%s", 769 }, 770 }, 771 } 772 773 for _, test := range tests { 774 t.Run(test.name, func(t *testing.T) { 775 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 776 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 777 }) 778 } 779 } 780 781 func (s) TestGetConfiguration_Federation(t *testing.T) { 782 cancel := setupBootstrapOverride(map[string]string{ 783 "badclientListenerResourceNameTemplate": ` 784 { 785 "node": { "id": "ENVOY_NODE_ID" }, 786 "xds_servers" : [{ 787 "server_uri": "trafficdirector.googleapis.com:443" 788 }], 789 "client_default_listener_resource_name_template": 123456789 790 }`, 791 "badclientListenerResourceNameTemplatePerAuthority": ` 792 { 793 "node": { "id": "ENVOY_NODE_ID" }, 794 "xds_servers" : [{ 795 "server_uri": "trafficdirector.googleapis.com:443", 796 "channel_creds": [ { "type": "google_default" } ] 797 }], 798 "authorities": { 799 "xds.td.com": { 800 "client_listener_resource_name_template": "some/template/%s", 801 "xds_servers": [{ 802 "server_uri": "td.com", 803 "channel_creds": [ { "type": "google_default" } ], 804 "server_features" : ["foo", "bar", "xds_v3"] 805 }] 806 } 807 } 808 }`, 809 "good": ` 810 { 811 "node": { 812 "id": "ENVOY_NODE_ID", 813 "metadata": { 814 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 815 } 816 }, 817 "xds_servers" : [{ 818 "server_uri": "trafficdirector.googleapis.com:443", 819 "channel_creds": [ { "type": "google_default" } ] 820 }], 821 "server_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s", 822 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 823 "authorities": { 824 "xds.td.com": { 825 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 826 "xds_servers": [{ 827 "server_uri": "td.com", 828 "channel_creds": [ { "type": "google_default" } ], 829 "server_features" : ["xds_v3"] 830 }] 831 } 832 } 833 }`, 834 // If client_default_listener_resource_name_template is not set, it 835 // defaults to "%s". 836 "goodWithDefaultDefaultClientListenerTemplate": ` 837 { 838 "node": { 839 "id": "ENVOY_NODE_ID", 840 "metadata": { 841 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 842 } 843 }, 844 "xds_servers" : [{ 845 "server_uri": "trafficdirector.googleapis.com:443", 846 "channel_creds": [ { "type": "google_default" } ] 847 }] 848 }`, 849 // If client_listener_resource_name_template in authority is not set, it 850 // defaults to 851 // "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s". 852 "goodWithDefaultClientListenerTemplatePerAuthority": ` 853 { 854 "node": { 855 "id": "ENVOY_NODE_ID", 856 "metadata": { 857 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 858 } 859 }, 860 "xds_servers" : [{ 861 "server_uri": "trafficdirector.googleapis.com:443", 862 "channel_creds": [ { "type": "google_default" } ] 863 }], 864 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 865 "authorities": { 866 "xds.td.com": { }, 867 "#.com": { } 868 } 869 }`, 870 // It's OK for an authority to not have servers. The top-level server 871 // will be used. 872 "goodWithNoServerPerAuthority": ` 873 { 874 "node": { 875 "id": "ENVOY_NODE_ID", 876 "metadata": { 877 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 878 } 879 }, 880 "xds_servers" : [{ 881 "server_uri": "trafficdirector.googleapis.com:443", 882 "channel_creds": [ { "type": "google_default" } ] 883 }], 884 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 885 "authorities": { 886 "xds.td.com": { 887 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s" 888 } 889 } 890 }`, 891 }) 892 defer cancel() 893 894 tests := []struct { 895 name string 896 wantConfig *Config 897 wantErr bool 898 }{ 899 { 900 name: "badclientListenerResourceNameTemplate", 901 wantErr: true, 902 }, 903 { 904 name: "badclientListenerResourceNameTemplatePerAuthority", 905 wantErr: true, 906 }, 907 { 908 name: "good", 909 wantConfig: &Config{ 910 xDSServers: []*ServerConfig{{ 911 serverURI: "trafficdirector.googleapis.com:443", 912 channelCreds: []ChannelCreds{{Type: "google_default"}}, 913 selectedCreds: ChannelCreds{Type: "google_default"}, 914 }}, 915 node: v3Node, 916 serverListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s", 917 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 918 authorities: map[string]*Authority{ 919 "xds.td.com": { 920 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 921 XDSServers: []*ServerConfig{{ 922 serverURI: "td.com", 923 channelCreds: []ChannelCreds{{Type: "google_default"}}, 924 serverFeatures: []string{"xds_v3"}, 925 selectedCreds: ChannelCreds{Type: "google_default"}, 926 }}, 927 }, 928 }, 929 }, 930 }, 931 { 932 name: "goodWithDefaultDefaultClientListenerTemplate", 933 wantConfig: &Config{ 934 xDSServers: []*ServerConfig{{ 935 serverURI: "trafficdirector.googleapis.com:443", 936 channelCreds: []ChannelCreds{{Type: "google_default"}}, 937 selectedCreds: ChannelCreds{Type: "google_default"}, 938 }}, 939 node: v3Node, 940 clientDefaultListenerResourceNameTemplate: "%s", 941 }, 942 }, 943 { 944 name: "goodWithDefaultClientListenerTemplatePerAuthority", 945 wantConfig: &Config{ 946 xDSServers: []*ServerConfig{{ 947 serverURI: "trafficdirector.googleapis.com:443", 948 channelCreds: []ChannelCreds{{Type: "google_default"}}, 949 selectedCreds: ChannelCreds{Type: "google_default"}, 950 }}, 951 node: v3Node, 952 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 953 authorities: map[string]*Authority{ 954 "xds.td.com": { 955 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 956 }, 957 "#.com": { 958 ClientListenerResourceNameTemplate: "xdstp://%23.com/envoy.config.listener.v3.Listener/%s", 959 }, 960 }, 961 }, 962 }, 963 { 964 name: "goodWithNoServerPerAuthority", 965 wantConfig: &Config{ 966 xDSServers: []*ServerConfig{{ 967 serverURI: "trafficdirector.googleapis.com:443", 968 channelCreds: []ChannelCreds{{Type: "google_default"}}, 969 selectedCreds: ChannelCreds{Type: "google_default"}, 970 }}, 971 node: v3Node, 972 clientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 973 authorities: map[string]*Authority{ 974 "xds.td.com": { 975 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 976 }, 977 }, 978 }, 979 }, 980 } 981 982 for _, test := range tests { 983 t.Run(test.name, func(t *testing.T) { 984 testGetConfigurationWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 985 testGetConfigurationWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 986 }) 987 } 988 } 989 990 func (s) TestServerConfigMarshalAndUnmarshal(t *testing.T) { 991 origConfig, err := ServerConfigForTesting(ServerConfigTestingOptions{URI: "test-server", ServerFeatures: []string{"xds_v3"}}) 992 if err != nil { 993 t.Fatalf("Failed to create server config for testing: %v", err) 994 } 995 marshaledCfg, err := json.Marshal(origConfig) 996 if err != nil { 997 t.Fatalf("failed to marshal: %v", err) 998 } 999 1000 unmarshaledConfig := new(ServerConfig) 1001 if err := json.Unmarshal(marshaledCfg, unmarshaledConfig); err != nil { 1002 t.Fatalf("failed to unmarshal: %v", err) 1003 } 1004 if diff := cmp.Diff(origConfig, unmarshaledConfig); diff != "" { 1005 t.Fatalf("Unexpected diff in server config (-want, +got):\n%s", diff) 1006 } 1007 } 1008 1009 func (s) TestDefaultBundles(t *testing.T) { 1010 tests := []string{"google_default", "insecure", "tls"} 1011 1012 for _, typename := range tests { 1013 t.Run(typename, func(t *testing.T) { 1014 if c := bootstrap.GetCredentials(typename); c == nil { 1015 t.Errorf(`bootstrap.GetCredentials(%s) credential is nil, want non-nil`, typename) 1016 } 1017 }) 1018 } 1019 } 1020 1021 type s struct { 1022 grpctest.Tester 1023 } 1024 1025 func Test(t *testing.T) { 1026 grpctest.RunSubTests(t, s{}) 1027 } 1028 1029 func newStructProtoFromMap(t *testing.T, input map[string]any) *structpb.Struct { 1030 t.Helper() 1031 1032 ret, err := structpb.NewStruct(input) 1033 if err != nil { 1034 t.Fatalf("Failed to create new struct proto from map %v: %v", input, err) 1035 } 1036 return ret 1037 } 1038 1039 func (s) TestNode_MarshalAndUnmarshal(t *testing.T) { 1040 tests := []struct { 1041 desc string 1042 inputJSON []byte 1043 wantNode node 1044 }{ 1045 { 1046 desc: "basic happy case", 1047 inputJSON: []byte(`{ 1048 "id": "id", 1049 "cluster": "cluster", 1050 "locality": { 1051 "region": "region", 1052 "zone": "zone", 1053 "sub_zone": "sub_zone" 1054 }, 1055 "metadata": { 1056 "k1": "v1", 1057 "k2": 101, 1058 "k3": 280.0 1059 } 1060 }`), 1061 wantNode: node{ 1062 ID: "id", 1063 Cluster: "cluster", 1064 Locality: locality{ 1065 Region: "region", 1066 Zone: "zone", 1067 SubZone: "sub_zone", 1068 }, 1069 Metadata: newStructProtoFromMap(t, map[string]any{ 1070 "k1": "v1", 1071 "k2": 101, 1072 "k3": 280.0, 1073 }), 1074 userAgentName: "gRPC Go", 1075 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 1076 clientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1077 }, 1078 }, 1079 { 1080 desc: "client controlled fields", 1081 inputJSON: []byte(`{ 1082 "id": "id", 1083 "cluster": "cluster", 1084 "user_agent_name": "user_agent_name", 1085 "user_agent_version_type": { 1086 "user_agent_version": "version" 1087 }, 1088 "client_features": ["feature1", "feature2"] 1089 }`), 1090 wantNode: node{ 1091 ID: "id", 1092 Cluster: "cluster", 1093 userAgentName: "gRPC Go", 1094 userAgentVersionType: userAgentVersion{UserAgentVersion: grpc.Version}, 1095 clientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1096 }, 1097 }, 1098 } 1099 1100 for _, test := range tests { 1101 t.Run(test.desc, func(t *testing.T) { 1102 // Unmarshal the input JSON into a node struct and check if it 1103 // matches expectations. 1104 unmarshaledNode := newNode() 1105 if err := json.Unmarshal([]byte(test.inputJSON), &unmarshaledNode); err != nil { 1106 t.Fatal(err) 1107 } 1108 if diff := cmp.Diff(test.wantNode, unmarshaledNode); diff != "" { 1109 t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff) 1110 } 1111 1112 // Marshal the recently unmarshaled node struct into JSON and 1113 // remarshal it into another node struct, and check that it still 1114 // matches expectations. 1115 marshaledJSON, err := json.Marshal(unmarshaledNode) 1116 if err != nil { 1117 t.Fatalf("node.MarshalJSON() failed: %v", err) 1118 } 1119 reUnmarshaledNode := newNode() 1120 if err := json.Unmarshal([]byte(marshaledJSON), &reUnmarshaledNode); err != nil { 1121 t.Fatal(err) 1122 } 1123 if diff := cmp.Diff(test.wantNode, reUnmarshaledNode); diff != "" { 1124 t.Fatalf("Unexpected diff in node: (-want, +got):\n%s", diff) 1125 } 1126 }) 1127 } 1128 } 1129 1130 func (s) TestNode_ToProto(t *testing.T) { 1131 tests := []struct { 1132 desc string 1133 inputNode node 1134 wantProto *v3corepb.Node 1135 }{ 1136 { 1137 desc: "all fields set", 1138 inputNode: func() node { 1139 n := newNode() 1140 n.ID = "id" 1141 n.Cluster = "cluster" 1142 n.Locality = locality{ 1143 Region: "region", 1144 Zone: "zone", 1145 SubZone: "sub_zone", 1146 } 1147 n.Metadata = newStructProtoFromMap(t, map[string]any{ 1148 "k1": "v1", 1149 "k2": 101, 1150 "k3": 280.0, 1151 }) 1152 return n 1153 }(), 1154 wantProto: &v3corepb.Node{ 1155 Id: "id", 1156 Cluster: "cluster", 1157 Locality: &v3corepb.Locality{ 1158 Region: "region", 1159 Zone: "zone", 1160 SubZone: "sub_zone", 1161 }, 1162 Metadata: newStructProtoFromMap(t, map[string]any{ 1163 "k1": "v1", 1164 "k2": 101, 1165 "k3": 280.0, 1166 }), 1167 UserAgentName: "gRPC Go", 1168 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 1169 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1170 }, 1171 }, 1172 { 1173 desc: "some fields unset", 1174 inputNode: func() node { 1175 n := newNode() 1176 n.ID = "id" 1177 return n 1178 }(), 1179 wantProto: &v3corepb.Node{ 1180 Id: "id", 1181 UserAgentName: "gRPC Go", 1182 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 1183 ClientFeatures: []string{"envoy.lb.does_not_support_overprovisioning", "xds.config.resource-in-sotw"}, 1184 }, 1185 }, 1186 } 1187 1188 for _, test := range tests { 1189 t.Run(test.desc, func(t *testing.T) { 1190 gotProto := test.inputNode.toProto() 1191 if diff := cmp.Diff(test.wantProto, gotProto, protocmp.Transform()); diff != "" { 1192 t.Fatalf("Unexpected diff in node proto: (-want, +got):\n%s", diff) 1193 } 1194 }) 1195 } 1196 } 1197 1198 // Tests the case where the xDS fallback env var is set to false, and verifies 1199 // that only the first server from the list of server configurations is used. 1200 func (s) TestGetConfiguration_FallbackDisabled(t *testing.T) { 1201 origFallbackEnv := envconfig.XDSFallbackSupport 1202 envconfig.XDSFallbackSupport = false 1203 defer func() { envconfig.XDSFallbackSupport = origFallbackEnv }() 1204 1205 cancel := setupBootstrapOverride(map[string]string{ 1206 "multipleXDSServers": ` 1207 { 1208 "node": { 1209 "id": "ENVOY_NODE_ID", 1210 "metadata": { 1211 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 1212 } 1213 }, 1214 "xds_servers" : [ 1215 { 1216 "server_uri": "trafficdirector.googleapis.com:443", 1217 "channel_creds": [{ "type": "google_default" }], 1218 "server_features": ["xds_v3"] 1219 }, 1220 { 1221 "server_uri": "backup.never.use.com:1234", 1222 "channel_creds": [{ "type": "google_default" }] 1223 } 1224 ], 1225 "authorities": { 1226 "xds.td.com": { 1227 "xds_servers": [ 1228 { 1229 "server_uri": "td.com", 1230 "channel_creds": [ { "type": "google_default" } ], 1231 "server_features" : ["xds_v3"] 1232 }, 1233 { 1234 "server_uri": "backup.never.use.com:1234", 1235 "channel_creds": [{ "type": "google_default" }] 1236 } 1237 ] 1238 } 1239 } 1240 }`, 1241 }) 1242 defer cancel() 1243 1244 wantConfig := &Config{ 1245 xDSServers: []*ServerConfig{{ 1246 serverURI: "trafficdirector.googleapis.com:443", 1247 channelCreds: []ChannelCreds{{Type: "google_default"}}, 1248 serverFeatures: []string{"xds_v3"}, 1249 selectedCreds: ChannelCreds{Type: "google_default"}, 1250 }}, 1251 node: v3Node, 1252 clientDefaultListenerResourceNameTemplate: "%s", 1253 authorities: map[string]*Authority{ 1254 "xds.td.com": { 1255 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 1256 XDSServers: []*ServerConfig{{ 1257 serverURI: "td.com", 1258 channelCreds: []ChannelCreds{{Type: "google_default"}}, 1259 serverFeatures: []string{"xds_v3"}, 1260 selectedCreds: ChannelCreds{Type: "google_default"}, 1261 }}, 1262 }, 1263 }, 1264 } 1265 t.Run("bootstrap_file_name", func(t *testing.T) { 1266 testGetConfigurationWithFileNameEnv(t, "multipleXDSServers", false, wantConfig) 1267 }) 1268 t.Run("bootstrap_file_contents", func(t *testing.T) { 1269 testGetConfigurationWithFileContentEnv(t, "multipleXDSServers", false, wantConfig) 1270 }) 1271 }