Skip to content

Commit ce27111

Browse files
authored
aws/external: Add Support for setting a default fallback region and resolving region from EC2 IMDS (#523)
1 parent 3c6b539 commit ce27111

File tree

8 files changed

+195
-2
lines changed

8 files changed

+195
-2
lines changed

CHANGELOG_PENDING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ SDK Features
1818
* `SignHTTP` replaces `Sign`, and usage of `Sign` should be migrated before it's removal at a later date
1919
* `PresignHTTP` replaces `Presign`, and usage of `Presign` should be migrated before it's removal at a later date
2020
* `DisableRequestBodyOverwrite` and `UnsignedPayload` are now deprecated options and have no effect on `SignHTTP` or `PresignHTTP`. These options will be removed at a later date.
21-
21+
* `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))
22+
* `WithDefaultRegion` helper has been added which can be passed to `LoadDefaultAWSConfig`
23+
* This helper can be used to configure a default fallback region in the event a region fails to be resolved from other sources
24+
* Support has been added to resolve region using EC2 IMDS when available
25+
* The IMDS region will be used if region as not found configured in either the shared config or the process environment.
26+
* Fixes [#244](https://github.com/aws/aws-sdk-go-v2/issues/244)
27+
* Fixes [#515](https://github.com/aws/aws-sdk-go-v2/issues/515)
2228
SDK Enhancements
2329
---
2430
* `internal/ini`: Normalize Section keys to lowercase ([#495](https://github.com/aws/aws-sdk-go-v2/pull/495))

aws/external/codegen/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var implAsserts = map[string][]string{
2323
"MFATokenFuncProvider": {`WithMFATokenFunc(func() (string, error) { return "", nil })`},
2424
"EnableEndpointDiscoveryProvider": {envConfigType, sharedConfigType, "WithEnableEndpointDiscovery(true)"},
2525
"CredentialsProviderProvider": {`WithCredentialsProvider{aws.NewStaticCredentialsProvider("", "", "")}`},
26+
"DefaultRegionProvider": {`WithDefaultRegion("")`},
2627
}
2728

2829
var tplProviderTests = template.Must(template.New("tplProviderTests").Funcs(map[string]interface{}{

aws/external/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ var DefaultAWSConfigResolvers = []AWSConfigResolver{
2424
ResolveEnableEndpointDiscovery,
2525

2626
ResolveRegion,
27+
ResolveEC2Region,
28+
ResolveDefaultRegion,
2729

2830
ResolveCredentials,
2931
}

aws/external/provider.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,33 @@ func GetWebIdentityCredentialProviderOptions(configs Configs) (f func(*stscreds.
507507
}
508508
return f, found, err
509509
}
510+
511+
// DefaultRegionProvider is an interface for retrieving a default region if a region was not resolved from other sources
512+
type DefaultRegionProvider interface {
513+
GetDefaultRegion() (string, bool, error)
514+
}
515+
516+
// WithDefaultRegion wraps a string and satisfies the DefaultRegionProvider interface
517+
type WithDefaultRegion string
518+
519+
// GetDefaultRegion returns wrapped fallback region
520+
func (w WithDefaultRegion) GetDefaultRegion() (string, bool, error) {
521+
return string(w), true, nil
522+
}
523+
524+
// GetDefaultRegion searches the slice of configs and returns the first fallback region found
525+
func GetDefaultRegion(configs Configs) (value string, found bool, err error) {
526+
for _, config := range configs {
527+
if p, ok := config.(DefaultRegionProvider); ok {
528+
value, found, err = p.GetDefaultRegion()
529+
if err != nil {
530+
return "", false, err
531+
}
532+
if found {
533+
break
534+
}
535+
}
536+
}
537+
538+
return value, found, err
539+
}

aws/external/provider_assert_test.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aws/external/resolve.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package external
22

33
import (
4+
"context"
45
"crypto/tls"
56
"crypto/x509"
67
"fmt"
@@ -9,6 +10,7 @@ import (
910
"github.com/aws/aws-sdk-go-v2/aws"
1011
"github.com/aws/aws-sdk-go-v2/aws/awserr"
1112
"github.com/aws/aws-sdk-go-v2/aws/defaults"
13+
"github.com/aws/aws-sdk-go-v2/aws/ec2metadata"
1214
)
1315

1416
// ResolveDefaultAWSConfig will write default configuration values into the cfg
@@ -136,3 +138,52 @@ func ResolveEndpointResolverFunc(cfg *aws.Config, configs Configs) error {
136138

137139
return nil
138140
}
141+
142+
// ResolveDefaultRegion extracts the first instance of a default region and sets `aws.Config.Region` to the default
143+
// region if region had not been resolved from other sources.
144+
func ResolveDefaultRegion(cfg *aws.Config, configs Configs) error {
145+
if len(cfg.Region) > 0 {
146+
return nil
147+
}
148+
149+
region, found, err := GetDefaultRegion(configs)
150+
if err != nil {
151+
return err
152+
}
153+
if !found {
154+
return nil
155+
}
156+
157+
cfg.Region = region
158+
159+
return nil
160+
}
161+
162+
type ec2MetadataRegionClient interface {
163+
Region(context.Context) (string, error)
164+
}
165+
166+
// newEC2MetadataClient is the EC2 instance metadata service client, allows for swapping during testing
167+
var newEC2MetadataClient = func(cfg aws.Config) ec2MetadataRegionClient {
168+
return ec2metadata.New(cfg)
169+
}
170+
171+
// ResolveEC2Region attempts to resolve the region using the EC2 instance metadata service. If region is already set on
172+
// the config no lookup occurs. If an error is returned the service is assumed unavailable.
173+
func ResolveEC2Region(cfg *aws.Config, _ Configs) error {
174+
if len(cfg.Region) > 0 {
175+
return nil
176+
}
177+
178+
client := newEC2MetadataClient(*cfg)
179+
180+
// TODO: What does context look like with external config loading and how to handle the impact to service client config loading
181+
region, err := client.Region(context.Background())
182+
if err != nil {
183+
return nil
184+
}
185+
186+
cfg.Region = region
187+
188+
return nil
189+
}

aws/external/resolve_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package external
22

33
import (
44
"context"
5+
"fmt"
56
"io/ioutil"
67
"net/http"
78
"testing"
@@ -161,3 +162,100 @@ func TestEnableEndpointDiscovery(t *testing.T) {
161162
t.Errorf("expected %v, got %v", e, a)
162163
}
163164
}
165+
166+
func TestDefaultRegion(t *testing.T) {
167+
configs := Configs{
168+
WithDefaultRegion("foo-region"),
169+
}
170+
171+
cfg := unit.Config()
172+
173+
err := ResolveDefaultRegion(&cfg, configs)
174+
if err != nil {
175+
t.Fatalf("expected no error, got %v", err)
176+
}
177+
178+
if e, a := "mock-region", cfg.Region; e != a {
179+
t.Errorf("expected %v, got %v", e, a)
180+
}
181+
182+
cfg.Region = ""
183+
184+
err = ResolveDefaultRegion(&cfg, configs)
185+
if err != nil {
186+
t.Fatalf("expected no error, got %v", err)
187+
}
188+
189+
if e, a := "foo-region", cfg.Region; e != a {
190+
t.Errorf("expected %v, got %v", e, a)
191+
}
192+
}
193+
194+
func TestResolveEC2Region(t *testing.T) {
195+
configs := Configs{}
196+
197+
cfg := unit.Config()
198+
199+
err := ResolveEC2Region(&cfg, configs)
200+
if err != nil {
201+
t.Fatalf("expected no error, got %v", err)
202+
}
203+
204+
if e, a := "mock-region", cfg.Region; e != a {
205+
t.Errorf("expected %v, got %v", e, a)
206+
}
207+
208+
resetOrig := swapEC2MetadataNew(func(config aws.Config) ec2MetadataRegionClient {
209+
return mockEC2MetadataClient{
210+
retRegion: "foo-region",
211+
}
212+
})
213+
defer resetOrig()
214+
215+
cfg.Region = ""
216+
err = ResolveEC2Region(&cfg, configs)
217+
if err != nil {
218+
t.Fatalf("expected no error, got %v", err)
219+
}
220+
221+
if e, a := "foo-region", cfg.Region; e != a {
222+
t.Errorf("expected %v, got %v", e, a)
223+
}
224+
225+
_ = swapEC2MetadataNew(func(config aws.Config) ec2MetadataRegionClient {
226+
return mockEC2MetadataClient{
227+
retErr: fmt.Errorf("some error"),
228+
}
229+
})
230+
231+
cfg.Region = ""
232+
err = ResolveEC2Region(&cfg, configs)
233+
if err != nil {
234+
t.Fatalf("expected no error, got %v", err)
235+
}
236+
237+
if len(cfg.Region) != 0 {
238+
t.Errorf("expected region to remain unset")
239+
}
240+
}
241+
242+
type mockEC2MetadataClient struct {
243+
retRegion string
244+
retErr error
245+
}
246+
247+
func (m mockEC2MetadataClient) Region(ctx context.Context) (string, error) {
248+
if m.retErr != nil {
249+
return "", m.retErr
250+
}
251+
252+
return m.retRegion, nil
253+
}
254+
255+
func swapEC2MetadataNew(f func(config aws.Config) ec2MetadataRegionClient) func() {
256+
orig := newEC2MetadataClient
257+
newEC2MetadataClient = f
258+
return func() {
259+
newEC2MetadataClient = orig
260+
}
261+
}

aws/external/shared_config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func TestLoadSharedConfigFromFile(t *testing.T) {
336336
},
337337
},
338338
{
339-
Profile: "with_mixed_case_keys",
339+
Profile: "with_mixed_case_keys",
340340
Expected: SharedConfig{
341341
Credentials: aws.Credentials{
342342
AccessKeyID: "accessKey",

0 commit comments

Comments
 (0)