Skip to content

Commit a90c9a5

Browse files
authored
chore(session): try retrieving credentials within 10s if not err (#3645)
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
1 parent 0e32c1c commit a90c9a5

File tree

10 files changed

+295
-14
lines changed

10 files changed

+295
-14
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ site-local:
144144
gen-mocks: tools
145145
GOBIN=${GOBIN} go get github.com/golang/mock/mockgen
146146
# TODO: make this more extensible?
147+
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/sessions/mocks/mock_sessions.go -source=./internal/pkg/aws/sessions/sessions.go
147148
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/cli/mocks/mock_rg.go -source=./internal/pkg/cli/env_delete.go resourceGetter
148149
${GOBIN}/mockgen -source=./internal/pkg/term/progress/spinner.go -package=mocks -destination=./internal/pkg/term/progress/mocks/mock_spinner.go
149150
${GOBIN}/mockgen -source=./internal/pkg/term/progress/render.go -package=mocks -destination=./internal/pkg/term/progress/mocks/mock_render.go

internal/pkg/aws/sessions/errors.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package sessions
55

66
import (
77
"fmt"
8+
"strings"
89

910
"github.com/aws/copilot-cli/internal/pkg/term/color"
1011
)
@@ -24,3 +25,33 @@ func (e *errMissingRegion) RecommendActions() string { // implements new actionR
2425
- Alternatively, you can run %s to set the environment variable.
2526
More information: https://aws.github.io/copilot-cli/docs/credentials/`, color.HighlightCode("export AWS_REGION=<application region>"))
2627
}
28+
29+
type errCredRetrieval struct {
30+
profile string
31+
parentErr error
32+
}
33+
34+
// Implements error interface.
35+
func (e *errCredRetrieval) Error() string {
36+
return e.parentErr.Error()
37+
}
38+
39+
// RecommendActions returns recommended actions to be taken after the error.
40+
// Implements main.actionRecommender interface.
41+
func (e *errCredRetrieval) RecommendActions() string {
42+
notice := "It looks like your credential settings are misconfigured or missing"
43+
if e.profile != "" {
44+
notice = fmt.Sprintf("It looks like your profile [%s] is misconfigured or missing", e.profile)
45+
}
46+
return fmt.Sprintf(`%s:
47+
https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
48+
- We recommend including your credentials in the shared credentials file.
49+
- Alternatively, you can also set credentials through
50+
* Environment Variables
51+
* EC2 Instance Metadata (credentials only)
52+
More information: https://aws.github.io/copilot-cli/docs/credentials/`, notice)
53+
}
54+
55+
func isCredRetrievalErr(err error) bool {
56+
return strings.Contains(err.Error(), "context deadline exceeded") || strings.Contains(err.Error(), "NoCredentialProviders")
57+
}

internal/pkg/aws/sessions/mocks/mock_sessions.go

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

internal/pkg/aws/sessions/sessions.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ type Provider struct {
3939
defaultSess *session.Session
4040

4141
// Metadata associated with the provider.
42-
userAgentExtras []string
42+
userAgentExtras []string
43+
sessionValidator sessionValidator
44+
}
45+
46+
type sessionValidator interface {
47+
ValidateCredentials(sess *session.Session) (credentials.Value, error)
4348
}
4449

4550
var instance *Provider
@@ -48,7 +53,9 @@ var once sync.Once
4853
// ImmutableProvider returns an immutable session Provider with the options applied.
4954
func ImmutableProvider(options ...func(*Provider)) *Provider {
5055
once.Do(func() {
51-
instance = &Provider{}
56+
instance = &Provider{
57+
sessionValidator: &validator{},
58+
}
5259
for _, option := range options {
5360
option(instance)
5461
}
@@ -104,6 +111,12 @@ func (p *Provider) FromProfile(name string) (*session.Session, error) {
104111
if aws.StringValue(sess.Config.Region) == "" {
105112
return nil, &errMissingRegion{}
106113
}
114+
if _, err := p.sessionValidator.ValidateCredentials(sess); err != nil {
115+
if isCredRetrievalErr(err) {
116+
return nil, &errCredRetrieval{profile: name, parentErr: err}
117+
}
118+
return nil, err
119+
}
107120
sess.Handlers.Build.PushBackNamed(p.userAgentHandler())
108121
return sess, nil
109122
}
@@ -155,6 +168,13 @@ func (p *Provider) defaultSession() (*session.Session, error) {
155168
if err != nil {
156169
return nil, err
157170
}
171+
if _, err = p.sessionValidator.ValidateCredentials(sess); err != nil {
172+
if isCredRetrievalErr(err) {
173+
return nil, &errCredRetrieval{parentErr: err}
174+
}
175+
return nil, err
176+
}
177+
158178
sess.Handlers.Build.PushBackNamed(p.userAgentHandler())
159179
p.defaultSess = sess
160180
return sess, nil
@@ -202,3 +222,11 @@ func (p *Provider) userAgentHandler() request.NamedHandler {
202222
Fn: request.MakeAddToUserAgentHandler(userAgentProductName, version.Version, extras...),
203223
}
204224
}
225+
226+
type validator struct{}
227+
228+
func (v *validator) ValidateCredentials(sess *session.Session) (credentials.Value, error) {
229+
ctx, cancel := context.WithTimeout(context.Background(), credsTimeout)
230+
defer cancel()
231+
return sess.Config.Credentials.GetWithContext(ctx)
232+
}

internal/pkg/aws/sessions/sessions_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
package sessions
55

66
import (
7+
"context"
78
"errors"
9+
"github.com/aws/copilot-cli/internal/pkg/aws/sessions/mocks"
10+
"github.com/golang/mock/gomock"
811
"os"
912
"testing"
1013

@@ -171,6 +174,13 @@ func TestProvider_FromProfile(t *testing.T) {
171174
})
172175

173176
t.Run("region information present", func(t *testing.T) {
177+
178+
ctrl := gomock.NewController(t)
179+
defer ctrl.Finish()
180+
181+
m := mocks.NewMocksessionValidator(ctrl)
182+
m.EXPECT().ValidateCredentials(gomock.Any()).Return(credentials.Value{}, nil)
183+
174184
ogRegion := os.Getenv("AWS_REGION")
175185
defer func() {
176186
err := restoreEnvVar("AWS_REGION", ogRegion)
@@ -183,12 +193,47 @@ func TestProvider_FromProfile(t *testing.T) {
183193
require.NoError(t, err)
184194

185195
// WHEN
186-
sess, err := ImmutableProvider().FromProfile("walk-like-an-egyptian")
196+
provider := &Provider{
197+
sessionValidator: m,
198+
}
199+
200+
sess, err := provider.FromProfile("walk-like-an-egyptian")
187201

188202
// THEN
189203
require.NoError(t, err)
190204
require.Equal(t, "us-west-2", *sess.Config.Region)
191205
})
206+
207+
t.Run("session credentials are incorrect", func(t *testing.T) {
208+
209+
ctrl := gomock.NewController(t)
210+
defer ctrl.Finish()
211+
212+
m := mocks.NewMocksessionValidator(ctrl)
213+
m.EXPECT().ValidateCredentials(gomock.Any()).Return(credentials.Value{}, context.DeadlineExceeded)
214+
215+
ogRegion := os.Getenv("AWS_REGION")
216+
defer func() {
217+
err := restoreEnvVar("AWS_REGION", ogRegion)
218+
require.NoError(t, err)
219+
}()
220+
221+
// Since "walk-like-an-egyptian" is (very likely) a non-existent profile, whether the region information
222+
// is missing depends on whether the `AWS_REGION` environment variable is set.
223+
err := os.Setenv("AWS_REGION", "us-west-2")
224+
require.NoError(t, err)
225+
226+
// WHEN
227+
provider := &Provider{
228+
sessionValidator: m,
229+
}
230+
231+
sess, err := provider.FromProfile("walk-like-an-egyptian")
232+
233+
// THEN
234+
require.EqualError(t, err, "context deadline exceeded")
235+
require.Nil(t, sess)
236+
})
192237
}
193238

194239
func restoreEnvVar(key string, originalValue string) error {

internal/pkg/cli/job_delete_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ package cli
66
import (
77
"errors"
88
"fmt"
9+
"github.com/aws/aws-sdk-go/aws"
910
"testing"
1011

1112
"github.com/aws/aws-sdk-go/aws/session"
1213

13-
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
1414
"github.com/aws/copilot-cli/internal/pkg/cli/mocks"
1515
"github.com/aws/copilot-cli/internal/pkg/config"
1616
"github.com/aws/copilot-cli/internal/pkg/term/log"
@@ -292,7 +292,7 @@ func TestDeleteJobOpts_Ask(t *testing.T) {
292292
type deleteJobMocks struct {
293293
store *mocks.Mockstore
294294
secretsmanager *mocks.MocksecretsManager
295-
sessProvider *sessions.Provider
295+
sessProvider *mocks.MocksessionProvider
296296
appCFN *mocks.MockjobRemoverFromApp
297297
spinner *mocks.Mockprogress
298298
jobCFN *mocks.MockwlDeleter
@@ -334,6 +334,8 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
334334
gomock.InOrder(
335335
// appEnvironments
336336
mocks.store.EXPECT().ListEnvironments(gomock.Eq(mockAppName)).Times(1).Return(mockEnvs, nil),
337+
338+
mocks.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{}, nil),
337339
// deleteStacks
338340
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobStackDeleteStart, mockJobName, mockEnvName)),
339341
mocks.jobCFN.EXPECT().DeleteWorkload(gomock.Any()).Return(nil),
@@ -342,9 +344,10 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
342344
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobTasksStopStart, mockJobName, mockEnvName)),
343345
mocks.ecs.EXPECT().StopWorkloadTasks(mockAppName, mockEnvName, mockJobName).Return(nil),
344346
mocks.spinner.EXPECT().Stop(log.Ssuccessf(fmtJobTasksStopComplete, mockJobName, mockEnvName)),
347+
348+
mocks.sessProvider.EXPECT().DefaultWithRegion(gomock.Any()).Return(&session.Session{}, nil),
345349
// emptyECRRepos
346350
mocks.ecr.EXPECT().ClearRepository(mockRepo).Return(nil),
347-
348351
// removeJobFromApp
349352
mocks.store.EXPECT().GetApplication(mockAppName).Return(mockApp, nil),
350353
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobDeleteResourcesStart, mockJobName, mockAppName)),
@@ -368,6 +371,7 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
368371
gomock.InOrder(
369372
// appEnvironments
370373
mocks.store.EXPECT().GetEnvironment(mockAppName, mockEnvName).Times(1).Return(mockEnv, nil),
374+
mocks.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{}, nil),
371375
// deleteStacks
372376
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobStackDeleteStart, mockJobName, mockEnvName)),
373377
mocks.jobCFN.EXPECT().DeleteWorkload(gomock.Any()).Return(nil),
@@ -397,6 +401,11 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
397401
gomock.InOrder(
398402
// appEnvironments
399403
mocks.store.EXPECT().GetEnvironment(mockAppName, mockEnvName).Times(1).Return(mockEnv, nil),
404+
mocks.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{
405+
Config: &aws.Config{
406+
Region: aws.String("mockRegion"),
407+
},
408+
}, nil),
400409
// deleteStacks
401410
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobStackDeleteStart, mockJobName, mockEnvName)),
402411
mocks.jobCFN.EXPECT().DeleteWorkload(gomock.Any()).Return(testError),
@@ -413,6 +422,11 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
413422
gomock.InOrder(
414423
// appEnvironments
415424
mocks.store.EXPECT().GetEnvironment(mockAppName, mockEnvName).Times(1).Return(mockEnv, nil),
425+
mocks.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{
426+
Config: &aws.Config{
427+
Region: aws.String("mockRegion"),
428+
},
429+
}, nil),
416430
// deleteStacks
417431
mocks.spinner.EXPECT().Start(fmt.Sprintf(fmtJobStackDeleteStart, mockJobName, mockEnvName)),
418432
mocks.jobCFN.EXPECT().DeleteWorkload(gomock.Any()).Return(nil),
@@ -435,7 +449,7 @@ func TestDeleteJobOpts_Execute(t *testing.T) {
435449
// GIVEN
436450
mockstore := mocks.NewMockstore(ctrl)
437451
mockSecretsManager := mocks.NewMocksecretsManager(ctrl)
438-
mockSession := sessions.ImmutableProvider()
452+
mockSession := mocks.NewMocksessionProvider(ctrl)
439453
mockAppCFN := mocks.NewMockjobRemoverFromApp(ctrl)
440454
mockJobCFN := mocks.NewMockwlDeleter(ctrl)
441455
mockSpinner := mocks.NewMockprogress(ctrl)

0 commit comments

Comments
 (0)