google.golang.org/grpc@v1.62.1/xds/internal/xdsclient/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 "github.com/google/go-cmp/cmp" 29 "github.com/google/go-cmp/cmp/cmpopts" 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/xds/bootstrap" 35 "google.golang.org/protobuf/proto" 36 "google.golang.org/protobuf/types/known/structpb" 37 38 v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 39 ) 40 41 var ( 42 v3BootstrapFileMap = map[string]string{ 43 "serverFeaturesIncludesXDSV3": ` 44 { 45 "node": { 46 "id": "ENVOY_NODE_ID", 47 "metadata": { 48 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 49 } 50 }, 51 "xds_servers" : [{ 52 "server_uri": "trafficdirector.googleapis.com:443", 53 "channel_creds": [ 54 { "type": "google_default" } 55 ], 56 "server_features" : ["xds_v3"] 57 }] 58 }`, 59 "serverFeaturesExcludesXDSV3": ` 60 { 61 "node": { 62 "id": "ENVOY_NODE_ID", 63 "metadata": { 64 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 65 } 66 }, 67 "xds_servers" : [{ 68 "server_uri": "trafficdirector.googleapis.com:443", 69 "channel_creds": [ 70 { "type": "google_default" } 71 ] 72 }] 73 }`, 74 "emptyNodeProto": ` 75 { 76 "xds_servers" : [{ 77 "server_uri": "trafficdirector.googleapis.com:443", 78 "channel_creds": [ 79 { "type": "insecure" } 80 ] 81 }] 82 }`, 83 "unknownTopLevelFieldInFile": ` 84 { 85 "node": { 86 "id": "ENVOY_NODE_ID", 87 "metadata": { 88 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 89 } 90 }, 91 "xds_servers" : [{ 92 "server_uri": "trafficdirector.googleapis.com:443", 93 "channel_creds": [ 94 { "type": "insecure" } 95 ] 96 }], 97 "unknownField": "foobar" 98 }`, 99 "unknownFieldInNodeProto": ` 100 { 101 "node": { 102 "id": "ENVOY_NODE_ID", 103 "unknownField": "foobar", 104 "metadata": { 105 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 106 } 107 }, 108 "xds_servers" : [{ 109 "server_uri": "trafficdirector.googleapis.com:443", 110 "channel_creds": [ 111 { "type": "insecure" } 112 ] 113 }] 114 }`, 115 "unknownFieldInXdsServer": ` 116 { 117 "node": { 118 "id": "ENVOY_NODE_ID", 119 "metadata": { 120 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 121 } 122 }, 123 "xds_servers" : [{ 124 "server_uri": "trafficdirector.googleapis.com:443", 125 "channel_creds": [ 126 { "type": "insecure" } 127 ], 128 "unknownField": "foobar" 129 }] 130 }`, 131 "multipleChannelCreds": ` 132 { 133 "node": { 134 "id": "ENVOY_NODE_ID", 135 "metadata": { 136 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 137 } 138 }, 139 "xds_servers" : [{ 140 "server_uri": "trafficdirector.googleapis.com:443", 141 "channel_creds": [ 142 { "type": "not-google-default" }, 143 { "type": "google_default" } 144 ], 145 "server_features": ["xds_v3"] 146 }] 147 }`, 148 "goodBootstrap": ` 149 { 150 "node": { 151 "id": "ENVOY_NODE_ID", 152 "metadata": { 153 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 154 } 155 }, 156 "xds_servers" : [{ 157 "server_uri": "trafficdirector.googleapis.com:443", 158 "channel_creds": [ 159 { "type": "google_default" } 160 ], 161 "server_features": ["xds_v3"] 162 }] 163 }`, 164 "multipleXDSServers": ` 165 { 166 "node": { 167 "id": "ENVOY_NODE_ID", 168 "metadata": { 169 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 170 } 171 }, 172 "xds_servers" : [ 173 { 174 "server_uri": "trafficdirector.googleapis.com:443", 175 "channel_creds": [{ "type": "google_default" }], 176 "server_features": ["xds_v3"] 177 }, 178 { 179 "server_uri": "backup.never.use.com:1234", 180 "channel_creds": [{ "type": "not-google-default" }] 181 } 182 ] 183 }`, 184 "serverSupportsIgnoreResourceDeletion": ` 185 { 186 "node": { 187 "id": "ENVOY_NODE_ID", 188 "metadata": { 189 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 190 } 191 }, 192 "xds_servers" : [{ 193 "server_uri": "trafficdirector.googleapis.com:443", 194 "channel_creds": [ 195 { "type": "google_default" } 196 ], 197 "server_features" : ["ignore_resource_deletion", "xds_v3"] 198 }] 199 }`, 200 } 201 metadata = &structpb.Struct{ 202 Fields: map[string]*structpb.Value{ 203 "TRAFFICDIRECTOR_GRPC_HOSTNAME": { 204 Kind: &structpb.Value_StringValue{StringValue: "trafficdirector"}, 205 }, 206 }, 207 } 208 v3NodeProto = &v3corepb.Node{ 209 Id: "ENVOY_NODE_ID", 210 Metadata: metadata, 211 UserAgentName: gRPCUserAgentName, 212 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 213 ClientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 214 } 215 nilCredsConfigNoServerFeatures = &Config{ 216 XDSServer: &ServerConfig{ 217 ServerURI: "trafficdirector.googleapis.com:443", 218 Creds: ChannelCreds{Type: "insecure"}, 219 }, 220 NodeProto: v3NodeProto, 221 ClientDefaultListenerResourceNameTemplate: "%s", 222 } 223 nonNilCredsConfigV3 = &Config{ 224 XDSServer: &ServerConfig{ 225 ServerURI: "trafficdirector.googleapis.com:443", 226 Creds: ChannelCreds{Type: "google_default"}, 227 ServerFeatures: []string{"xds_v3"}, 228 }, 229 NodeProto: v3NodeProto, 230 ClientDefaultListenerResourceNameTemplate: "%s", 231 } 232 nonNilCredsConfigWithDeletionIgnored = &Config{ 233 XDSServer: &ServerConfig{ 234 ServerURI: "trafficdirector.googleapis.com:443", 235 Creds: ChannelCreds{Type: "google_default"}, 236 IgnoreResourceDeletion: true, 237 ServerFeatures: []string{"ignore_resource_deletion", "xds_v3"}, 238 }, 239 NodeProto: v3NodeProto, 240 ClientDefaultListenerResourceNameTemplate: "%s", 241 } 242 nonNilCredsConfigNoServerFeatures = &Config{ 243 XDSServer: &ServerConfig{ 244 ServerURI: "trafficdirector.googleapis.com:443", 245 Creds: ChannelCreds{Type: "google_default"}, 246 }, 247 NodeProto: v3NodeProto, 248 ClientDefaultListenerResourceNameTemplate: "%s", 249 } 250 ) 251 252 func (c *Config) compare(want *Config) error { 253 if diff := cmp.Diff(want, c, 254 cmpopts.EquateEmpty(), 255 cmp.Comparer(proto.Equal), 256 cmp.Comparer(func(a, b grpc.DialOption) bool { return (a != nil) == (b != nil) }), 257 cmp.Transformer("certproviderconfigstring", func(a *certprovider.BuildableConfig) string { return a.String() }), 258 ); diff != "" { 259 return fmt.Errorf("unexpected diff in config (-want, +got):\n%s", diff) 260 } 261 return nil 262 } 263 264 func fileReadFromFileMap(bootstrapFileMap map[string]string, name string) ([]byte, error) { 265 if b, ok := bootstrapFileMap[name]; ok { 266 return []byte(b), nil 267 } 268 return nil, os.ErrNotExist 269 } 270 271 func setupBootstrapOverride(bootstrapFileMap map[string]string) func() { 272 oldFileReadFunc := bootstrapFileReadFunc 273 bootstrapFileReadFunc = func(filename string) ([]byte, error) { 274 return fileReadFromFileMap(bootstrapFileMap, filename) 275 } 276 return func() { bootstrapFileReadFunc = oldFileReadFunc } 277 } 278 279 // TODO: enable leak check for this package when 280 // https://github.com/googleapis/google-cloud-go/issues/2417 is fixed. 281 282 // This function overrides the bootstrap file NAME env variable, to test the 283 // code that reads file with the given fileName. 284 func testNewConfigWithFileNameEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) { 285 origBootstrapFileName := envconfig.XDSBootstrapFileName 286 envconfig.XDSBootstrapFileName = fileName 287 defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }() 288 289 c, err := NewConfig() 290 if (err != nil) != wantError { 291 t.Fatalf("NewConfig() returned error %v, wantError: %v", err, wantError) 292 } 293 if wantError { 294 return 295 } 296 if err := c.compare(wantConfig); err != nil { 297 t.Fatal(err) 298 } 299 } 300 301 // This function overrides the bootstrap file CONTENT env variable, to test the 302 // code that uses the content from env directly. 303 func testNewConfigWithFileContentEnv(t *testing.T, fileName string, wantError bool, wantConfig *Config) { 304 t.Helper() 305 b, err := bootstrapFileReadFunc(fileName) 306 if err != nil { 307 t.Skip(err) 308 } 309 origBootstrapContent := envconfig.XDSBootstrapFileContent 310 envconfig.XDSBootstrapFileContent = string(b) 311 defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }() 312 313 c, err := NewConfig() 314 if (err != nil) != wantError { 315 t.Fatalf("NewConfig() returned error %v, wantError: %v", err, wantError) 316 } 317 if wantError { 318 return 319 } 320 if err := c.compare(wantConfig); err != nil { 321 t.Fatal(err) 322 } 323 } 324 325 // TestNewConfigV3ProtoFailure exercises the functionality in NewConfig with 326 // different bootstrap file contents which are expected to fail. 327 func TestNewConfigV3ProtoFailure(t *testing.T) { 328 bootstrapFileMap := map[string]string{ 329 "empty": "", 330 "badJSON": `["test": 123]`, 331 "noBalancerName": `{"node": {"id": "ENVOY_NODE_ID"}}`, 332 "emptyXdsServer": ` 333 { 334 "node": { 335 "id": "ENVOY_NODE_ID", 336 "metadata": { 337 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 338 } 339 } 340 }`, 341 "emptyChannelCreds": ` 342 { 343 "node": { 344 "id": "ENVOY_NODE_ID", 345 "metadata": { 346 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 347 } 348 }, 349 "xds_servers" : [{ 350 "server_uri": "trafficdirector.googleapis.com:443" 351 }] 352 }`, 353 "nonGoogleDefaultCreds": ` 354 { 355 "node": { 356 "id": "ENVOY_NODE_ID", 357 "metadata": { 358 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 359 } 360 }, 361 "xds_servers" : [{ 362 "server_uri": "trafficdirector.googleapis.com:443", 363 "channel_creds": [ 364 { "type": "not-google-default" } 365 ] 366 }] 367 }`, 368 } 369 cancel := setupBootstrapOverride(bootstrapFileMap) 370 defer cancel() 371 372 tests := []struct { 373 name string 374 wantError bool 375 }{ 376 {"nonExistentBootstrapFile", true}, 377 {"empty", true}, 378 {"badJSON", true}, 379 {"noBalancerName", true}, 380 {"emptyXdsServer", true}, 381 } 382 383 for _, test := range tests { 384 t.Run(test.name, func(t *testing.T) { 385 testNewConfigWithFileNameEnv(t, test.name, true, nil) 386 testNewConfigWithFileContentEnv(t, test.name, true, nil) 387 }) 388 } 389 } 390 391 // TestNewConfigV3ProtoSuccess exercises the functionality in NewConfig with 392 // different bootstrap file contents. It overrides the fileReadFunc by returning 393 // bootstrap file contents defined in this test, instead of reading from a file. 394 func TestNewConfigV3ProtoSuccess(t *testing.T) { 395 cancel := setupBootstrapOverride(v3BootstrapFileMap) 396 defer cancel() 397 398 tests := []struct { 399 name string 400 wantConfig *Config 401 }{ 402 { 403 "emptyNodeProto", &Config{ 404 XDSServer: &ServerConfig{ 405 ServerURI: "trafficdirector.googleapis.com:443", 406 Creds: ChannelCreds{Type: "insecure"}, 407 }, 408 NodeProto: &v3corepb.Node{ 409 UserAgentName: gRPCUserAgentName, 410 UserAgentVersionType: &v3corepb.Node_UserAgentVersion{UserAgentVersion: grpc.Version}, 411 ClientFeatures: []string{clientFeatureNoOverprovisioning, clientFeatureResourceWrapper}, 412 }, 413 ClientDefaultListenerResourceNameTemplate: "%s", 414 }, 415 }, 416 {"unknownTopLevelFieldInFile", nilCredsConfigNoServerFeatures}, 417 {"unknownFieldInNodeProto", nilCredsConfigNoServerFeatures}, 418 {"unknownFieldInXdsServer", nilCredsConfigNoServerFeatures}, 419 {"multipleChannelCreds", nonNilCredsConfigV3}, 420 {"goodBootstrap", nonNilCredsConfigV3}, 421 {"multipleXDSServers", nonNilCredsConfigV3}, 422 {"serverSupportsIgnoreResourceDeletion", nonNilCredsConfigWithDeletionIgnored}, 423 } 424 425 for _, test := range tests { 426 t.Run(test.name, func(t *testing.T) { 427 testNewConfigWithFileNameEnv(t, test.name, false, test.wantConfig) 428 testNewConfigWithFileContentEnv(t, test.name, false, test.wantConfig) 429 }) 430 } 431 } 432 433 // TestNewConfigBootstrapEnvPriority tests that the two env variables are read 434 // in correct priority. 435 // 436 // "GRPC_XDS_BOOTSTRAP" which specifies the file name containing the bootstrap 437 // configuration takes precedence over "GRPC_XDS_BOOTSTRAP_CONFIG", which 438 // directly specifies the bootstrap configuration in itself. 439 func TestNewConfigBootstrapEnvPriority(t *testing.T) { 440 oldFileReadFunc := bootstrapFileReadFunc 441 bootstrapFileReadFunc = func(filename string) ([]byte, error) { 442 return fileReadFromFileMap(v3BootstrapFileMap, filename) 443 } 444 defer func() { bootstrapFileReadFunc = oldFileReadFunc }() 445 446 goodFileName1 := "serverFeaturesIncludesXDSV3" 447 goodConfig1 := nonNilCredsConfigV3 448 449 goodFileName2 := "serverFeaturesExcludesXDSV3" 450 goodFileContent2 := v3BootstrapFileMap[goodFileName2] 451 goodConfig2 := nonNilCredsConfigNoServerFeatures 452 453 origBootstrapFileName := envconfig.XDSBootstrapFileName 454 envconfig.XDSBootstrapFileName = "" 455 defer func() { envconfig.XDSBootstrapFileName = origBootstrapFileName }() 456 457 origBootstrapContent := envconfig.XDSBootstrapFileContent 458 envconfig.XDSBootstrapFileContent = "" 459 defer func() { envconfig.XDSBootstrapFileContent = origBootstrapContent }() 460 461 // When both env variables are empty, NewConfig should fail. 462 if _, err := NewConfig(); err == nil { 463 t.Errorf("NewConfig() returned nil error, expected to fail") 464 } 465 466 // When one of them is set, it should be used. 467 envconfig.XDSBootstrapFileName = goodFileName1 468 envconfig.XDSBootstrapFileContent = "" 469 c, err := NewConfig() 470 if err != nil { 471 t.Errorf("NewConfig() failed: %v", err) 472 } 473 if err := c.compare(goodConfig1); err != nil { 474 t.Error(err) 475 } 476 477 envconfig.XDSBootstrapFileName = "" 478 envconfig.XDSBootstrapFileContent = goodFileContent2 479 c, err = NewConfig() 480 if err != nil { 481 t.Errorf("NewConfig() failed: %v", err) 482 } 483 if err := c.compare(goodConfig2); err != nil { 484 t.Error(err) 485 } 486 487 // Set both, file name should be read. 488 envconfig.XDSBootstrapFileName = goodFileName1 489 envconfig.XDSBootstrapFileContent = goodFileContent2 490 c, err = NewConfig() 491 if err != nil { 492 t.Errorf("NewConfig() failed: %v", err) 493 } 494 if err := c.compare(goodConfig1); err != nil { 495 t.Error(err) 496 } 497 } 498 499 func init() { 500 certprovider.Register(&fakeCertProviderBuilder{}) 501 } 502 503 const fakeCertProviderName = "fake-certificate-provider" 504 505 // fakeCertProviderBuilder builds new instances of fakeCertProvider and 506 // interprets the config provided to it as JSON with a single key and value. 507 type fakeCertProviderBuilder struct{} 508 509 // ParseConfig expects input in JSON format containing a map from string to 510 // string, with a single entry and mapKey being "configKey". 511 func (b *fakeCertProviderBuilder) ParseConfig(cfg any) (*certprovider.BuildableConfig, error) { 512 config, ok := cfg.(json.RawMessage) 513 if !ok { 514 return nil, fmt.Errorf("fakeCertProviderBuilder received config of type %T, want []byte", config) 515 } 516 var cfgData map[string]string 517 if err := json.Unmarshal(config, &cfgData); err != nil { 518 return nil, fmt.Errorf("fakeCertProviderBuilder config parsing failed: %v", err) 519 } 520 if len(cfgData) != 1 || cfgData["configKey"] == "" { 521 return nil, errors.New("fakeCertProviderBuilder received invalid config") 522 } 523 fc := &fakeStableConfig{config: cfgData} 524 return certprovider.NewBuildableConfig(fakeCertProviderName, fc.canonical(), func(certprovider.BuildOptions) certprovider.Provider { 525 return &fakeCertProvider{} 526 }), nil 527 } 528 529 func (b *fakeCertProviderBuilder) Name() string { 530 return fakeCertProviderName 531 } 532 533 type fakeStableConfig struct { 534 config map[string]string 535 } 536 537 func (c *fakeStableConfig) canonical() []byte { 538 var cfg string 539 for k, v := range c.config { 540 cfg = fmt.Sprintf("%s:%s", k, v) 541 } 542 return []byte(cfg) 543 } 544 545 // fakeCertProvider is an empty implementation of the Provider interface. 546 type fakeCertProvider struct { 547 certprovider.Provider 548 } 549 550 func TestNewConfigWithCertificateProviders(t *testing.T) { 551 bootstrapFileMap := map[string]string{ 552 "badJSONCertProviderConfig": ` 553 { 554 "node": { 555 "id": "ENVOY_NODE_ID", 556 "metadata": { 557 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 558 } 559 }, 560 "xds_servers" : [{ 561 "server_uri": "trafficdirector.googleapis.com:443", 562 "channel_creds": [ 563 { "type": "google_default" } 564 ], 565 "server_features" : ["foo", "bar", "xds_v3"], 566 }], 567 "certificate_providers": "bad JSON" 568 }`, 569 "allUnknownCertProviders": ` 570 { 571 "node": { 572 "id": "ENVOY_NODE_ID", 573 "metadata": { 574 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 575 } 576 }, 577 "xds_servers" : [{ 578 "server_uri": "trafficdirector.googleapis.com:443", 579 "channel_creds": [ 580 { "type": "google_default" } 581 ], 582 "server_features" : ["xds_v3"] 583 }], 584 "certificate_providers": { 585 "unknownProviderInstance1": { 586 "plugin_name": "foo", 587 "config": {"foo": "bar"} 588 }, 589 "unknownProviderInstance2": { 590 "plugin_name": "bar", 591 "config": {"foo": "bar"} 592 } 593 } 594 }`, 595 "badCertProviderConfig": ` 596 { 597 "node": { 598 "id": "ENVOY_NODE_ID", 599 "metadata": { 600 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 601 } 602 }, 603 "xds_servers" : [{ 604 "server_uri": "trafficdirector.googleapis.com:443", 605 "channel_creds": [ 606 { "type": "google_default" } 607 ], 608 "server_features" : ["xds_v3"], 609 }], 610 "certificate_providers": { 611 "unknownProviderInstance": { 612 "plugin_name": "foo", 613 "config": {"foo": "bar"} 614 }, 615 "fakeProviderInstanceBad": { 616 "plugin_name": "fake-certificate-provider", 617 "config": {"configKey": 666} 618 } 619 } 620 }`, 621 "goodCertProviderConfig": ` 622 { 623 "node": { 624 "id": "ENVOY_NODE_ID", 625 "metadata": { 626 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 627 } 628 }, 629 "xds_servers" : [{ 630 "server_uri": "trafficdirector.googleapis.com:443", 631 "channel_creds": [ 632 { "type": "insecure" } 633 ], 634 "server_features" : ["xds_v3"] 635 }], 636 "certificate_providers": { 637 "unknownProviderInstance": { 638 "plugin_name": "foo", 639 "config": {"foo": "bar"} 640 }, 641 "fakeProviderInstance": { 642 "plugin_name": "fake-certificate-provider", 643 "config": {"configKey": "configValue"} 644 } 645 } 646 }`, 647 } 648 649 getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder) 650 parser := getBuilder(fakeCertProviderName) 651 if parser == nil { 652 t.Fatalf("missing certprovider plugin %q", fakeCertProviderName) 653 } 654 wantCfg, err := parser.ParseConfig(json.RawMessage(`{"configKey": "configValue"}`)) 655 if err != nil { 656 t.Fatalf("config parsing for plugin %q failed: %v", fakeCertProviderName, err) 657 } 658 659 cancel := setupBootstrapOverride(bootstrapFileMap) 660 defer cancel() 661 662 // Cannot use xdstestutils.ServerConfigForAddress here, as it would lead to 663 // a cyclic dependency. 664 jsonCfg := `{ 665 "server_uri": "trafficdirector.googleapis.com:443", 666 "channel_creds": [{"type": "insecure"}], 667 "server_features": ["xds_v3"] 668 }` 669 serverCfg, err := ServerConfigFromJSON([]byte(jsonCfg)) 670 if err != nil { 671 t.Fatalf("Failed to create server config from JSON %s: %v", jsonCfg, err) 672 } 673 goodConfig := &Config{ 674 XDSServer: serverCfg, 675 NodeProto: v3NodeProto, 676 CertProviderConfigs: map[string]*certprovider.BuildableConfig{ 677 "fakeProviderInstance": wantCfg, 678 }, 679 ClientDefaultListenerResourceNameTemplate: "%s", 680 } 681 tests := []struct { 682 name string 683 wantConfig *Config 684 wantErr bool 685 }{ 686 { 687 name: "badJSONCertProviderConfig", 688 wantErr: true, 689 }, 690 { 691 692 name: "badCertProviderConfig", 693 wantErr: true, 694 }, 695 { 696 697 name: "allUnknownCertProviders", 698 wantConfig: nonNilCredsConfigV3, 699 }, 700 { 701 name: "goodCertProviderConfig", 702 wantConfig: goodConfig, 703 }, 704 } 705 706 for _, test := range tests { 707 t.Run(test.name, func(t *testing.T) { 708 testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 709 testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 710 }) 711 } 712 } 713 714 func TestNewConfigWithServerListenerResourceNameTemplate(t *testing.T) { 715 cancel := setupBootstrapOverride(map[string]string{ 716 "badServerListenerResourceNameTemplate:": ` 717 { 718 "node": { 719 "id": "ENVOY_NODE_ID", 720 "metadata": { 721 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 722 } 723 }, 724 "xds_servers" : [{ 725 "server_uri": "trafficdirector.googleapis.com:443", 726 "channel_creds": [ 727 { "type": "google_default" } 728 ] 729 }], 730 "server_listener_resource_name_template": 123456789 731 }`, 732 "goodServerListenerResourceNameTemplate": ` 733 { 734 "node": { 735 "id": "ENVOY_NODE_ID", 736 "metadata": { 737 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 738 } 739 }, 740 "xds_servers" : [{ 741 "server_uri": "trafficdirector.googleapis.com:443", 742 "channel_creds": [ 743 { "type": "google_default" } 744 ] 745 }], 746 "server_listener_resource_name_template": "grpc/server?xds.resource.listening_address=%s" 747 }`, 748 }) 749 defer cancel() 750 751 tests := []struct { 752 name string 753 wantConfig *Config 754 wantErr bool 755 }{ 756 { 757 name: "badServerListenerResourceNameTemplate", 758 wantErr: true, 759 }, 760 { 761 name: "goodServerListenerResourceNameTemplate", 762 wantConfig: &Config{ 763 XDSServer: &ServerConfig{ 764 ServerURI: "trafficdirector.googleapis.com:443", 765 Creds: ChannelCreds{Type: "google_default"}, 766 }, 767 NodeProto: v3NodeProto, 768 ServerListenerResourceNameTemplate: "grpc/server?xds.resource.listening_address=%s", 769 ClientDefaultListenerResourceNameTemplate: "%s", 770 }, 771 }, 772 } 773 774 for _, test := range tests { 775 t.Run(test.name, func(t *testing.T) { 776 testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 777 testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 778 }) 779 } 780 } 781 782 func TestNewConfigWithFederation(t *testing.T) { 783 cancel := setupBootstrapOverride(map[string]string{ 784 "badClientListenerResourceNameTemplate": ` 785 { 786 "node": { "id": "ENVOY_NODE_ID" }, 787 "xds_servers" : [{ 788 "server_uri": "trafficdirector.googleapis.com:443" 789 }], 790 "client_default_listener_resource_name_template": 123456789 791 }`, 792 "badClientListenerResourceNameTemplatePerAuthority": ` 793 { 794 "node": { "id": "ENVOY_NODE_ID" }, 795 "xds_servers" : [{ 796 "server_uri": "trafficdirector.googleapis.com:443", 797 "channel_creds": [ { "type": "google_default" } ] 798 }], 799 "authorities": { 800 "xds.td.com": { 801 "client_listener_resource_name_template": "some/template/%s", 802 "xds_servers": [{ 803 "server_uri": "td.com", 804 "channel_creds": [ { "type": "google_default" } ], 805 "server_features" : ["foo", "bar", "xds_v3"] 806 }] 807 } 808 } 809 }`, 810 "good": ` 811 { 812 "node": { 813 "id": "ENVOY_NODE_ID", 814 "metadata": { 815 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 816 } 817 }, 818 "xds_servers" : [{ 819 "server_uri": "trafficdirector.googleapis.com:443", 820 "channel_creds": [ { "type": "google_default" } ] 821 }], 822 "server_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/grpc/server?listening_address=%s", 823 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 824 "authorities": { 825 "xds.td.com": { 826 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 827 "xds_servers": [{ 828 "server_uri": "td.com", 829 "channel_creds": [ { "type": "google_default" } ], 830 "server_features" : ["xds_v3"] 831 }] 832 } 833 } 834 }`, 835 // If client_default_listener_resource_name_template is not set, it 836 // defaults to "%s". 837 "goodWithDefaultDefaultClientListenerTemplate": ` 838 { 839 "node": { 840 "id": "ENVOY_NODE_ID", 841 "metadata": { 842 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 843 } 844 }, 845 "xds_servers" : [{ 846 "server_uri": "trafficdirector.googleapis.com:443", 847 "channel_creds": [ { "type": "google_default" } ] 848 }] 849 }`, 850 // If client_listener_resource_name_template in authority is not set, it 851 // defaults to 852 // "xdstp://<authority_name>/envoy.config.listener.v3.Listener/%s". 853 "goodWithDefaultClientListenerTemplatePerAuthority": ` 854 { 855 "node": { 856 "id": "ENVOY_NODE_ID", 857 "metadata": { 858 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 859 } 860 }, 861 "xds_servers" : [{ 862 "server_uri": "trafficdirector.googleapis.com:443", 863 "channel_creds": [ { "type": "google_default" } ] 864 }], 865 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 866 "authorities": { 867 "xds.td.com": { }, 868 "#.com": { } 869 } 870 }`, 871 // It's OK for an authority to not have servers. The top-level server 872 // will be used. 873 "goodWithNoServerPerAuthority": ` 874 { 875 "node": { 876 "id": "ENVOY_NODE_ID", 877 "metadata": { 878 "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector" 879 } 880 }, 881 "xds_servers" : [{ 882 "server_uri": "trafficdirector.googleapis.com:443", 883 "channel_creds": [ { "type": "google_default" } ] 884 }], 885 "client_default_listener_resource_name_template": "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 886 "authorities": { 887 "xds.td.com": { 888 "client_listener_resource_name_template": "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s" 889 } 890 } 891 }`, 892 }) 893 defer cancel() 894 895 tests := []struct { 896 name string 897 wantConfig *Config 898 wantErr bool 899 }{ 900 { 901 name: "badClientListenerResourceNameTemplate", 902 wantErr: true, 903 }, 904 { 905 name: "badClientListenerResourceNameTemplatePerAuthority", 906 wantErr: true, 907 }, 908 { 909 name: "good", 910 wantConfig: &Config{ 911 XDSServer: &ServerConfig{ 912 ServerURI: "trafficdirector.googleapis.com:443", 913 Creds: ChannelCreds{Type: "google_default"}, 914 }, 915 NodeProto: v3NodeProto, 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 XDSServer: &ServerConfig{ 922 ServerURI: "td.com", 923 Creds: ChannelCreds{Type: "google_default"}, 924 ServerFeatures: []string{"xds_v3"}, 925 }, 926 }, 927 }, 928 }, 929 }, 930 { 931 name: "goodWithDefaultDefaultClientListenerTemplate", 932 wantConfig: &Config{ 933 XDSServer: &ServerConfig{ 934 ServerURI: "trafficdirector.googleapis.com:443", 935 Creds: ChannelCreds{Type: "google_default"}, 936 }, 937 NodeProto: v3NodeProto, 938 ClientDefaultListenerResourceNameTemplate: "%s", 939 }, 940 }, 941 { 942 name: "goodWithDefaultClientListenerTemplatePerAuthority", 943 wantConfig: &Config{ 944 XDSServer: &ServerConfig{ 945 ServerURI: "trafficdirector.googleapis.com:443", 946 Creds: ChannelCreds{Type: "google_default"}, 947 }, 948 NodeProto: v3NodeProto, 949 ClientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 950 Authorities: map[string]*Authority{ 951 "xds.td.com": { 952 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 953 }, 954 "#.com": { 955 ClientListenerResourceNameTemplate: "xdstp://%23.com/envoy.config.listener.v3.Listener/%s", 956 }, 957 }, 958 }, 959 }, 960 { 961 name: "goodWithNoServerPerAuthority", 962 wantConfig: &Config{ 963 XDSServer: &ServerConfig{ 964 ServerURI: "trafficdirector.googleapis.com:443", 965 Creds: ChannelCreds{Type: "google_default"}, 966 }, 967 NodeProto: v3NodeProto, 968 ClientDefaultListenerResourceNameTemplate: "xdstp://xds.example.com/envoy.config.listener.v3.Listener/%s", 969 Authorities: map[string]*Authority{ 970 "xds.td.com": { 971 ClientListenerResourceNameTemplate: "xdstp://xds.td.com/envoy.config.listener.v3.Listener/%s", 972 }, 973 }, 974 }, 975 }, 976 } 977 978 for _, test := range tests { 979 t.Run(test.name, func(t *testing.T) { 980 testNewConfigWithFileNameEnv(t, test.name, test.wantErr, test.wantConfig) 981 testNewConfigWithFileContentEnv(t, test.name, test.wantErr, test.wantConfig) 982 }) 983 } 984 } 985 986 func TestServerConfigMarshalAndUnmarshal(t *testing.T) { 987 jsonCfg := `{ 988 "server_uri": "test-server", 989 "channel_creds": [{"type": "insecure"}], 990 "server_features": ["xds_v3"] 991 }` 992 origConfig, err := ServerConfigFromJSON([]byte(jsonCfg)) 993 if err != nil { 994 t.Fatalf("Failed to create server config from JSON %s: %v", jsonCfg, err) 995 } 996 bs, err := json.Marshal(origConfig) 997 if err != nil { 998 t.Fatalf("failed to marshal: %v", err) 999 } 1000 1001 unmarshaledConfig := new(ServerConfig) 1002 if err := json.Unmarshal(bs, unmarshaledConfig); err != nil { 1003 t.Fatalf("failed to unmarshal: %v", err) 1004 } 1005 if diff := cmp.Diff(origConfig, unmarshaledConfig); diff != "" { 1006 t.Fatalf("Unexpected diff in server config (-want, +got):\n%s", diff) 1007 } 1008 } 1009 1010 func TestDefaultBundles(t *testing.T) { 1011 tests := []string{"google_default", "insecure", "tls"} 1012 1013 for _, typename := range tests { 1014 t.Run(typename, func(t *testing.T) { 1015 if c := bootstrap.GetCredentials(typename); c == nil { 1016 t.Errorf(`bootstrap.GetCredentials(%s) credential is nil, want non-nil`, typename) 1017 } 1018 }) 1019 } 1020 } 1021 1022 func TestCredsBuilders(t *testing.T) { 1023 tests := []struct { 1024 typename string 1025 builder bootstrap.Credentials 1026 }{ 1027 {"google_default", &googleDefaultCredsBuilder{}}, 1028 {"insecure", &insecureCredsBuilder{}}, 1029 {"tls", &tlsCredsBuilder{}}, 1030 } 1031 1032 for _, test := range tests { 1033 t.Run(test.typename, func(t *testing.T) { 1034 if got, want := test.builder.Name(), test.typename; got != want { 1035 t.Errorf("%T.Name = %v, want %v", test.builder, got, want) 1036 } 1037 1038 _, stop, err := test.builder.Build(nil) 1039 if err != nil { 1040 t.Fatalf("%T.Build failed: %v", test.builder, err) 1041 } 1042 stop() 1043 }) 1044 } 1045 } 1046 1047 func TestTlsCredsBuilder(t *testing.T) { 1048 tls := &tlsCredsBuilder{} 1049 _, stop, err := tls.Build(json.RawMessage(`{}`)) 1050 if err != nil { 1051 t.Fatalf("tls.Build() failed with error %s when expected to succeed", err) 1052 } 1053 stop() 1054 1055 if _, stop, err := tls.Build(json.RawMessage(`{"ca_certificate_file":"/ca_certificates.pem","refresh_interval": "asdf"}`)); err == nil { 1056 t.Errorf("tls.Build() succeeded with an invalid refresh interval, when expected to fail") 1057 stop() 1058 } 1059 // package internal/xdsclient/tlscreds has tests for config validity. 1060 }