Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws/external: Add Support for setting a default fallback region and resolving region from EC2 IMDS #523

Merged
merged 2 commits into from
Apr 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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