diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index dccde08d4..a10fc52f3 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -15,36 +15,45 @@ import ( // AccessConfig is for common configuration related to AWS access type AccessConfig struct { - AccessKey string `mapstructure:"access_key"` - SecretKey string `mapstructure:"secret_key"` - RawRegion string `mapstructure:"region"` - Token string `mapstructure:"token"` + AccessKey string `mapstructure:"access_key"` + SecretKey string `mapstructure:"secret_key"` + RawRegion string `mapstructure:"region"` + Token string `mapstructure:"token"` + ProfileName string `mapstructure:"profile"` } // Config returns a valid aws.Config object for access to AWS services, or // an error if the authentication and region couldn't be resolved func (c *AccessConfig) Config() (*aws.Config, error) { - creds := credentials.NewChainCredentials([]credentials.Provider{ - &credentials.StaticProvider{Value: credentials.Value{ - AccessKeyID: c.AccessKey, - SecretAccessKey: c.SecretKey, - SessionToken: c.Token, - }}, - &credentials.EnvProvider{}, - &credentials.SharedCredentialsProvider{Filename: "", Profile: ""}, - &ec2rolecreds.EC2RoleProvider{}, - }) + var creds *credentials.Credentials region, err := c.Region() if err != nil { return nil, err } - - return &aws.Config{ - Region: aws.String(region), - Credentials: creds, - MaxRetries: aws.Int(11), - }, nil + config := aws.NewConfig().WithRegion(region).WithMaxRetries(11) + if c.ProfileName != "" { + profile, err := NewFromProfile(c.ProfileName) + if err != nil { + return nil, err + } + creds, err = profile.CredentialsFromProfile(config) + if err != nil { + return nil, err + } + } else { + creds = credentials.NewChainCredentials([]credentials.Provider{ + &credentials.StaticProvider{Value: credentials.Value{ + AccessKeyID: c.AccessKey, + SecretAccessKey: c.SecretKey, + SessionToken: c.Token, + }}, + &credentials.EnvProvider{}, + &credentials.SharedCredentialsProvider{Filename: "", Profile: ""}, + &ec2rolecreds.EC2RoleProvider{}, + }) + } + return config.WithCredentials(creds), nil } // Region returns the aws.Region object for access to AWS services, requesting diff --git a/builder/amazon/common/cli_config.go b/builder/amazon/common/cli_config.go new file mode 100644 index 000000000..88b7a4afd --- /dev/null +++ b/builder/amazon/common/cli_config.go @@ -0,0 +1,155 @@ +package common + +import ( + "fmt" + "os" + "path" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/go-ini/ini" + "github.com/mitchellh/go-homedir" +) + +type CLIConfig struct { + ProfileName string + SourceProfile string + + AssumeRoleInput *sts.AssumeRoleInput + SourceCredentials *credentials.Credentials + + profileCfg *ini.Section + profileCred *ini.Section +} + +// Return a new CLIConfig with stored profile settings +func NewFromProfile(name string) (*CLIConfig, error) { + c := &CLIConfig{} + c.AssumeRoleInput = new(sts.AssumeRoleInput) + err := c.Prepare(name) + if err != nil { + return nil, err + } + sessName, err := c.getSessionName(c.profileCfg.Key("role_session_name").Value()) + if err != nil { + return nil, err + } + c.AssumeRoleInput.RoleSessionName = aws.String(sessName) + arn := c.profileCfg.Key("role_arn").Value() + if arn != "" { + c.AssumeRoleInput.RoleArn = aws.String(arn) + } + id := c.profileCfg.Key("external_id").Value() + if id != "" { + c.AssumeRoleInput.ExternalId = aws.String(id) + } + c.SourceCredentials = credentials.NewStaticCredentials( + c.profileCred.Key("aws_access_key_id").Value(), + c.profileCred.Key("aws_secret_access_key").Value(), + c.profileCred.Key("aws_session_token").Value(), + ) + return c, nil +} + +// Return AWS Credentials using current profile. Must supply source config. +func (c *CLIConfig) CredentialsFromProfile(conf *aws.Config) (*credentials.Credentials, error) { + // If the profile name is equal to the source profile, there is no role to assume so return + // the source credentials as they were captured. + if c.ProfileName == c.SourceProfile { + return c.SourceCredentials, nil + } + srcCfg := aws.NewConfig().Copy(conf).WithCredentials(c.SourceCredentials) + svc := sts.New(session.New(), srcCfg) + res, err := svc.AssumeRole(c.AssumeRoleInput) + if err != nil { + return nil, err + } + return credentials.NewStaticCredentials( + *res.Credentials.AccessKeyId, + *res.Credentials.SecretAccessKey, + *res.Credentials.SessionToken, + ), nil +} + +// Sets params in the struct based on the file section +func (c *CLIConfig) Prepare(name string) error { + var err error + c.ProfileName = name + c.profileCfg, err = configFromName(c.ProfileName) + if err != nil { + return err + } + c.SourceProfile = c.profileCfg.Key("source_profile").Value() + if c.SourceProfile == "" { + c.SourceProfile = c.ProfileName + } + c.profileCred, err = credsFromName(c.SourceProfile) + if err != nil { + return err + } + return nil +} + +func (c *CLIConfig) getSessionName(rawName string) (string, error) { + if rawName == "" { + name := "packer-" + host, err := os.Hostname() + if err != nil { + return name, err + } + return fmt.Sprintf("%s%s", name, host), nil + } else { + return rawName, nil + } +} + +func configFromName(name string) (*ini.Section, error) { + filePath := os.Getenv("AWS_CONFIG_FILE") + if filePath == "" { + home, err := homedir.Dir() + if err != nil { + return nil, err + } + filePath = path.Join(home, ".aws", "config") + } + file, err := readFile(filePath) + if err != nil { + return nil, err + } + profileName := fmt.Sprintf("profile %s", name) + cfg, err := file.GetSection(profileName) + if err != nil { + return nil, err + } + return cfg, nil +} + +func credsFromName(name string) (*ini.Section, error) { + filePath := os.Getenv("AWS_SHARED_CREDENTIALS_FILE") + if filePath == "" { + home, err := homedir.Dir() + if err != nil { + return nil, err + } + filePath = path.Join(home, ".aws", "credentials") + } + file, err := readFile(filePath) + if err != nil { + return nil, err + } + cfg, err := file.GetSection(name) + if err != nil { + return nil, err + } + return cfg, nil +} + +func readFile(path string) (*ini.File, error) { + cfg, err := ini.Load(path) + if err != nil { + return nil, err + } + return cfg, nil +} diff --git a/builder/amazon/common/cli_config_test.go b/builder/amazon/common/cli_config_test.go new file mode 100644 index 000000000..6a6dc7ed5 --- /dev/null +++ b/builder/amazon/common/cli_config_test.go @@ -0,0 +1,118 @@ +package common + +import ( + "io/ioutil" + "os" + "path" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" +) + +func init() { + os.Setenv("AWS_ACCESS_KEY_ID", "") + os.Setenv("AWS_ACCESS_KEY", "") + os.Setenv("AWS_SECRET_ACCESS_KEY", "") + os.Setenv("AWS_SECRET_KEY", "") + os.Setenv("AWS_CONFIG_FILE", "") + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "") +} + +func testCLIConfig() *CLIConfig { + return &CLIConfig{} +} + +func TestCLIConfigNewFromProfile(t *testing.T) { + tmpDir := mockConfig(t) + + c, err := NewFromProfile("testing2") + if err != nil { + t.Error(err) + } + if c.AssumeRoleInput.RoleArn != nil { + t.Errorf("RoleArn should be nil. Instead %p", c.AssumeRoleInput.RoleArn) + } + if c.AssumeRoleInput.ExternalId != nil { + t.Errorf("ExternalId should be nil. Instead %p", c.AssumeRoleInput.ExternalId) + } + + mockConfigClose(t, tmpDir) +} + +func TestAssumeRole(t *testing.T) { + tmpDir := mockConfig(t) + + c, err := NewFromProfile("testing1") + if err != nil { + t.Error(err) + } + // Role + e := "arn:aws:iam::123456789011:role/rolename" + a := *c.AssumeRoleInput.RoleArn + if e != a { + t.Errorf("RoleArn value should be %s. Instead %s", e, a) + } + // Session + a = *c.AssumeRoleInput.RoleSessionName + e = "testsession" + if e != a { + t.Errorf("RoleSessionName value should be %s. Instead %s", e, a) + } + + config := aws.NewConfig() + _, err = c.CredentialsFromProfile(config) + if err == nil { + t.Error("Should have errored") + } + mockConfigClose(t, tmpDir) +} + +func mockConfig(t *testing.T) string { + time := time.Now().UnixNano() + dir, err := ioutil.TempDir("", strconv.FormatInt(time, 10)) + if err != nil { + t.Error(err) + } + + cfg := []byte(`[profile testing1] +region=us-west-2 +source_profile=testingcredentials +role_arn = arn:aws:iam::123456789011:role/rolename +role_session_name = testsession + +[profile testing2] +region=us-west-2 + `) + cfgFile := path.Join(dir, "config") + err = ioutil.WriteFile(cfgFile, cfg, 0644) + if err != nil { + t.Error(err) + } + os.Setenv("AWS_CONFIG_FILE", cfgFile) + + crd := []byte(`[testingcredentials] +aws_access_key_id = foo +aws_secret_access_key = bar + +[testing2] +aws_access_key_id = baz +aws_secret_access_key = qux + `) + crdFile := path.Join(dir, "credentials") + err = ioutil.WriteFile(crdFile, crd, 0644) + if err != nil { + t.Error(err) + } + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", crdFile) + + return dir +} + +func mockConfigClose(t *testing.T, dir string) { + err := os.RemoveAll(dir) + if err != nil { + t.Error(err) + } +}