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.
This commit is contained in:
James Nugent 2016-01-11 15:22:09 +00:00
parent e94ee6b77c
commit ace215481a
4 changed files with 143 additions and 25 deletions

View File

@ -48,11 +48,13 @@ import (
) )
type Config struct { type Config struct {
AccessKey string AccessKey string
SecretKey string SecretKey string
Token string CredsFilename string
Region string Profile string
MaxRetries int Token string
Region string
MaxRetries int
AllowedAccountIds []interface{} AllowedAccountIds []interface{}
ForbiddenAccountIds []interface{} ForbiddenAccountIds []interface{}
@ -113,7 +115,7 @@ func (c *Config) Client() (interface{}, error) {
client.region = c.Region client.region = c.Region
log.Println("[INFO] Building AWS auth structure") 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 // Call Get to check for credential provider. If nothing found, we'll get an
// error, and we can present it nicely to the user // error, and we can present it nicely to the user
_, err = creds.Get() _, err = creds.Get()
@ -341,7 +343,7 @@ func (c *Config) ValidateAccountId(iamconn *iam.IAM) error {
// This function is responsible for reading credentials from the // This function is responsible for reading credentials from the
// environment in the case that they're not explicitly specified // environment in the case that they're not explicitly specified
// in the Terraform configuration. // 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 // build a chain provider, lazy-evaulated by aws-sdk
providers := []awsCredentials.Provider{ providers := []awsCredentials.Provider{
&awsCredentials.StaticProvider{Value: awsCredentials.Value{ &awsCredentials.StaticProvider{Value: awsCredentials.Value{
@ -350,7 +352,10 @@ func getCreds(key, secret, token string) *awsCredentials.Credentials {
SessionToken: token, SessionToken: token,
}}, }},
&awsCredentials.EnvProvider{}, &awsCredentials.EnvProvider{},
&awsCredentials.SharedCredentialsProvider{}, &awsCredentials.SharedCredentialsProvider{
Filename: credsfile,
Profile: profile,
},
} }
// We only look in the EC2 metadata API if we can connect // We only look in the EC2 metadata API if we can connect

View File

@ -3,6 +3,7 @@ package aws
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -16,7 +17,7 @@ func TestAWSConfig_shouldError(t *testing.T) {
defer resetEnv() defer resetEnv()
cfg := Config{} 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() _, err := c.Get()
if awsErr, ok := err.(awserr.Error); ok { if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() != "NoCredentialProviders" { if awsErr.Code() != "NoCredentialProviders" {
@ -49,7 +50,7 @@ func TestAWSConfig_shouldBeStatic(t *testing.T) {
Token: c.Token, 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 { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") 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 // An empty config, no key supplied
cfg := Config{} 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 { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") t.Fatalf("Expected a static creds provider to be returned")
} }
@ -133,7 +134,7 @@ func TestAWSConfig_shouldIgnoreIAM(t *testing.T) {
Token: c.Token, 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 { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") 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) { func TestAWSConfig_shouldBeENV(t *testing.T) {
// need to set the environment variables to a dummy string, as we don't know // need to set the environment variables to a dummy string, as we don't know
// what they may be at runtime without hardcoding here // what they may be at runtime without hardcoding here
s := "some_env" s := "some_env"
resetEnv := setEnv(s, t) resetEnv := setEnv(s, t)
defer resetEnv() defer resetEnv()
cfg := Config{} 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 { if creds == nil {
t.Fatalf("Expected a static creds provider to be returned") 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 { if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil {
t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err) 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() { return func() {
// re-set all the envs we unset above // 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 { if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) 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 { if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil {
t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err) 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() { return func() {
// re-set all the envs we unset above // 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 { if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil {
t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) 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 // Grab any existing AWS keys and preserve. In some tests we'll unset these, so
// we need to have them and restore them after // we need to have them and restore them after
return &currentEnv{ return &currentEnv{
Key: os.Getenv("AWS_ACCESS_KEY_ID"), Key: os.Getenv("AWS_ACCESS_KEY_ID"),
Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"), Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"),
Token: os.Getenv("AWS_SESSION_TOKEN"), Token: os.Getenv("AWS_SESSION_TOKEN"),
Profile: os.Getenv("AWS_TOKEN"),
CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"),
} }
} }
// struct to preserve the current environment // struct to preserve the current environment
type currentEnv struct { type currentEnv struct {
Key, Secret, Token string Key, Secret, Token, Profile, CredsFilename string
} }
type routes struct { type routes struct {

View File

@ -29,6 +29,20 @@ func Provider() terraform.ResourceProvider {
Description: descriptions["secret_key"], 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{ "token": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -222,6 +236,12 @@ func init() {
"secret_key": "The secret key for API operations. You can retrieve this\n" + "secret_key": "The secret key for API operations. You can retrieve this\n" +
"from the 'Security & Credentials' section of the AWS console.", "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" + "token": "session token. A session token is only required if you are\n" +
"using temporary security credentials.", "using temporary security credentials.",
@ -241,6 +261,8 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{ config := Config{
AccessKey: d.Get("access_key").(string), AccessKey: d.Get("access_key").(string),
SecretKey: d.Get("secret_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), Token: d.Get("token").(string),
Region: d.Get("region").(string), Region: d.Get("region").(string),
MaxRetries: d.Get("max_retries").(int), MaxRetries: d.Get("max_retries").(int),

View File

@ -34,14 +34,26 @@ resource "aws_instance" "web" {
The following arguments are supported in the `provider` block: The following arguments are supported in the `provider` block:
* `access_key` - (Required) This is the AWS access key. It must be provided, but * `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. 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 * `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. 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 * `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 * `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. 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). to prevent you mistakenly using a wrong one (and end up destroying live environment).
Conflicts with `allowed_account_ids`. 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.