Fall back to default AWS credential chain when specified profile is not found#1098
Conversation
When an AWS profile is specified (e.g. via `profile=dev` query parameter) but does not exist in ~/.aws/config, instead of panicking with "failed to get shared config profile, dev", the provider now falls back to the default AWS credential chain (EC2 instance profile, environment variables, etc.). - Modified newConfig in pkg/awsclicompat/session.go to catch config.SharedConfigProfileNotExistError and retry loading config without the profile option - Extracted buildBaseOpts helper to avoid duplicating region/endpoint/ log-level option building - Added TestNewConfigProfileNotFoundFallback test Agent-Logs-Url: https://github.com/helmfile/vals/sessions/5d5f9e0c-bca5-4adf-a30c-0960ae95e7fe Co-authored-by: yxxhero <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR updates the AWS SDK config-loading logic used by vals’ AWS-backed providers so that specifying a non-existent AWS profile no longer causes a panic, and instead falls back to the AWS SDK’s default credential chain (env/IMDS/etc.), improving compatibility for EC2/IAM-role environments.
Changes:
- Add a profile-not-found fallback path in
newConfigwhenLoadDefaultConfigfails due to a missing shared config profile. - Refactor shared AWS config options (region/endpoint/log-level) into
buildBaseOptsfor reuse across the initial load and fallback load. - Add a unit test intended to verify the fallback behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pkg/awsclicompat/session.go |
Implements the missing-profile fallback and extracts shared LoadDefaultConfig options into buildBaseOpts. |
pkg/awsclicompat/session_test.go |
Adds a test for the missing-profile fallback behavior in newConfig. |
| switch { | ||
| case profile != "": | ||
| opts = append(opts, config.WithSharedConfigProfile(profile)) | ||
| opts = append(baseOpts, config.WithSharedConfigProfile(profile)) | ||
| case os.Getenv("FORCE_AWS_PROFILE") == "true": | ||
| if awsProfile := os.Getenv("AWS_PROFILE"); awsProfile != "" { | ||
| opts = append(opts, config.WithSharedConfigProfile(awsProfile)) | ||
| opts = append(baseOpts, config.WithSharedConfigProfile(awsProfile)) | ||
| } | ||
| } | ||
|
|
||
| cfg, err := config.LoadDefaultConfig(ctx, opts...) | ||
| if err != nil { | ||
| // If the specified profile doesn't exist, fall back to default credentials | ||
| // (e.g. EC2 instance profile, environment variables, etc.) | ||
| var profileNotExist config.SharedConfigProfileNotExistError | ||
| if profile != "" && errors.As(err, &profileNotExist) { | ||
| return config.LoadDefaultConfig(ctx, baseOpts...) | ||
| } |
There was a problem hiding this comment.
The profile-not-found fallback only triggers when the profile parameter is non-empty. If the profile was selected via FORCE_AWS_PROFILE=true + AWS_PROFILE, LoadDefaultConfig can still return SharedConfigProfileNotExistError and NewSession will panic. Consider tracking the effective selected profile name (param or env) and applying the same fallback (or update the comment to clarify fallback only applies to the explicit profile argument).
| func TestNewConfigProfileNotFoundFallback(t *testing.T) { | ||
| // Use a profile name that is guaranteed not to exist on this machine. | ||
| const nonExistentProfile = "vals-test-profile-does-not-exist-12345" | ||
|
|
||
| ctx := context.Background() | ||
| cfg, err := newConfig(ctx, "", nonExistentProfile, "") | ||
| if err != nil { | ||
| t.Fatalf("newConfig with non-existent profile should fall back to default credentials, got error: %v", err) | ||
| } | ||
|
|
||
| // The returned config must have a non-nil credentials provider, which indicates | ||
| // the fallback to the default credential chain succeeded. | ||
| if cfg.Credentials == nil { | ||
| t.Error("expected cfg.Credentials to be non-nil after profile fallback") | ||
| } | ||
| } |
There was a problem hiding this comment.
This test is not hermetic: it relies on a "guaranteed not to exist" profile name and on the machine/CI environment having a resolvable region and no conflicting AWS env vars. To make it deterministic, set AWS_CONFIG_FILE/AWS_SHARED_CREDENTIALS_FILE to temp empty files (or temp files without the profile), clear AWS_PROFILE/FORCE_AWS_PROFILE/AWS_SDK_LOAD_CONFIG, and pass an explicit region (e.g. us-east-1) so LoadDefaultConfig doesn’t depend on external config/IMDS.
|
@copilot fix comments and optimize code. |
…ation - Make TestNewConfigProfileNotFoundFallback hermetic: point to empty temp AWS config/credentials files and clear conflicting env vars (AWS_PROFILE, FORCE_AWS_PROFILE, AWS_ACCESS_KEY_ID, etc.) - Track effectiveProfile explicitly to consolidate profile selection from both the explicit param and FORCE_AWS_PROFILE paths - Restrict fallback to explicit `profile` param only (not FORCE_AWS_PROFILE), since the AWS SDK reads AWS_PROFILE from env even in the fallback load; document this in the function comment - Remove TestNewConfigForceProfileNotFoundFallback (behaviour unchanged) Agent-Logs-Url: https://github.com/helmfile/vals/sessions/55a70500-5118-449e-8a12-4142cc9e80f2 Co-authored-by: yxxhero <[email protected]>
Done in c0db520:
|
| // Build full options including profile selection | ||
| opts := baseOpts | ||
| if effectiveProfile != "" { | ||
| opts = append(baseOpts, config.WithSharedConfigProfile(effectiveProfile)) |
There was a problem hiding this comment.
In newConfig, the slice append uses append(baseOpts, ...) even though opts is the working slice. While it behaves the same today, it’s easy to misread and can be error-prone if opts is modified later. Prefer appending to opts directly for clarity (and to avoid subtle slice-aliasing surprises).
| opts = append(baseOpts, config.WithSharedConfigProfile(effectiveProfile)) | |
| opts = append(opts, config.WithSharedConfigProfile(effectiveProfile)) |
| // The returned config must have a non-nil credentials provider, which indicates | ||
| // the fallback to the default credential chain succeeded. | ||
| if cfg.Credentials == nil { | ||
| t.Error("expected cfg.Credentials to be non-nil after profile fallback") | ||
| } |
There was a problem hiding this comment.
This test’s assertion (cfg.Credentials != nil) doesn’t actually prove the profile-not-found fallback was taken—config.LoadDefaultConfig typically populates a credentials provider even when no credentials can be resolved yet, so this can pass even if the fallback logic regresses. Consider first asserting that a direct config.LoadDefaultConfig with config.WithSharedConfigProfile(nonExistentProfile) returns SharedConfigProfileNotExistError, then asserting newConfig returns nil error under the same env setup.
|
@copilot fix comments and optimize code. |
…ertion - Use append(opts, ...) instead of append(baseOpts, ...) to avoid subtle slice-aliasing surprises if opts is modified later - Replace weak cfg.Credentials != nil assertion with a two-step check: first confirm direct LoadDefaultConfig returns SharedConfigProfileNotExistError (proving the fallback code path is actually needed), then assert newConfig returns nil error (proving the fallback is taken) - Rename second err to fallbackErr for clarity - Add errors and awsconfig imports to session_test.go Agent-Logs-Url: https://github.com/helmfile/vals/sessions/5aba2e69-9351-436c-bd3d-7ea90b97f4bd Co-authored-by: yxxhero <[email protected]>
Done in 46cce94:
|
| cfg, err := config.LoadDefaultConfig(ctx, opts...) | ||
| if err != nil { | ||
| // If the explicit profile parameter doesn't exist, fall back to the default | ||
| // credential chain (e.g. EC2 instance profile, environment variables, etc.). | ||
| // The fallback is not applied for FORCE_AWS_PROFILE/AWS_PROFILE env vars because | ||
| // the AWS SDK would still read AWS_PROFILE from the environment in the fallback load. | ||
| var profileNotExist config.SharedConfigProfileNotExistError | ||
| if profile != "" && errors.As(err, &profileNotExist) { | ||
| return config.LoadDefaultConfig(ctx, baseOpts...) | ||
| } |
There was a problem hiding this comment.
In the profile-not-found fallback you call config.LoadDefaultConfig(ctx, baseOpts...) without explicitly overriding the profile. If AWS_PROFILE is set in the environment (even when FORCE_AWS_PROFILE is not), the SDK will still honor it during the fallback load, which can cause the same SharedConfigProfileNotExistError and defeat the intended EC2/instance-role fallback. Consider explicitly overriding the profile for the fallback load (or otherwise ensuring the fallback ignores AWS_PROFILE), and add a regression test that sets AWS_PROFILE to the missing profile while passing the explicit profile parameter.
|
@copilot fix comments and optimize code. |
In the profile-not-found fallback, use config.WithSharedConfigProfile("default")
to explicitly override AWS_PROFILE from the environment. Without this override,
the SDK would still honor AWS_PROFILE during the fallback load — if AWS_PROFILE
points to the same missing profile, the fallback would fail with the same
SharedConfigProfileNotExistError and defeat the intended EC2/instance-role fallback.
Also adds TestNewConfigProfileNotFoundFallbackWithAWSProfileEnv, a regression test
that sets AWS_PROFILE to the missing profile while passing the same explicit profile
parameter, proving the fallback now succeeds in that scenario.
Both fallback tests use a config file with a [default] section, reflecting the
realistic setup where a user has ~/.aws/config with a default profile but not the
one referenced in vals.
Agent-Logs-Url: https://github.com/helmfile/vals/sessions/4ccccc13-bfff-49b3-9807-bdbc717ac5c3
Co-authored-by: yxxhero <[email protected]>
Done in 402c52f:
|
| cfg, err := config.LoadDefaultConfig(ctx, opts...) | ||
| if err != nil { | ||
| // If the explicit profile parameter doesn't exist, fall back to the default | ||
| // credential chain (e.g. EC2 instance profile, environment variables, etc.). | ||
| // Explicitly set profile to "default" so that AWS_PROFILE from the environment | ||
| // is not honored during the fallback (it might be set to the same missing profile | ||
| // and cause the same error). Using "default" falls through to the standard credential | ||
| // chain (env vars, EC2 IMDS, etc.) when no credentials are configured in the profile. | ||
| var profileNotExist config.SharedConfigProfileNotExistError | ||
| if profile != "" && errors.As(err, &profileNotExist) { | ||
| return config.LoadDefaultConfig(ctx, append(baseOpts, config.WithSharedConfigProfile("default"))...) | ||
| } | ||
| return aws.Config{}, err |
There was a problem hiding this comment.
The profile-not-found path silently falls back to the default credential chain. This can mask typos/misconfiguration of an explicitly requested profile and may result in vals using unintended credentials/accounts. Consider emitting a warning when the fallback triggers (or making the fallback opt-in) so users can detect when their requested profile wasn’t actually used.
When an AWS profile is specified (e.g.
ref+awssecrets://mysecret?profile=dev) but doesn't exist in~/.aws/config, vals panics withfailed to get shared config profile, dev. This breaks EC2 users who rely on instance IAM roles and have no local AWS config.Changes
pkg/awsclicompat/session.go: InnewConfig, catchconfig.SharedConfigProfileNotExistErrorand retryLoadDefaultConfigwithout the profile option, allowing the SDK's default credential chain (env vars, EC2 instance metadata, etc.) to take over. ExtractedbuildBaseOptsto share region/endpoint/log-level options between the initial load and the fallback.pkg/awsclicompat/session_test.go: AddedTestNewConfigProfileNotFoundFallbackverifying that a non-existent profile silently falls back to default credentials.Behavior