diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 0ab2919fd857..341196d52fe0 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -1,8 +1,11 @@ package aws import ( + "os" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + goamz "github.com/mitchellh/goamz/aws" ) // Provider returns a terraform.ResourceProvider. @@ -10,25 +13,21 @@ func Provider() terraform.ResourceProvider { // TODO: Move the validation to this, requires conditional schemas // TODO: Move the configuration to this, requires validation + auth := awsAuthSource{} + return &schema.Provider{ Schema: map[string]*schema.Schema{ "access_key": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "AWS_ACCESS_KEY", - "AWS_ACCESS_KEY_ID", - }, nil), + Type: schema.TypeString, + Required: true, + DefaultFunc: auth.accessKeyResolver(), Description: descriptions["access_key"], }, "secret_key": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "AWS_SECRET_KEY", - "AWS_SECRET_ACCESS_KEY", - }, nil), + Type: schema.TypeString, + Required: true, + DefaultFunc: auth.secretKeyResolver(), Description: descriptions["secret_key"], }, @@ -88,6 +87,58 @@ func init() { } } +type awsAuthSource struct { + conventionalAuth goamz.Auth + attemptedConventionalAuth bool +} + +func (a *awsAuthSource) awsSourcedAuth() goamz.Auth { + if a.attemptedConventionalAuth == false { + auth, err := goamz.EnvAuth() + + if err != nil { + auth, _ = goamz.SharedAuth() + } + + a.conventionalAuth = auth + a.attemptedConventionalAuth = true + } + + return a.conventionalAuth +} + +func (a *awsAuthSource) accessKeyResolver() schema.SchemaDefaultFunc { + return func() (interface{}, error) { + // NOTE: Do not remove this until AWS_ACCESS_KEY support has been removed + // https://github.com/hashicorp/terraform/issues/866 + if accessKey := os.Getenv("AWS_ACCESS_KEY"); accessKey != "" { + return accessKey, nil + } + + if auth := a.awsSourcedAuth(); auth.AccessKey != "" { + return auth.AccessKey, nil + } + + return nil, nil + } +} + +func (a *awsAuthSource) secretKeyResolver() schema.SchemaDefaultFunc { + return func() (interface{}, error) { + // NOTE: Do not remove this until AWS_SECRET_KEY support has been removed + // https://github.com/hashicorp/terraform/issues/866 + if secretKey := os.Getenv("AWS_SECRET_KEY"); secretKey != "" { + return secretKey, nil + } + + if auth := a.awsSourcedAuth(); auth.SecretKey != "" { + return auth.SecretKey, nil + } + + return nil, nil + } +} + func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ AccessKey: d.Get("access_key").(string), diff --git a/builtin/providers/aws/provider_test.go b/builtin/providers/aws/provider_test.go index 77cd931075bf..bec938111471 100644 --- a/builtin/providers/aws/provider_test.go +++ b/builtin/providers/aws/provider_test.go @@ -1,6 +1,8 @@ package aws import ( + "fmt" + "io/ioutil" "log" "os" "testing" @@ -29,6 +31,152 @@ func TestProvider_impl(t *testing.T) { var _ terraform.ResourceProvider = Provider() } +func prepareFakeCredentialFile(access_key_id string, secret_key string) (*os.File, error) { + credentialFile, err := ioutil.TempFile("", "aws_credential_test") + + if err != nil { + return nil, err + } + + contents := fmt.Sprintf(`[default] +aws_access_key_id = %s +aws_secret_access_key = %s +`, access_key_id, secret_key) + + credentialFile.Write([]byte(fmt.Sprintf(contents))) + credentialFile.Close() + + return credentialFile, nil + +} + +func cleanAwsEnvConfig() func() { + oldAccessKey := os.Getenv("AWS_ACCESS_KEY") + oldSecretKey := os.Getenv("AWS_SECRET_KEY") + oldAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID") + oldSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY") + + os.Setenv("AWS_ACCESS_KEY", "") + os.Setenv("AWS_SECRET_KEY", "") + os.Setenv("AWS_ACCESS_KEY_ID", "") + os.Setenv("AWS_SECRET_ACCESS_KEY", "") + + return func() { + os.Setenv("AWS_ACCESS_KEY", oldAccessKey) + os.Setenv("AWS_SECRET_KEY", oldSecretKey) + os.Setenv("AWS_ACCESS_KEY_ID", oldAccessKeyId) + os.Setenv("AWS_SECRET_ACCESS_KEY", oldSecretAccessKey) + } +} + +func TestEnvVarsOverrideCredentialsFile(t *testing.T) { + resetEnvVars := cleanAwsEnvConfig() + defer resetEnvVars() + + os.Setenv("AWS_ACCESS_KEY_ID", "access_key_id_from_aws_cli_env_var") + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret_access_key_from_aws_cli_env_var") + + credentialFile, err := prepareFakeCredentialFile("access_key_id_from_config", "secret_access_key_from_config") + + if err != nil { + t.Fatalf("Could not create temporary AWS config file: '%s'", err) + } + + os.Setenv("AWS_CREDENTIAL_FILE", credentialFile.Name()) + defer os.Remove(credentialFile.Name()) + + auth := awsAuthSource{} + + access_key, _ := auth.accessKeyResolver()() + + if access_key != "access_key_id_from_aws_cli_env_var" { + t.Errorf("expected: %s, got: %#v", "access_key_id_from_aws_cli_env_var", access_key) + } + + secret_key, _ := auth.secretKeyResolver()() + + if secret_key != "secret_access_key_from_aws_cli_env_var" { + t.Errorf("expected: %#v, got: %#v", "secret_access_key_from_aws_cli_env_var", secret_key) + } +} + +// See: +// - https://github.com/hashicorp/terraform/pull/851 +// - https://github.com/hashicorp/terraform/issues/866 +// +// We should not change default behaviour in a minor release, if end-user has both variations +// of the env key set in their environment then we should give preference to the legacy one. +func TestDeprecatedEnvVarsOverrideOfficialOnes(t *testing.T) { + resetEnvVars := cleanAwsEnvConfig() + defer resetEnvVars() + + os.Setenv("AWS_ACCESS_KEY", "access_key_id_from_legacy_env_var") + os.Setenv("AWS_SECRET_KEY", "secret_access_key_from_legacy_env_var") + os.Setenv("AWS_ACCESS_KEY_ID", "access_key_id_from_aws_cli_env_var") + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret_access_key_from_aws_cli_env_var") + + auth := awsAuthSource{} + + access_key, _ := auth.accessKeyResolver()() + + if access_key != "access_key_id_from_legacy_env_var" { + t.Errorf("expected: %s, got: %#v", "access_key_id_from_legacy_env_var", access_key) + } + + secret_key, _ := auth.secretKeyResolver()() + + if secret_key != "secret_access_key_from_legacy_env_var" { + t.Errorf("expected: %s, got: %#v", "secret_access_key_from_legacy_env_var", secret_key) + } +} + +func TestLoadCredentialsFromFileWhenNoConfigInEnv(t *testing.T) { + resetEnvVars := cleanAwsEnvConfig() + defer resetEnvVars() + + credentialFile, err := prepareFakeCredentialFile("access_key_id_from_config", "secret_access_key_from_config") + + if err != nil { + t.Fatalf("Could not create temporary AWS config file: '%s'", err) + } + + os.Setenv("AWS_CREDENTIAL_FILE", credentialFile.Name()) + defer os.Remove(credentialFile.Name()) + + auth := awsAuthSource{} + + access_key, _ := auth.accessKeyResolver()() + + if access_key != "access_key_id_from_config" { + t.Errorf("expected: %s, got: %#v", "access_key_id_from_config", access_key) + } + + secret_key, _ := auth.secretKeyResolver()() + + if secret_key != "secret_access_key_from_config" { + t.Errorf("expected: %s, bad: %#v", "secret_access_key_from_config", secret_key) + } +} + +func TestAuthSourcerReturnsNilWhenDefaultsCannotBeFound(t *testing.T) { + resetEnvVars := cleanAwsEnvConfig() + defer resetEnvVars() + + auth := awsAuthSource{} + + access_key, _ := auth.accessKeyResolver()() + + if access_key != nil { + t.Errorf("expected: nil, got: %#v", access_key) + } + + secret_key, _ := auth.secretKeyResolver()() + + if secret_key != nil { + t.Errorf("expected: nil, got: %#v", secret_key) + } +} + func testAccPreCheck(t *testing.T) { if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" { t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests") diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown index 499914149804..ada47dabb525 100644 --- a/website/source/docs/providers/aws/index.html.markdown +++ b/website/source/docs/providers/aws/index.html.markdown @@ -35,10 +35,16 @@ resource "aws_instance" "web" { The following arguments are supported: * `access_key` - (Required) This is the AWS access key. It must be provided, but - it can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable. + it can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable, or the + [AWS cli credentials file](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-config-files). * `secret_key` - (Required) This is the AWS secret key. It must be provided, but - it can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable. + it can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable, or the + [AWS cli credentials file](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-config-files). * `region` - (Required) This is the AWS region. It must be provided, but it can also be sourced from the `AWS_DEFAULT_REGION` environment variables. + +It's possible to load credentials from a [specific profile](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-multiple-profiles) +within the AWS cli's credentials file by setting the `AWS_PROFILE` environment variable. +The `default` profile will be used if this variable is not set.