From ace215481aa46c2a419a42c05b7529f787c53c19 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Mon, 11 Jan 2016 15:22:09 +0000 Subject: [PATCH] provider/aws: Add profile to provider config This allows specification of the profile for the shared credentials provider for AWS to be specified in Terraform configuration. This is useful if defining providers with aliases, or if you don't want to set environment variables. Example: $ aws configure --profile this_is_dog ... enter keys $ cat main.tf provider "aws" { profile = "this_is_dog" # Optionally also specify the path to the credentials file shared_credentials_file = "/tmp/credentials" } This is equivalent to specifying AWS_PROFILE or AWS_SHARED_CREDENTIALS_FILE in the environment. --- builtin/providers/aws/config.go | 21 ++-- builtin/providers/aws/config_test.go | 95 +++++++++++++++++-- builtin/providers/aws/provider.go | 22 +++++ .../docs/providers/aws/index.html.markdown | 30 ++++-- 4 files changed, 143 insertions(+), 25 deletions(-) diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 51091a20e..1c9ab296d 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 316bf1893..5c58a5729 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 7345d7d75..9829972c8 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 7199111c2..e110e9f7c 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.