Skip to content

Commit 22f76f8

Browse files
authored
feat(awsutil-v2): implement awsutil for aws-sdk-go-v2 (#83)
This major version release utilizes the latest version of the aws-sdk-go-v2. The following behavioral changes are included in this major version release: - Custom endpoint resolvers are attached to the STS and IAM clients, not to the credentials. This is apart of the aws-sdk-go-v2 EndpointResolverV2 feature. - withStsEndpoint is no longer a string type, but a sts.EndpointResolverV2 type. This option was relabeled to withStsEndpointResolver. - withIamEndpoint is no longer a string type, but a iam.EndpointResolverV2 type. This option was relabeled to withIamEndpointResolver. - By default, aws credential configurations will load values from environment variables. The user provided options will overload the default values. - The ability to mock out the underlying credential provider for unit testing. Changed behaviors from awsutil v1 includes the following: - Replaced aws errors with aws smithy-go errors - No longer able to utilize the aws default remote credential provider - The function GenerateCredentialChain returns a aws.Config, which contains the credential provider.
1 parent 7a5e901 commit 22f76f8

17 files changed

+1155
-639
lines changed

awsutil/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# AWSUTIL - Go library for generating aws credentials
2+
3+
*NOTE*: This is version 2 of the library. The `v0` branch contains version 0,
4+
which may be needed for legacy applications or while transitioning to version 2.
5+
6+
## Usage
7+
8+
Following is an example usage of generating AWS credentials with static user credentials
9+
10+
```go
11+
12+
// AWS access keys for an IAM user can be used as your AWS credentials.
13+
// This is an example of an access key and secret key
14+
var accessKey = "AKIAIOSFODNN7EXAMPLE"
15+
var secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
16+
17+
// Access key IDs beginning with AKIA are long-term access keys. A long-term
18+
// access key should be supplied when generating static credentials.
19+
config, err := awsutil.NewCredentialsConfig(
20+
awsutil.WithAccessKey(accessKey),
21+
awsutil.WithSecretKey(secretKey),
22+
)
23+
if err != nil {
24+
return err
25+
}
26+
27+
s3Client := s3.NewFromConfig(config)
28+
29+
```
30+
31+
## Contributing to v0
32+
33+
To push a bug fix or feature for awsutil `v0`, branch out from the [awsutil/v0](https://github.com/hashicorp/go-secure-stdlib/tree/awsutil/v0) branch.
34+
Commit the code changes you want to this new branch and open a PR. Make sure the PR
35+
is configured so that the base branch is set to `awsutil/v0` and not `main`. Once the PR
36+
is reviewed, feel free to merge it into the `awsutil/v0` branch. When creating a new
37+
release, validate that the `Target` branch is `awsutil/v0` and the tag is `awsutil/v0.x.x`.

awsutil/clients.go

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,90 +4,98 @@
44
package awsutil
55

66
import (
7-
"errors"
7+
"context"
88
"fmt"
99

10-
"github.com/aws/aws-sdk-go/aws/session"
11-
"github.com/aws/aws-sdk-go/service/iam"
12-
"github.com/aws/aws-sdk-go/service/iam/iamiface"
13-
"github.com/aws/aws-sdk-go/service/sts"
14-
"github.com/aws/aws-sdk-go/service/sts/stsiface"
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/service/iam"
12+
"github.com/aws/aws-sdk-go-v2/service/sts"
1513
)
1614

1715
// IAMAPIFunc is a factory function for returning an IAM interface,
18-
// useful for supplying mock interfaces for testing IAM. The session
19-
// is passed into the function in the same way as done with the
20-
// standard iam.New() constructor.
21-
type IAMAPIFunc func(sess *session.Session) (iamiface.IAMAPI, error)
16+
// useful for supplying mock interfaces for testing IAM.
17+
type IAMAPIFunc func(awsConfig *aws.Config) (IAMClient, error)
18+
19+
// IAMClient represents an iam.Client
20+
type IAMClient interface {
21+
CreateAccessKey(context.Context, *iam.CreateAccessKeyInput, ...func(*iam.Options)) (*iam.CreateAccessKeyOutput, error)
22+
DeleteAccessKey(context.Context, *iam.DeleteAccessKeyInput, ...func(*iam.Options)) (*iam.DeleteAccessKeyOutput, error)
23+
ListAccessKeys(context.Context, *iam.ListAccessKeysInput, ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error)
24+
GetUser(context.Context, *iam.GetUserInput, ...func(*iam.Options)) (*iam.GetUserOutput, error)
25+
}
2226

2327
// STSAPIFunc is a factory function for returning a STS interface,
24-
// useful for supplying mock interfaces for testing STS. The session
25-
// is passed into the function in the same way as done with the
26-
// standard sts.New() constructor.
27-
type STSAPIFunc func(sess *session.Session) (stsiface.STSAPI, error)
28+
// useful for supplying mock interfaces for testing STS.
29+
type STSAPIFunc func(awsConfig *aws.Config) (STSClient, error)
30+
31+
// STSClient represents an sts.Client
32+
type STSClient interface {
33+
AssumeRole(context.Context, *sts.AssumeRoleInput, ...func(*sts.Options)) (*sts.AssumeRoleOutput, error)
34+
GetCallerIdentity(context.Context, *sts.GetCallerIdentityInput, ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
35+
}
2836

2937
// IAMClient returns an IAM client.
3038
//
31-
// Supported options: WithSession, WithIAMAPIFunc.
39+
// Supported options: WithAwsConfig, WithIAMAPIFunc, WithIamEndpointResolver.
3240
//
3341
// If WithIAMAPIFunc is supplied, the included function is used as
3442
// the IAM client constructor instead. This can be used for Mocking
3543
// the IAM API.
36-
func (c *CredentialsConfig) IAMClient(opt ...Option) (iamiface.IAMAPI, error) {
44+
func (c *CredentialsConfig) IAMClient(ctx context.Context, opt ...Option) (IAMClient, error) {
3745
opts, err := getOpts(opt...)
3846
if err != nil {
3947
return nil, fmt.Errorf("error reading options: %w", err)
4048
}
4149

42-
sess := opts.withAwsSession
43-
if sess == nil {
44-
sess, err = c.GetSession(opt...)
50+
cfg := opts.withAwsConfig
51+
if cfg == nil {
52+
cfg, err = c.GenerateCredentialChain(ctx, opt...)
4553
if err != nil {
46-
return nil, fmt.Errorf("error calling GetSession: %w", err)
54+
return nil, fmt.Errorf("error calling GenerateCredentialChain: %w", err)
4755
}
4856
}
4957

5058
if opts.withIAMAPIFunc != nil {
51-
return opts.withIAMAPIFunc(sess)
59+
return opts.withIAMAPIFunc(cfg)
5260
}
5361

54-
client := iam.New(sess)
55-
if client == nil {
56-
return nil, errors.New("could not obtain iam client from session")
62+
var iamOpts []func(*iam.Options)
63+
if c.IAMEndpointResolver != nil {
64+
iamOpts = append(iamOpts, iam.WithEndpointResolverV2(c.IAMEndpointResolver))
5765
}
5866

59-
return client, nil
67+
return iam.NewFromConfig(*cfg, iamOpts...), nil
6068
}
6169

6270
// STSClient returns a STS client.
6371
//
64-
// Supported options: WithSession, WithSTSAPIFunc.
72+
// Supported options: WithAwsConfig, WithSTSAPIFunc, WithStsEndpointResolver.
6573
//
6674
// If WithSTSAPIFunc is supplied, the included function is used as
6775
// the STS client constructor instead. This can be used for Mocking
6876
// the STS API.
69-
func (c *CredentialsConfig) STSClient(opt ...Option) (stsiface.STSAPI, error) {
77+
func (c *CredentialsConfig) STSClient(ctx context.Context, opt ...Option) (STSClient, error) {
7078
opts, err := getOpts(opt...)
7179
if err != nil {
7280
return nil, fmt.Errorf("error reading options: %w", err)
7381
}
7482

75-
sess := opts.withAwsSession
76-
if sess == nil {
77-
sess, err = c.GetSession(opt...)
83+
cfg := opts.withAwsConfig
84+
if cfg == nil {
85+
cfg, err = c.GenerateCredentialChain(ctx, opt...)
7886
if err != nil {
79-
return nil, fmt.Errorf("error calling GetSession: %w", err)
87+
return nil, fmt.Errorf("error calling GenerateCredentialChain: %w", err)
8088
}
8189
}
8290

8391
if opts.withSTSAPIFunc != nil {
84-
return opts.withSTSAPIFunc(sess)
92+
return opts.withSTSAPIFunc(cfg)
8593
}
8694

87-
client := sts.New(sess)
88-
if client == nil {
89-
return nil, errors.New("could not obtain sts client from session")
95+
var stsOpts []func(*sts.Options)
96+
if c.STSEndpointResolver != nil {
97+
stsOpts = append(stsOpts, sts.WithEndpointResolverV2(c.STSEndpointResolver))
9098
}
9199

92-
return client, nil
100+
return sts.NewFromConfig(*cfg, stsOpts...), nil
93101
}

awsutil/clients_test.go

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,24 @@
44
package awsutil
55

66
import (
7+
"context"
78
"errors"
89
"fmt"
910
"testing"
1011

11-
"github.com/aws/aws-sdk-go/service/iam"
12-
"github.com/aws/aws-sdk-go/service/iam/iamiface"
13-
"github.com/aws/aws-sdk-go/service/sts"
14-
"github.com/aws/aws-sdk-go/service/sts/stsiface"
12+
"github.com/aws/aws-sdk-go-v2/service/iam"
13+
"github.com/aws/aws-sdk-go-v2/service/sts"
1514
"github.com/stretchr/testify/require"
1615
)
1716

1817
const testOptionErr = "test option error"
19-
const testBadClientType = "badclienttype"
20-
21-
func testWithBadClientType(o *options) error {
22-
o.withClientType = testBadClientType
23-
return nil
24-
}
2518

2619
func TestCredentialsConfigIAMClient(t *testing.T) {
2720
cases := []struct {
2821
name string
2922
credentialsConfig *CredentialsConfig
3023
opts []Option
31-
require func(t *testing.T, actual iamiface.IAMAPI)
24+
require func(t *testing.T, actual IAMClient)
3225
requireErr string
3326
}{
3427
{
@@ -37,17 +30,11 @@ func TestCredentialsConfigIAMClient(t *testing.T) {
3730
opts: []Option{MockOptionErr(errors.New(testOptionErr))},
3831
requireErr: fmt.Sprintf("error reading options: %s", testOptionErr),
3932
},
40-
{
41-
name: "session error",
42-
credentialsConfig: &CredentialsConfig{},
43-
opts: []Option{testWithBadClientType},
44-
requireErr: fmt.Sprintf("error calling GetSession: unknown client type %q in GetSession", testBadClientType),
45-
},
4633
{
4734
name: "with mock IAM session",
4835
credentialsConfig: &CredentialsConfig{},
4936
opts: []Option{WithIAMAPIFunc(NewMockIAM())},
50-
require: func(t *testing.T, actual iamiface.IAMAPI) {
37+
require: func(t *testing.T, actual IAMClient) {
5138
t.Helper()
5239
require := require.New(t)
5340
require.Equal(&MockIAM{}, actual)
@@ -57,10 +44,10 @@ func TestCredentialsConfigIAMClient(t *testing.T) {
5744
name: "no mock client",
5845
credentialsConfig: &CredentialsConfig{},
5946
opts: []Option{},
60-
require: func(t *testing.T, actual iamiface.IAMAPI) {
47+
require: func(t *testing.T, actual IAMClient) {
6148
t.Helper()
6249
require := require.New(t)
63-
require.IsType(&iam.IAM{}, actual)
50+
require.IsType(&iam.Client{}, actual)
6451
},
6552
},
6653
}
@@ -69,7 +56,7 @@ func TestCredentialsConfigIAMClient(t *testing.T) {
6956
tc := tc
7057
t.Run(tc.name, func(t *testing.T) {
7158
require := require.New(t)
72-
actual, err := tc.credentialsConfig.IAMClient(tc.opts...)
59+
actual, err := tc.credentialsConfig.IAMClient(context.TODO(), tc.opts...)
7360
if tc.requireErr != "" {
7461
require.EqualError(err, tc.requireErr)
7562
return
@@ -86,7 +73,7 @@ func TestCredentialsConfigSTSClient(t *testing.T) {
8673
name string
8774
credentialsConfig *CredentialsConfig
8875
opts []Option
89-
require func(t *testing.T, actual stsiface.STSAPI)
76+
require func(t *testing.T, actual STSClient)
9077
requireErr string
9178
}{
9279
{
@@ -95,17 +82,11 @@ func TestCredentialsConfigSTSClient(t *testing.T) {
9582
opts: []Option{MockOptionErr(errors.New(testOptionErr))},
9683
requireErr: fmt.Sprintf("error reading options: %s", testOptionErr),
9784
},
98-
{
99-
name: "session error",
100-
credentialsConfig: &CredentialsConfig{},
101-
opts: []Option{testWithBadClientType},
102-
requireErr: fmt.Sprintf("error calling GetSession: unknown client type %q in GetSession", testBadClientType),
103-
},
10485
{
10586
name: "with mock STS session",
10687
credentialsConfig: &CredentialsConfig{},
10788
opts: []Option{WithSTSAPIFunc(NewMockSTS())},
108-
require: func(t *testing.T, actual stsiface.STSAPI) {
89+
require: func(t *testing.T, actual STSClient) {
10990
t.Helper()
11091
require := require.New(t)
11192
require.Equal(&MockSTS{}, actual)
@@ -115,10 +96,10 @@ func TestCredentialsConfigSTSClient(t *testing.T) {
11596
name: "no mock client",
11697
credentialsConfig: &CredentialsConfig{},
11798
opts: []Option{},
118-
require: func(t *testing.T, actual stsiface.STSAPI) {
99+
require: func(t *testing.T, actual STSClient) {
119100
t.Helper()
120101
require := require.New(t)
121-
require.IsType(&sts.STS{}, actual)
102+
require.IsType(&sts.Client{}, actual)
122103
},
123104
},
124105
}
@@ -127,7 +108,7 @@ func TestCredentialsConfigSTSClient(t *testing.T) {
127108
tc := tc
128109
t.Run(tc.name, func(t *testing.T) {
129110
require := require.New(t)
130-
actual, err := tc.credentialsConfig.STSClient(tc.opts...)
111+
actual, err := tc.credentialsConfig.STSClient(context.TODO(), tc.opts...)
131112
if tc.requireErr != "" {
132113
require.EqualError(err, tc.requireErr)
133114
return

awsutil/error.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package awsutil
66
import (
77
"errors"
88

9-
awsRequest "github.com/aws/aws-sdk-go/aws/request"
9+
"github.com/aws/aws-sdk-go-v2/aws/retry"
1010
multierror "github.com/hashicorp/go-multierror"
1111
)
1212

@@ -15,10 +15,10 @@ var ErrUpstreamRateLimited = errors.New("upstream rate limited")
1515
// CheckAWSError will examine an error and convert to a logical error if
1616
// appropriate. If no appropriate error is found, return nil
1717
func CheckAWSError(err error) error {
18-
// IsErrorThrottle will check if the error returned is one that matches
19-
// known request limiting errors:
20-
// https://github.com/aws/aws-sdk-go/blob/488d634b5a699b9118ac2befb5135922b4a77210/aws/request/retryer.go#L35
21-
if awsRequest.IsErrorThrottle(err) {
18+
retryErr := retry.ThrottleErrorCode{
19+
Codes: retry.DefaultThrottleErrorCodes,
20+
}
21+
if retryErr.IsErrorThrottle(err).Bool() {
2222
return ErrUpstreamRateLimited
2323
}
2424
return nil

awsutil/error_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"fmt"
88
"testing"
99

10-
"github.com/aws/aws-sdk-go/aws/awserr"
10+
awserr "github.com/aws/smithy-go"
1111
multierror "github.com/hashicorp/go-multierror"
1212
)
1313

@@ -23,12 +23,16 @@ func Test_CheckAWSError(t *testing.T) {
2323
},
2424
{
2525
Name: "Upstream throttle error",
26-
Err: awserr.New("Throttling", "", nil),
26+
Err: MockAWSThrottleErr(),
2727
Expected: ErrUpstreamRateLimited,
2828
},
2929
{
30-
Name: "Upstream RequestLimitExceeded",
31-
Err: awserr.New("RequestLimitExceeded", "Request rate limited", nil),
30+
Name: "Upstream RequestLimitExceeded",
31+
Err: &MockAWSErr{
32+
Code: "RequestLimitExceeded",
33+
Message: "Request rate limited",
34+
Fault: awserr.FaultServer,
35+
},
3236
Expected: ErrUpstreamRateLimited,
3337
},
3438
}
@@ -50,7 +54,7 @@ func Test_CheckAWSError(t *testing.T) {
5054
}
5155

5256
func Test_AppendRateLimitedError(t *testing.T) {
53-
awsErr := awserr.New("Throttling", "", nil)
57+
throttleErr := MockAWSThrottleErr()
5458
testCases := []struct {
5559
Name string
5660
Err error
@@ -63,8 +67,8 @@ func Test_AppendRateLimitedError(t *testing.T) {
6367
},
6468
{
6569
Name: "Upstream throttle error",
66-
Err: awsErr,
67-
Expected: multierror.Append(awsErr, ErrUpstreamRateLimited),
70+
Err: throttleErr,
71+
Expected: multierror.Append(throttleErr, ErrUpstreamRateLimited),
6872
},
6973
{
7074
Name: "Nil",

0 commit comments

Comments
 (0)