Skip to content

Commit

Permalink
aws/external: Add Support for setting a default fallback region and r…
Browse files Browse the repository at this point in the history
…esolving region from EC2 IMDS (#523)
  • Loading branch information
skmcgrail authored Apr 3, 2020
1 parent 3c6b539 commit ce27111
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 2 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions aws/external/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand Down
2 changes: 2 additions & 0 deletions aws/external/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var DefaultAWSConfigResolvers = []AWSConfigResolver{
ResolveEnableEndpointDiscovery,

ResolveRegion,
ResolveEC2Region,
ResolveDefaultRegion,

ResolveCredentials,
}
Expand Down
30 changes: 30 additions & 0 deletions aws/external/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions aws/external/provider_assert_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions aws/external/resolve.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package external

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
Expand All @@ -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
Expand Down Expand Up @@ -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
}
98 changes: 98 additions & 0 deletions aws/external/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package external

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"testing"
Expand Down Expand Up @@ -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
}
}
2 changes: 1 addition & 1 deletion aws/external/shared_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit ce27111

Please sign in to comment.