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

@ -50,6 +50,8 @@ import (
type Config struct {
AccessKey string
SecretKey string
CredsFilename string
Profile string
Token string
Region string
MaxRetries int
@ -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

View File

@ -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)
}
}
}
@ -267,12 +342,14 @@ func getEnv() *currentEnv {
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 {

View File

@ -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),

View File

@ -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.