From ce271114c0ddaf408c923a7006bb4da31b1aa68a Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Fri, 3 Apr 2020 14:12:57 -0700 Subject: [PATCH] aws/external: Add Support for setting a default fallback region and resolving region from EC2 IMDS (#523) --- CHANGELOG_PENDING.md | 8 ++- aws/external/codegen/main.go | 1 + aws/external/config.go | 2 + aws/external/provider.go | 30 +++++++++ aws/external/provider_assert_test.go | 5 ++ aws/external/resolve.go | 51 +++++++++++++++ aws/external/resolve_test.go | 98 ++++++++++++++++++++++++++++ aws/external/shared_config_test.go | 2 +- 8 files changed, 195 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6581407a995..86e92381e96 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,7 +18,13 @@ SDK Features * `SignHTTP` replaces `Sign`, and usage of `Sign` should be migrated before it's removal at a later date * `PresignHTTP` replaces `Presign`, and usage of `Presign` should be migrated before it's removal at a later date * `DisableRequestBodyOverwrite` and `UnsignedPayload` are now deprecated options and have no effect on `SignHTTP` or `PresignHTTP`. These options will be removed at a later date. - +* `aws/external`: Add Support for setting a default fallback region and resolving region from EC2 IMDS ([#523](https://github.com/aws/aws-sdk-go-v2/pull/523)) + * `WithDefaultRegion` helper has been added which can be passed to `LoadDefaultAWSConfig` + * This helper can be used to configure a default fallback region in the event a region fails to be resolved from other sources + * Support has been added to resolve region using EC2 IMDS when available + * The IMDS region will be used if region as not found configured in either the shared config or the process environment. + * Fixes [#244](https://github.com/aws/aws-sdk-go-v2/issues/244) + * Fixes [#515](https://github.com/aws/aws-sdk-go-v2/issues/515) SDK Enhancements --- * `internal/ini`: Normalize Section keys to lowercase ([#495](https://github.com/aws/aws-sdk-go-v2/pull/495)) diff --git a/aws/external/codegen/main.go b/aws/external/codegen/main.go index 642534c3041..9408eaaabd0 100644 --- a/aws/external/codegen/main.go +++ b/aws/external/codegen/main.go @@ -23,6 +23,7 @@ var implAsserts = map[string][]string{ "MFATokenFuncProvider": {`WithMFATokenFunc(func() (string, error) { return "", nil })`}, "EnableEndpointDiscoveryProvider": {envConfigType, sharedConfigType, "WithEnableEndpointDiscovery(true)"}, "CredentialsProviderProvider": {`WithCredentialsProvider{aws.NewStaticCredentialsProvider("", "", "")}`}, + "DefaultRegionProvider": {`WithDefaultRegion("")`}, } var tplProviderTests = template.Must(template.New("tplProviderTests").Funcs(map[string]interface{}{ diff --git a/aws/external/config.go b/aws/external/config.go index e65d45e4421..447b2070974 100644 --- a/aws/external/config.go +++ b/aws/external/config.go @@ -24,6 +24,8 @@ var DefaultAWSConfigResolvers = []AWSConfigResolver{ ResolveEnableEndpointDiscovery, ResolveRegion, + ResolveEC2Region, + ResolveDefaultRegion, ResolveCredentials, } diff --git a/aws/external/provider.go b/aws/external/provider.go index 4e0ad94bb57..a091db5e350 100644 --- a/aws/external/provider.go +++ b/aws/external/provider.go @@ -507,3 +507,33 @@ func GetWebIdentityCredentialProviderOptions(configs Configs) (f func(*stscreds. } return f, found, err } + +// DefaultRegionProvider is an interface for retrieving a default region if a region was not resolved from other sources +type DefaultRegionProvider interface { + GetDefaultRegion() (string, bool, error) +} + +// WithDefaultRegion wraps a string and satisfies the DefaultRegionProvider interface +type WithDefaultRegion string + +// GetDefaultRegion returns wrapped fallback region +func (w WithDefaultRegion) GetDefaultRegion() (string, bool, error) { + return string(w), true, nil +} + +// GetDefaultRegion searches the slice of configs and returns the first fallback region found +func GetDefaultRegion(configs Configs) (value string, found bool, err error) { + for _, config := range configs { + if p, ok := config.(DefaultRegionProvider); ok { + value, found, err = p.GetDefaultRegion() + if err != nil { + return "", false, err + } + if found { + break + } + } + } + + return value, found, err +} diff --git a/aws/external/provider_assert_test.go b/aws/external/provider_assert_test.go index 17a4cbb3436..aec37c17cdf 100755 --- a/aws/external/provider_assert_test.go +++ b/aws/external/provider_assert_test.go @@ -17,6 +17,11 @@ var ( _ CustomCABundleProvider = WithCustomCABundle([]byte{}) ) +// DefaultRegionProvider implementor assertions +var ( + _ DefaultRegionProvider = WithDefaultRegion("") +) + // EnableEndpointDiscoveryProvider implementor assertions var ( _ EnableEndpointDiscoveryProvider = &EnvConfig{} diff --git a/aws/external/resolve.go b/aws/external/resolve.go index ce2e5882abf..404ad88f276 100644 --- a/aws/external/resolve.go +++ b/aws/external/resolve.go @@ -1,6 +1,7 @@ package external import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -9,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/awserr" "github.com/aws/aws-sdk-go-v2/aws/defaults" + "github.com/aws/aws-sdk-go-v2/aws/ec2metadata" ) // ResolveDefaultAWSConfig will write default configuration values into the cfg @@ -136,3 +138,52 @@ func ResolveEndpointResolverFunc(cfg *aws.Config, configs Configs) error { return nil } + +// ResolveDefaultRegion extracts the first instance of a default region and sets `aws.Config.Region` to the default +// region if region had not been resolved from other sources. +func ResolveDefaultRegion(cfg *aws.Config, configs Configs) error { + if len(cfg.Region) > 0 { + return nil + } + + region, found, err := GetDefaultRegion(configs) + if err != nil { + return err + } + if !found { + return nil + } + + cfg.Region = region + + return nil +} + +type ec2MetadataRegionClient interface { + Region(context.Context) (string, error) +} + +// newEC2MetadataClient is the EC2 instance metadata service client, allows for swapping during testing +var newEC2MetadataClient = func(cfg aws.Config) ec2MetadataRegionClient { + return ec2metadata.New(cfg) +} + +// ResolveEC2Region attempts to resolve the region using the EC2 instance metadata service. If region is already set on +// the config no lookup occurs. If an error is returned the service is assumed unavailable. +func ResolveEC2Region(cfg *aws.Config, _ Configs) error { + if len(cfg.Region) > 0 { + return nil + } + + client := newEC2MetadataClient(*cfg) + + // TODO: What does context look like with external config loading and how to handle the impact to service client config loading + region, err := client.Region(context.Background()) + if err != nil { + return nil + } + + cfg.Region = region + + return nil +} diff --git a/aws/external/resolve_test.go b/aws/external/resolve_test.go index 2127006c18a..c3c20b6ea79 100644 --- a/aws/external/resolve_test.go +++ b/aws/external/resolve_test.go @@ -2,6 +2,7 @@ package external import ( "context" + "fmt" "io/ioutil" "net/http" "testing" @@ -161,3 +162,100 @@ func TestEnableEndpointDiscovery(t *testing.T) { t.Errorf("expected %v, got %v", e, a) } } + +func TestDefaultRegion(t *testing.T) { + configs := Configs{ + WithDefaultRegion("foo-region"), + } + + cfg := unit.Config() + + err := ResolveDefaultRegion(&cfg, configs) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if e, a := "mock-region", cfg.Region; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + cfg.Region = "" + + err = ResolveDefaultRegion(&cfg, configs) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if e, a := "foo-region", cfg.Region; e != a { + t.Errorf("expected %v, got %v", e, a) + } +} + +func TestResolveEC2Region(t *testing.T) { + configs := Configs{} + + cfg := unit.Config() + + err := ResolveEC2Region(&cfg, configs) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if e, a := "mock-region", cfg.Region; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + resetOrig := swapEC2MetadataNew(func(config aws.Config) ec2MetadataRegionClient { + return mockEC2MetadataClient{ + retRegion: "foo-region", + } + }) + defer resetOrig() + + cfg.Region = "" + err = ResolveEC2Region(&cfg, configs) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if e, a := "foo-region", cfg.Region; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + _ = swapEC2MetadataNew(func(config aws.Config) ec2MetadataRegionClient { + return mockEC2MetadataClient{ + retErr: fmt.Errorf("some error"), + } + }) + + cfg.Region = "" + err = ResolveEC2Region(&cfg, configs) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(cfg.Region) != 0 { + t.Errorf("expected region to remain unset") + } +} + +type mockEC2MetadataClient struct { + retRegion string + retErr error +} + +func (m mockEC2MetadataClient) Region(ctx context.Context) (string, error) { + if m.retErr != nil { + return "", m.retErr + } + + return m.retRegion, nil +} + +func swapEC2MetadataNew(f func(config aws.Config) ec2MetadataRegionClient) func() { + orig := newEC2MetadataClient + newEC2MetadataClient = f + return func() { + newEC2MetadataClient = orig + } +} diff --git a/aws/external/shared_config_test.go b/aws/external/shared_config_test.go index 09fa74bb6df..e5afd8fce3d 100644 --- a/aws/external/shared_config_test.go +++ b/aws/external/shared_config_test.go @@ -336,7 +336,7 @@ func TestLoadSharedConfigFromFile(t *testing.T) { }, }, { - Profile: "with_mixed_case_keys", + Profile: "with_mixed_case_keys", Expected: SharedConfig{ Credentials: aws.Credentials{ AccessKeyID: "accessKey",