diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 51091a20e3..1c9ab296db 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -48,11 +48,13 @@ import ( ) type Config struct { - AccessKey string - SecretKey string - Token string - Region string - MaxRetries int + AccessKey string + SecretKey string + CredsFilename string + Profile string + Token string + Region string + MaxRetries int AllowedAccountIds []interface{} ForbiddenAccountIds []interface{} @@ -113,7 +115,7 @@ func (c *Config) Client() (interface{}, error) { client.region = c.Region log.Println("[INFO] Building AWS auth structure") - creds := getCreds(c.AccessKey, c.SecretKey, c.Token) + creds := getCreds(c.AccessKey, c.SecretKey, c.Token, c.Profile, c.CredsFilename) // Call Get to check for credential provider. If nothing found, we'll get an // error, and we can present it nicely to the user _, err = creds.Get() @@ -341,7 +343,7 @@ func (c *Config) ValidateAccountId(iamconn *iam.IAM) error { // This function is responsible for reading credentials from the // environment in the case that they're not explicitly specified // in the Terraform configuration. -func getCreds(key, secret, token string) *awsCredentials.Credentials { +func getCreds(key, secret, token, profile, credsfile string) *awsCredentials.Credentials { // build a chain provider, lazy-evaulated by aws-sdk providers := []awsCredentials.Provider{ &awsCredentials.StaticProvider{Value: awsCredentials.Value{ @@ -350,7 +352,10 @@ func getCreds(key, secret, token string) *awsCredentials.Credentials { SessionToken: token, }}, &awsCredentials.EnvProvider{}, - &awsCredentials.SharedCredentialsProvider{}, + &awsCredentials.SharedCredentialsProvider{ + Filename: credsfile, + Profile: profile, + }, } // We only look in the EC2 metadata API if we can connect diff --git a/builtin/providers/aws/config_test.go b/builtin/providers/aws/config_test.go index 316bf18939..5c58a57290 100644 --- a/builtin/providers/aws/config_test.go +++ b/builtin/providers/aws/config_test.go @@ -3,6 +3,7 @@ package aws import ( "encoding/json" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -16,7 +17,7 @@ func TestAWSConfig_shouldError(t *testing.T) { defer resetEnv() cfg := Config{} - c := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token) + c := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) _, err := c.Get() if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() != "NoCredentialProviders" { @@ -49,7 +50,7 @@ func TestAWSConfig_shouldBeStatic(t *testing.T) { Token: c.Token, } - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token) + creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) if creds == nil { t.Fatalf("Expected a static creds provider to be returned") } @@ -84,7 +85,7 @@ func TestAWSConfig_shouldIAM(t *testing.T) { // An empty config, no key supplied cfg := Config{} - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token) + creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) if creds == nil { t.Fatalf("Expected a static creds provider to be returned") } @@ -133,7 +134,7 @@ func TestAWSConfig_shouldIgnoreIAM(t *testing.T) { Token: c.Token, } - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token) + creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) if creds == nil { t.Fatalf("Expected a static creds provider to be returned") } @@ -153,15 +154,65 @@ func TestAWSConfig_shouldIgnoreIAM(t *testing.T) { } } +var credentialsFileContents = `[myprofile] +aws_access_key_id = accesskey +aws_secret_access_key = secretkey +` + +func TestAWSConfig_shouldBeShared(t *testing.T) { + file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_cred") + if err != nil { + t.Fatalf("Error writing temporary credentials file: %s", err) + } + _, err = file.WriteString(credentialsFileContents) + if err != nil { + t.Fatalf("Error writing temporary credentials to file: %s", err) + } + err = file.Close() + if err != nil { + t.Fatalf("Error closing temporary credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + resetEnv := unsetEnv(t) + defer resetEnv() + + if err := os.Setenv("AWS_PROFILE", "myprofile"); err != nil { + t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", file.Name()); err != nil { + t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } + + creds := getCreds("", "", "", "myprofile", file.Name()) + if creds == nil { + t.Fatalf("Expected a provider chain to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + + if v.AccessKeyID != "accesskey" { + t.Fatalf("AccessKeyID mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) + } + + if v.SecretAccessKey != "secretkey" { + t.Fatalf("SecretAccessKey mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) + } +} + func TestAWSConfig_shouldBeENV(t *testing.T) { // need to set the environment variables to a dummy string, as we don't know // what they may be at runtime without hardcoding here s := "some_env" resetEnv := setEnv(s, t) + defer resetEnv() cfg := Config{} - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token) + creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) if creds == nil { t.Fatalf("Expected a static creds provider to be returned") } @@ -195,6 +246,12 @@ func unsetEnv(t *testing.T) func() { if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil { t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err) } + if err := os.Unsetenv("AWS_PROFILE"); err != nil { + t.Fatalf("Error unsetting env var AWS_TOKEN: %s", err) + } + if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil { + t.Fatalf("Error unsetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } return func() { // re-set all the envs we unset above @@ -207,6 +264,12 @@ func unsetEnv(t *testing.T) func() { if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) } + if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { + t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", e.CredsFilename); err != nil { + t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } } } @@ -222,6 +285,12 @@ func setEnv(s string, t *testing.T) func() { if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil { t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err) } + if err := os.Setenv("AWS_PROFILE", s); err != nil { + t.Fatalf("Error setting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { + t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) + } return func() { // re-set all the envs we unset above @@ -234,6 +303,12 @@ func setEnv(s string, t *testing.T) func() { if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) } + if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { + t.Fatalf("Error setting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { + t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) + } } } @@ -264,15 +339,17 @@ func getEnv() *currentEnv { // Grab any existing AWS keys and preserve. In some tests we'll unset these, so // we need to have them and restore them after return ¤tEnv{ - Key: os.Getenv("AWS_ACCESS_KEY_ID"), - Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"), - Token: os.Getenv("AWS_SESSION_TOKEN"), + Key: os.Getenv("AWS_ACCESS_KEY_ID"), + Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"), + Token: os.Getenv("AWS_SESSION_TOKEN"), + Profile: os.Getenv("AWS_TOKEN"), + CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"), } } // struct to preserve the current environment type currentEnv struct { - Key, Secret, Token string + Key, Secret, Token, Profile, CredsFilename string } type routes struct { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 7345d7d752..9829972c8b 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -29,6 +29,20 @@ func Provider() terraform.ResourceProvider { Description: descriptions["secret_key"], }, + "profile": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + Description: descriptions["profile"], + }, + + "shared_credentials_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + Description: descriptions["shared_credentials_file"], + }, + "token": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -222,6 +236,12 @@ func init() { "secret_key": "The secret key for API operations. You can retrieve this\n" + "from the 'Security & Credentials' section of the AWS console.", + "profile": "The profile for API operations. If not set, the default profile\n" + + "created with `aws configure` will be used.", + + "shared_credentials_file": "The path to the shared credentials file. If not set\n" + + "this defaults to ~/.aws/credentials.", + "token": "session token. A session token is only required if you are\n" + "using temporary security credentials.", @@ -241,6 +261,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ AccessKey: d.Get("access_key").(string), SecretKey: d.Get("secret_key").(string), + Profile: d.Get("profile").(string), + CredsFilename: d.Get("shared_credentials_file").(string), Token: d.Get("token").(string), Region: d.Get("region").(string), MaxRetries: d.Get("max_retries").(int), diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown index 7199111c2b..e110e9f7c4 100644 --- a/website/source/docs/providers/aws/index.html.markdown +++ b/website/source/docs/providers/aws/index.html.markdown @@ -34,14 +34,26 @@ resource "aws_instance" "web" { The following arguments are supported in the `provider` block: -* `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. +* `access_key` - (Optional) This is the AWS access key. It must be provided, but + it can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable, or via + a shared credentials file if `profile` is specified. -* `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. +* `secret_key` - (Optional) This is the AWS secret key. It must be provided, but + it can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable, or + via a shared credentials file if `profile` is specified. * `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 can also be sourced from the `AWS_DEFAULT_REGION` environment variables, or + via a shared credentials file if `profile` is specified. + +* `profile` - (Optional) This is the AWS profile name as set in the shared credentials + file. + +* `shared_credentials_file` = (Optional) This is the path to the shared credentials file. + If this is not set and a profile is specified, ~/.aws/credentials will be used. + +* `token` - (Optional) Use this to set an MFA token. It can also be sourced + from the `AWS_SECURITY_TOKEN` environment variable. * `max_retries` - (Optional) This is the maximum number of times an API call is being retried in case requests are being throttled or experience transient failures. @@ -55,8 +67,10 @@ The following arguments are supported in the `provider` block: to prevent you mistakenly using a wrong one (and end up destroying live environment). Conflicts with `allowed_account_ids`. -* `dynamodb_endpoint` - (Optional) Use this to override the default endpoint URL constructed from the `region`. It's typically used to connect to dynamodb-local. +* `dynamodb_endpoint` - (Optional) Use this to override the default endpoint + URL constructed from the `region`. It's typically used to connect to + dynamodb-local. -* `kinesis_endpoint` - (Optional) Use this to override the default endpoint URL constructed from the `region`. It's typically used to connect to kinesalite. +* `kinesis_endpoint` - (Optional) Use this to override the default endpoint URL + constructed from the `region`. It's typically used to connect to kinesalite. -* `token` - (Optional) Use this to set an MFA token. It can also be sourced from the `AWS_SECURITY_TOKEN` environment variable.