Skip to content

Commit

Permalink
Improve error messages around AWS config (#3310)
Browse files Browse the repository at this point in the history
Should fully address #2285
after pulumi/pulumi-terraform-bridge#1640

This makes the error messages when the user has no credentials or no
region configured better and more actionable:

Before, no credentials configured:

```
error: pulumi:providers:aws resource 'default_6_18_2' has a problem: could not validate provider configuration: unable to validate AWS credentials.
    Details: No valid credential sources found. Please see https://www.pulumi.com/registry/packages/aws/installation-configuration/
    for more information about providing credentials.

    Error: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, request canceled, context deadline exceeded

    Make sure you have set your AWS region, e.g. `pulumi config set aws:region us-west-2`.
```

The line about the region is irrelevant here.

After, no credentials configured:
```
Diagnostics:
  pulumi:providers:aws (default_6_18_2):
    error: pulumi:providers:aws resource 'default_6_18_2' has a problem: could not validate provider configuration: unable to validate AWS credentials.
    Details: No valid credential sources found. Please see https://www.pulumi.com/registry/packages/aws/installation-configuration/
    for more information about providing credentials.

    Error: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, request canceled, context deadline exceeded
```


Before, no region configured:
```
Diagnostics:
  pulumi:providers:aws (default_6_18_2):
    error: pulumi:providers:aws resource 'default_6_18_2' has a problem: could not validate provider configuration: unable to validate AWS credentials.
    Details: validating provider credentials: retrieving caller identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 0, RequestID: , request send failed, Post "https://sts..amazonaws.com/": dial tcp: lookup sts..amazonaws.com: no such host
    Make sure you have set your AWS region, e.g. `pulumi config set aws:region us-west-2`.
```

Here, it is not at all clear that it is the region at fault, since the
note about setting the region shows up every time.

After, no region configured:
```
Diagnostics:
  pulumi:providers:aws (default_6_18_2):
    error: pulumi:providers:aws resource 'default_6_18_2' has a problem: could not validate provider configuration: missing region information
    Make sure you have set your AWS region, e.g. `pulumi config set aws:region us-west-2`.
    Details: validating provider credentials: retrieving caller identity from STS: operation error STS: GetCallerIdentity, https response error StatusCode: 0, RequestID: , request send failed, Post "https://sts/..amazonaws.com/": dial tcp: lookup sts..amazonaws.com: no such host
```
The note about `config set aws:region` only shows up in this error case,
so clearly actionable.


For comparison, upstream, no credentials configured:
```
│ Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.
│
│ Please see https://registry.terraform.io/providers/hashicorp/aws
│ for more information about providing credentials.
│
│ AWS Error: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, http response error StatusCode: 404, request to EC2 IMDS failed
│
│
│   with provider["registry.terraform.io/hashicorp/aws"],
│   on main.tf line 12, in provider "aws":
│   12: provider "aws" {
│
╵
```
  • Loading branch information
VenelinMartinov authored Jan 31, 2024
1 parent 8dc14b1 commit be20793
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ sdk/python/*.egg-info


sdk/python/venv
go.work
go.work.sum
5 changes: 4 additions & 1 deletion examples/diagnostic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ func TestCredentialsErrorNotDuplicated(t *testing.T) {
"AWS_SECRET_ACCESS_KEY=INVALID",
},
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.Equal(t, 1, strings.Count(outputBuf.String(), "The security token included in the request is invalid"))
assert.Contains(t, outputBuf.String(), "Invalid credentials configured.")
assert.Equal(t, 1, strings.Count(outputBuf.String(),
"Please see https://www.pulumi.com/registry/packages/aws/installation-configuration/ "+
"for more information about providing credentials."))
},
}

Expand Down
152 changes: 152 additions & 0 deletions provider/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package provider

import (
"context"
"os"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -117,6 +118,157 @@ func TestCheckConfigFastWithCustomEndpoints(t *testing.T) {
require.Truef(t, time0.Add(cutoff).After(time.Now()), "CheckConfig with custom endpoints is taking more than %v", cutoff)
}

func unsetAWSEnv() {
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
os.Unsetenv("AWS_SESSION_TOKEN")
os.Unsetenv("AWS_DEFAULT_REGION")
os.Unsetenv("AWS_PROFILE")
os.Unsetenv("AWS_ACCESS_KEY_ID")
os.Unsetenv("AWS_SECRET_ACCESS_KEY")
os.Unsetenv("AWS_REGION")
os.Setenv("AWS_CONFIG_FILE", "non-existent/config.json")
os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "non-existent/credentials")
}

func skipIfNotShort(t *testing.T) {
if !testing.Short() {
t.Skipf("Skipping test in non-short mode")
}
}

func TestMissingCredentialsErrorMessage(t *testing.T) {
skipIfNotShort(t)
unsetAWSEnv()
os.Setenv("AWS_SKIP_CREDENTIALS_VALIDATION", "false")

replaySequence(t, `
[{
"method": "/pulumirpc.ResourceProvider/CheckConfig",
"request": {
"urn": "urn:pulumi:dev::aws_no_creds::pulumi:providers:aws::default_6_18_2",
"olds": {},
"news": {
"version": "6.18.2"
}
},
"response": {
"inputs": "*",
"failures": [
{
"reason": "No valid credential sources found.\nPlease see https://www.pulumi.com/registry/packages/aws/installation-configuration/ for more information about providing credentials.\nNEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the correct and valid credentials are used.\nLearn more: https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials"
}
]
},
"metadata": {
"kind": "resource",
"mode": "client",
"name": "aws"
}
}]`)
}

func TestMissingRegionErrorMessage(t *testing.T) {
skipIfNotShort(t)
unsetAWSEnv()
os.Setenv("AWS_ACCESS_KEY_ID", "VALID")
os.Setenv("AWS_SECRET_ACCESS_KEY", "VALID")
os.Setenv("AWS_SKIP_CREDENTIALS_VALIDATION", "false")

replaySequence(t, strings.ReplaceAll(`
[{
"method": "/pulumirpc.ResourceProvider/CheckConfig",
"request": {
"urn": "urn:pulumi:dev::aws_no_creds::pulumi:providers:aws::default_6_18_2",
"olds": {},
"news": {
"version": "6.18.2"
}
},
"response": {
"inputs": "*",
"failures": [
{
"reason": "Missing region information\nMake sure you have set your AWS region, e.g. '''pulumi config set aws:region us-west-2'''."
}
]
},
"metadata": {
"kind": "resource",
"mode": "client",
"name": "aws"
}
}]`, "'''", "`"))
}

func TestInvalidCredentialsErrorMessage(t *testing.T) {
skipIfNotShort(t)
unsetAWSEnv()
os.Setenv("AWS_ACCESS_KEY_ID", "INVALID")
os.Setenv("AWS_SECRET_ACCESS_KEY", "INVALID")
os.Setenv("AWS_REGION", "us-west-2")
os.Setenv("AWS_SKIP_CREDENTIALS_VALIDATION", "false")

replaySequence(t, `
[{
"method": "/pulumirpc.ResourceProvider/CheckConfig",
"request": {
"urn": "urn:pulumi:dev::aws_no_creds::pulumi:providers:aws::default_6_18_2",
"olds": {},
"news": {
"version": "6.18.2"
}
},
"response": {
"inputs": "*",
"failures": [
{
"reason": "Invalid credentials configured.\nPlease see https://www.pulumi.com/registry/packages/aws/installation-configuration/ for more information about providing credentials.\nNEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the correct and valid credentials are used.\nLearn more: https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials"
}
]
},
"metadata": {
"kind": "resource",
"mode": "client",
"name": "aws"
}
}]`)
}

func TestOtherFailureErrorMessage(t *testing.T) {
skipIfNotShort(t)
unsetAWSEnv()
os.Setenv("AWS_ACCESS_KEY_ID", "INVALID")
os.Setenv("AWS_SECRET_ACCESS_KEY", "INVALID")
os.Setenv("AWS_REGION", "us-west-2")
os.Setenv("AWS_PROFILE", "non-existent-profile")
os.Setenv("AWS_SKIP_CREDENTIALS_VALIDATION", "false")

replaySequence(t, `
[{
"method": "/pulumirpc.ResourceProvider/CheckConfig",
"request": {
"urn": "urn:pulumi:dev::aws_no_creds::pulumi:providers:aws::default_6_18_2",
"olds": {},
"news": {
"version": "6.18.2"
}
},
"response": {
"inputs": "*",
"failures": [
{
"reason": "unable to validate AWS credentials.\nDetails: loading configuration: failed to get shared config profile, non-existent-profile\n"
}
]
},
"metadata": {
"kind": "resource",
"mode": "client",
"name": "aws"
}
}]`)
}

func replaySequence(t *testing.T, sequence string) {
info := *Provider()
ctx := context.Background()
Expand Down
4 changes: 4 additions & 0 deletions provider/errors/expired_sso.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Failed to refresh cached SSO credentials.
Please refresh SSO login.
NEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the correct and valid credentials are used.
Learn more: https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials
4 changes: 4 additions & 0 deletions provider/errors/invalid_credentials.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Invalid credentials configured.
Please see https://www.pulumi.com/registry/packages/aws/installation-configuration/ for more information about providing credentials.
NEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the correct and valid credentials are used.
Learn more: https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials
4 changes: 4 additions & 0 deletions provider/errors/no_credentials.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
No valid credential sources found.
Please see https://www.pulumi.com/registry/packages/aws/installation-configuration/ for more information about providing credentials.
NEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the correct and valid credentials are used.
Learn more: https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials
2 changes: 2 additions & 0 deletions provider/errors/no_region.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Missing region information
Make sure you have set your AWS region, e.g. `pulumi config set aws:region us-west-2`.
6 changes: 5 additions & 1 deletion provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ func providerServer(t *testing.T) pulumirpc.ResourceProviderServer {
return p
}

func test(t *testing.T, dir string, opts ...providertest.Option) {
func skipIfShort(t *testing.T) {
if testing.Short() {
t.Skipf("Skipping in testing.Short() mode, assuming this is a CI run without AWS creds")
return
}
}

func test(t *testing.T, dir string, opts ...providertest.Option) {
skipIfShort(t)
opts = append(opts,
providertest.WithProviderName("aws"),
providertest.WithBaselineVersion("5.42.0"),
Expand Down
123 changes: 91 additions & 32 deletions provider/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,18 @@ func durationFromConfig(vars resource.PropertyMap, prop resource.PropertyKey) (*
return nil, nil
}

//go:embed errors/no_credentials.txt
var noCredentialsError string

//go:embed errors/invalid_credentials.txt
var invalidCredentialsError string

//go:embed errors/no_region.txt
var noRegionError string

//go:embed errors/expired_sso.txt
var expiredSSOError string

func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error {
config := &awsbase.Config{
AccessKey: stringValue(vars, "accessKey", []string{"AWS_ACCESS_KEY_ID"}),
Expand Down Expand Up @@ -664,48 +676,92 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error
config.SharedConfigFiles = []string{configPath}

if _, _, diag := awsbase.GetAwsConfig(context.Background(), config); diag != nil && diag.HasError() {
return fmt.Errorf("unable to validate AWS credentials. \n"+
"Details: %s\n"+
"Make sure you have set your AWS region, e.g. `pulumi config set aws:region us-west-2`. \n\n"+
"NEW: You can use Pulumi ESC to set up dynamic credentials with AWS OIDC to ensure the "+
"correct and valid credentials are used.\nLearn more: "+
"https://www.pulumi.com/registry/packages/aws/installation-configuration/#dynamically-generate-credentials",
formatDiags(diag))
formattedDiag := formatDiags(diag)
// Normally it'd query sts.REGION.amazonaws.com
// but if we query sts..amazonaws.com, then we don't have a region.
if strings.Contains(formattedDiag, "dial tcp: lookup sts..amazonaws.com: no such host") {
return tfbridge.CheckFailureError{
Failures: []tfbridge.CheckFailureErrorElement{
{
Reason: noRegionError,
Property: "",
},
},
}
}
if strings.Contains(formattedDiag, "no EC2 IMDS role found") {
return tfbridge.CheckFailureError{
Failures: []tfbridge.CheckFailureErrorElement{
{
Reason: noCredentialsError,
Property: "",
},
},
}
}
if strings.Contains(formattedDiag, "The security token included in the request is invalid") {
return tfbridge.CheckFailureError{
Failures: []tfbridge.CheckFailureErrorElement{
{
Reason: invalidCredentialsError,
Property: "",
},
},
}
}
if strings.Contains(formattedDiag, "failed to refresh cached credentials") {
return tfbridge.CheckFailureError{
Failures: []tfbridge.CheckFailureErrorElement{
{
Reason: expiredSSOError,
Property: "",
},
},
}
}

return tfbridge.CheckFailureError{
Failures: []tfbridge.CheckFailureErrorElement{
{
Reason: fmt.Sprintf("unable to validate AWS credentials.\nDetails: %s\n", formattedDiag),
Property: "",
},
},
}
}

return nil
}

// We should only run the validation once to avoid duplicating the reported errors.
var credentialsValidationRun atomic.Bool

// preConfigureCallback validates that AWS credentials can be successfully discovered. This emulates the credentials
// configuration subset of `github.com/terraform-providers/terraform-provider-aws/aws.providerConfigure`. We do this
// before passing control to the TF provider to ensure we can report actionable errors.
func preConfigureCallback(vars resource.PropertyMap, c shim.ResourceConfig) error {
skipCredentialsValidation := boolValue(vars, "skipCredentialsValidation",
[]string{"AWS_SKIP_CREDENTIALS_VALIDATION"})

// if we skipCredentialsValidation then we don't need to do anything in
// preConfigureCallback as this is an explicit operation
if skipCredentialsValidation {
log.Printf("[INFO] pulumi-aws: skip credentials validation")
return nil
}
func preConfigureCallback(alreadyRun *atomic.Bool) func(vars resource.PropertyMap, c shim.ResourceConfig) error {
return func(vars resource.PropertyMap, c shim.ResourceConfig) error {
skipCredentialsValidation := boolValue(vars, "skipCredentialsValidation",
[]string{"AWS_SKIP_CREDENTIALS_VALIDATION"})

// if we skipCredentialsValidation then we don't need to do anything in
// preConfigureCallback as this is an explicit operation
if skipCredentialsValidation {
log.Printf("[INFO] pulumi-aws: skip credentials validation")
return nil
}

var err error
if credentialsValidationRun.CompareAndSwap(false, true) {
log.Printf("[INFO] pulumi-aws: starting to validate credentials. " +
"Disable this by AWS_SKIP_CREDENTIALS_VALIDATION or " +
"skipCredentialsValidation option")
err = validateCredentials(vars, c)
if err == nil {
log.Printf("[INFO] pulumi-aws: credentials are valid")
} else {
log.Printf("[INFO] pulumi-aws: error validating credentials: %v", err)
var err error
if alreadyRun.CompareAndSwap(false, true) {
log.Printf("[INFO] pulumi-aws: starting to validate credentials. " +
"Disable this by AWS_SKIP_CREDENTIALS_VALIDATION or " +
"skipCredentialsValidation option")
err = validateCredentials(vars, c)
if err == nil {
log.Printf("[INFO] pulumi-aws: credentials are valid")
} else {
log.Printf("[INFO] pulumi-aws: error validating credentials: %v", err)
}
}
return err
}
return err
}

// managedByPulumi is a default used for some managed resources, in the absence of something more meaningful.
Expand Down Expand Up @@ -738,6 +794,9 @@ func ProviderFromMeta(metaInfo *tfbridge.MetadataInfo) *tfbridge.ProviderInfo {

p := pftfbridge.MuxShimWithDisjointgPF(ctx, v2p, upstreamProvider.PluginFrameworkProvider)

// We should only run the validation once to avoid duplicating the reported errors.
var credentialsValidationRun atomic.Bool

prov := tfbridge.ProviderInfo{
P: p,
Name: "aws",
Expand Down Expand Up @@ -791,7 +850,7 @@ func ProviderFromMeta(metaInfo *tfbridge.MetadataInfo) *tfbridge.ProviderInfo {
},
},
},
PreConfigureCallback: preConfigureCallback,
PreConfigureCallback: preConfigureCallback(&credentialsValidationRun),
Resources: map[string]*tfbridge.ResourceInfo{
// AWS Certificate Manager
"aws_acm_certificate_validation": {
Expand Down

0 comments on commit be20793

Please sign in to comment.