diff --git a/.changelog/4cb8965feb3b48b6afcfa099607e4bb0.json b/.changelog/4cb8965feb3b48b6afcfa099607e4bb0.json new file mode 100644 index 00000000000..ad3411473f3 --- /dev/null +++ b/.changelog/4cb8965feb3b48b6afcfa099607e4bb0.json @@ -0,0 +1,8 @@ +{ + "id": "4cb8965f-eb3b-48b6-afcf-a099607e4bb0", + "type": "bugfix", + "description": "Fixes a bug in LoadDefaultConfig to correctly assign ConfigSources so all config resolvers have access to the config sources. This fixes the feature/ec2/imds client not having configuration applied via config.LoadOptions such as EC2IMDSClientEnableState. PR [#1682](https://github.com/aws/aws-sdk-go-v2/pull/1682)", + "modules": [ + "config" + ] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go index 79f067017e2..3e9c20009fe 100644 --- a/config/config.go +++ b/config/config.go @@ -137,17 +137,10 @@ func (cs configs) ResolveAWSConfig(ctx context.Context, resolvers []awsConfigRes for _, fn := range resolvers { if err := fn(ctx, &cfg, cs); err != nil { - // TODO provide better error? return aws.Config{}, err } } - var sources []interface{} - for _, s := range cs { - sources = append(sources, s) - } - cfg.ConfigSources = sources - return cfg, nil } diff --git a/config/resolve.go b/config/resolve.go index 4a802476949..4428ba49c20 100644 --- a/config/resolve.go +++ b/config/resolve.go @@ -21,9 +21,15 @@ import ( // This should be used as the first resolver in the slice of resolvers when // resolving external configuration. func resolveDefaultAWSConfig(ctx context.Context, cfg *aws.Config, cfgs configs) error { + var sources []interface{} + for _, s := range cfgs { + sources = append(sources, s) + } + *cfg = aws.Config{ - Credentials: aws.AnonymousCredentials{}, - Logger: logging.NewStandardLogger(os.Stderr), + Credentials: aws.AnonymousCredentials{}, + Logger: logging.NewStandardLogger(os.Stderr), + ConfigSources: sources, } return nil } diff --git a/config/resolve_credentials_test.go b/config/resolve_credentials_test.go index 268d3a6ae61..b2454250928 100644 --- a/config/resolve_credentials_test.go +++ b/config/resolve_credentials_test.go @@ -2,6 +2,7 @@ package config import ( "context" + "errors" "fmt" "io/ioutil" "net/http" @@ -15,9 +16,11 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "github.com/aws/aws-sdk-go-v2/internal/awstesting" "github.com/aws/aws-sdk-go-v2/service/sso" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" "github.com/aws/smithy-go/middleware" smithytime "github.com/aws/smithy-go/time" ) @@ -471,3 +474,122 @@ func TestResolveCredentialsCacheOptions(t *testing.T) { t.Errorf("expect options to be called") } } + +func TestResolveCredentialsIMDSClient(t *testing.T) { + expectEnabled := func(t *testing.T, err error) { + if err == nil { + t.Fatalf("expect error got none") + } + if e, a := "expected HTTP client error", err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected %v error in %v", e, a) + } + } + + expectDisabled := func(t *testing.T, err error) { + var oe *smithy.OperationError + if !errors.As(err, &oe) { + t.Fatalf("unexpected error: %v", err) + } else { + e := errors.Unwrap(oe) + if e == nil { + t.Fatalf("unexpected empty operation error: %v", oe) + } else { + if !strings.HasPrefix(e.Error(), "access disabled to EC2 IMDS") { + t.Fatalf("unexpected operation error: %v", oe) + } + } + } + } + + testcases := map[string]struct { + enabledState imds.ClientEnableState + envvar string + expectedState imds.ClientEnableState + expectedError func(*testing.T, error) + }{ + "default no options": { + expectedState: imds.ClientDefaultEnableState, + expectedError: expectEnabled, + }, + + "state enabled": { + enabledState: imds.ClientEnabled, + expectedState: imds.ClientEnabled, + expectedError: expectEnabled, + }, + "state disabled": { + enabledState: imds.ClientDisabled, + expectedState: imds.ClientDisabled, + expectedError: expectDisabled, + }, + + "env var DISABLED true": { + envvar: "true", + expectedState: imds.ClientDisabled, + expectedError: expectDisabled, + }, + "env var DISABLED false": { + envvar: "false", + expectedState: imds.ClientEnabled, + expectedError: expectEnabled, + }, + + "option state enabled overrides env var DISABLED true": { + enabledState: imds.ClientEnabled, + envvar: "true", + expectedState: imds.ClientEnabled, + expectedError: expectEnabled, + }, + "option state disabled overrides env var DISABLED false": { + enabledState: imds.ClientDisabled, + envvar: "false", + expectedState: imds.ClientDisabled, + expectedError: expectDisabled, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + restoreEnv := awstesting.StashEnv() + defer awstesting.PopEnv(restoreEnv) + + var httpClient HTTPClient + if tc.expectedState == imds.ClientDisabled { + httpClient = stubErrorClient{err: fmt.Errorf("expect HTTP client not to be called")} + } else { + httpClient = stubErrorClient{err: fmt.Errorf("expected HTTP client error")} + } + + opts := []func(*LoadOptions) error{ + WithRetryer(func() aws.Retryer { return aws.NopRetryer{} }), + WithHTTPClient(httpClient), + } + + if tc.enabledState != imds.ClientDefaultEnableState { + opts = append(opts, + WithEC2IMDSClientEnableState(tc.enabledState), + ) + } + + if tc.envvar != "" { + os.Setenv("AWS_EC2_METADATA_DISABLED", tc.envvar) + } + + c, err := LoadDefaultConfig(context.TODO(), opts...) + if err != nil { + t.Fatalf("could not load config: %s", err) + } + + creds := c.Credentials + + _, err = creds.Retrieve(context.TODO()) + tc.expectedError(t, err) + }) + } +} + +type stubErrorClient struct { + err error +} + +func (c stubErrorClient) Do(*http.Request) (*http.Response, error) { return nil, c.err }