From 6dc9de175dedf4e1a9b7ddebf08f3969d416fd2a Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Mon, 14 Aug 2017 23:41:17 -0700 Subject: [PATCH] Remove assume role functionality. Rely on Terraform. Fixes #112 --- CHANGELOG.md | 10 ++++-- README.md | 38 ++++++++++++++++++++--- aws/aws.go | 58 ----------------------------------- cmd/server.go | 11 ------- server/apply_executor.go | 33 ++------------------ server/plan_executor.go | 35 +++------------------ server/server.go | 21 +++---------- terraform/terraform_client.go | 18 ++++------- 8 files changed, 59 insertions(+), 165 deletions(-) delete mode 100644 aws/aws.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 584a33c41..ab5bcf5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,13 @@ # v0.1.1 (Unreleased) +### Backwards Incompatibilities / Notes: +* `--aws-assume-role-arn` and `--aws-region` flags removed. Instead, to name the +assume role session with the GitHub username of the user running the Atlantis command +use the `atlantis_user` terraform variable alongside Terraform's +[built-in support](https://www.terraform.io/docs/providers/aws/#assume-role) for assume role +(see https://github.com/hootsuite/atlantis/blob/master/README.md#assume-role-session-names) + ### Improvements -* Support for HTTPS cloning using Github username and token provided to atlantis server ([#117](https://github.com/hootsuite/atlantis/pull/117)) -* Set custom user agent for AWS interactions by atlantis server ([#115](https://github.com/hootsuite/atlantis/pull/115)) +* Support for HTTPS cloning using GitHub username and token provided to atlantis server ([#117](https://github.com/hootsuite/atlantis/pull/117)) * Adding `post_plan` and `post_apply` commands ([#102](https://github.com/hootsuite/atlantis/pull/102)) * Adding the ability to verify webhook secret ([#120](https://github.com/hootsuite/atlantis/pull/120)) diff --git a/README.md b/README.md index b86ddf737..cd9f7743e 100644 --- a/README.md +++ b/README.md @@ -294,14 +294,42 @@ Atlantis simply shells out to `terraform` so you don't need to do anything speci As long as `terraform` works where you're hosting Atlantis, then Atlantis will work. See https://www.terraform.io/docs/providers/aws/#authentication for more detail. +### Multiple AWS Accounts +Atlantis supports multiple AWS accounts through the use of Terraform's +[AWS Authentication](https://www.terraform.io/docs/providers/aws/#authentication). + +If you're using the [Shared Credentials file](https://www.terraform.io/docs/providers/aws/#shared-credentials-file) +you'll need to ensure the server that Atlantis is executing on has the corresponding credentials file. + +If you're using [Assume role](https://www.terraform.io/docs/providers/aws/#assume-role) +you'll need to ensure that the credentials file has a `default` profile that is able +to assume all required roles. + +[Environment variables](https://www.terraform.io/docs/providers/aws/#environment-variables) authentication +won't work for multiple accounts since Atlantis wouldn't know which environment variables to execute +Terraform with. + ### Assume Role Session Names -Atlantis provides the ability to use AWS's Assume Role and **dynamically name the session** with the GitHub username of whoever commented `atlantis apply`. +Atlantis injects the Terraform variable `atlantis_user` and sets it to the GitHub username of +the user that is running the Atlantis command. This can be used to dynamically name the assume role +session. This is used at Hootsuite so AWS API actions can be correlated with a specific user. -This is used at Hootsuite so AWS API actions can be correlated with a specific user. -To take advantage of this feature, simply set the `--aws-assume-role-arn` flag to the -role to be assumed: `arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME`. +To take advantage of this feature, use Terraform's [built-in support](https://www.terraform.io/docs/providers/aws/#assume-role) for assume role +and use the `atlantis_user` terraform variable -If you're using Terraform's [built-in support](https://www.terraform.io/docs/providers/aws/#assume-role) for assume role then there is no need to set this flag (unless you also want your sessions to take the name of the GitHub user). +```hcl +provider "aws" { + assume_role { + role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" + session_name = "${var.atlantis_user}" + } +} + +# need to define the atlantis_user variable to avoid terraform errors +variable "atlantis_user" { + default = "atlantis_user" +} +``` ## Glossary #### Project diff --git a/aws/aws.go b/aws/aws.go deleted file mode 100644 index bfb3aba38..000000000 --- a/aws/aws.go +++ /dev/null @@ -1,58 +0,0 @@ -// Package aws handles Amazon Web Services actions. -package aws - -import ( - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/sts" -) - -const sessionDuration = 30 * time.Minute - -// Config configures our aws clients. -type Config struct { - // Region to connect to for api calls. - Region string - // RoleARN is the arn of the role to be assumed. - // If empty, we won't assume a role and will use the normal - // AWS authentication methods - RoleARN string -} - -// CreateSession creates a new valid AWS session to be used by AWS clients. -// If RoleARN is not empty, we will assume a role and name that role whatever -// is passed in as sessionName. Otherwise we'll create a -// session using the AWS SDK's normal mechanism. -func (c *Config) CreateSession(sessionName string) (*session.Session, error) { - awsSession, err := session.NewSessionWithOptions(session.Options{ - Config: aws.Config{Region: aws.String(c.Region)}, - SharedConfigState: session.SharedConfigEnable, - }) - if err != nil { - return nil, err - } - if _, err = awsSession.Config.Credentials.Get(); err != nil { - return nil, err - } - - // generate a new session if aws role is provided - if c.RoleARN != "" { - return c.assumeRole(awsSession, sessionName), nil - } - - return awsSession, nil -} - -// assumeRole calls Amazon's Security Token Service and attempts to assume roleArn and provide credentials for that role. -func (c *Config) assumeRole(s *session.Session, sessionName string) *session.Session { - stsClient := sts.New(s, s.Config) - creds := stscreds.NewCredentialsWithClient(stsClient, c.RoleARN, func(p *stscreds.AssumeRoleProvider) { - p.RoleSessionName = sessionName - // override default 15 minute time - p.Duration = sessionDuration - }) - return session.New(&aws.Config{Credentials: creds, Region: aws.String(c.Region)}) -} diff --git a/cmd/server.go b/cmd/server.go index 3307633c7..eeed9c73f 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -18,8 +18,6 @@ import ( // 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices const ( atlantisURLFlag = "atlantis-url" - awsAssumeRoleFlag = "aws-assume-role-arn" - awsRegionFlag = "aws-region" configFlag = "config" dataDirFlag = "data-dir" ghHostnameFlag = "gh-hostname" @@ -36,15 +34,6 @@ var stringFlags = []stringFlag{ name: atlantisURLFlag, description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + portFlag + ".", }, - { - name: awsAssumeRoleFlag, - description: "ARN of the role to assume when running Terraform against AWS. If not using assume role, no need to set.", - }, - { - name: awsRegionFlag, - description: "Amazon region to use for assume role. If not setting --" + awsAssumeRoleFlag + " then ignore.", - value: "us-east-1", - }, { name: configFlag, description: "Path to config file.", diff --git a/server/apply_executor.go b/server/apply_executor.go index 67c02f1a0..8d532a7d7 100644 --- a/server/apply_executor.go +++ b/server/apply_executor.go @@ -9,7 +9,6 @@ import ( "path/filepath" version "github.com/hashicorp/go-version" - "github.com/hootsuite/atlantis/aws" "github.com/hootsuite/atlantis/github" "github.com/hootsuite/atlantis/locking" "github.com/hootsuite/atlantis/models" @@ -20,7 +19,6 @@ import ( type ApplyExecutor struct { github *github.Client githubStatus *GithubStatus - awsConfig *aws.Config terraform *terraform.Client githubCommentRenderer *GithubCommentRenderer lockingClient *locking.Client @@ -125,31 +123,6 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P applyExtraArgs = config.GetExtraArguments(ctx.Command.Name.String()) } - // todo: de-duplicate this section between plan and apply - var credsEnvVars []string - // If awsConfig is nil we know that we're not using assume role and so - // don't need to do an AWS calls ourselves - if a.awsConfig != nil { - awsSession, err := a.awsConfig.CreateSession(ctx.User.Username) - if err != nil { - ctx.Log.Err(err.Error()) - return ProjectResult{Error: err} - } - creds, err := awsSession.Config.Credentials.Get() - if err != nil { - err = errors.Wrap(err, "getting aws credentials") - ctx.Log.Err(err.Error()) - return ProjectResult{Error: err} - } - ctx.Log.Info("created aws session") - - credsEnvVars = []string{ - fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", creds.AccessKeyID), - fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", creds.SecretAccessKey), - fmt.Sprintf("AWS_SESSION_TOKEN=%s", creds.SessionToken), - } - } - // check if terraform version is >= 0.9.0 terraformVersion := a.terraform.Version() if config.TerraformVersion != nil { @@ -158,7 +131,7 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P constraints, _ := version.NewConstraint(">= 0.9.0") if constraints.Check(terraformVersion) { ctx.Log.Info("determined that we are running terraform with version >= 0.9.0. Running version %s", terraformVersion) - _, err := a.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), credsEnvVars, terraformVersion) + _, err := a.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), terraformVersion) if err != nil { return ProjectResult{Error: err} } @@ -172,8 +145,8 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P } } - tfApplyCmd := append([]string{"apply", "-no-color", plan.LocalPath}, applyExtraArgs...) - output, err := a.terraform.RunCommandWithVersion(ctx.Log, absolutePath, tfApplyCmd, credsEnvVars, terraformVersion) + tfApplyCmd := append([]string{"apply", "-no-color", "-var", fmt.Sprintf("%s=%s", atlantisUserTFVar, ctx.User.Username), plan.LocalPath}, applyExtraArgs...) + output, err := a.terraform.RunCommandWithVersion(ctx.Log, absolutePath, tfApplyCmd, terraformVersion) if err != nil { return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)} } diff --git a/server/plan_executor.go b/server/plan_executor.go index 2d217cd8c..f2a8a33ab 100644 --- a/server/plan_executor.go +++ b/server/plan_executor.go @@ -8,7 +8,6 @@ import ( "strings" version "github.com/hashicorp/go-version" - "github.com/hootsuite/atlantis/aws" "github.com/hootsuite/atlantis/github" "github.com/hootsuite/atlantis/locking" "github.com/hootsuite/atlantis/models" @@ -22,7 +21,6 @@ import ( type PlanExecutor struct { github *github.Client githubStatus *GithubStatus - awsConfig *aws.Config s3Bucket string terraform *terraform.Client githubCommentRenderer *GithubCommentRenderer @@ -119,31 +117,6 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. planExtraArgs = config.GetExtraArguments(ctx.Command.Name.String()) } - // todo: de-duplicate this section between plan and apply - var credsEnvVars []string - // If awsConfig is nil we know that we're not using assume role and so - // don't need to do an AWS calls ourselves - if p.awsConfig != nil { - awsSession, err := p.awsConfig.CreateSession(ctx.User.Username) - if err != nil { - ctx.Log.Err(err.Error()) - return ProjectResult{Error: err} - } - creds, err := awsSession.Config.Credentials.Get() - if err != nil { - err = errors.Wrap(err, "getting aws credentials") - ctx.Log.Err(err.Error()) - return ProjectResult{Error: err} - } - ctx.Log.Info("created aws session") - - credsEnvVars = []string{ - fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", creds.AccessKeyID), - fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", creds.SecretAccessKey), - fmt.Sprintf("AWS_SESSION_TOKEN=%s", creds.SessionToken), - } - } - // check if terraform version is >= 0.9.0 terraformVersion := p.terraform.Version() if config.TerraformVersion != nil { @@ -152,14 +125,14 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. constraints, _ := version.NewConstraint(">= 0.9.0") if constraints.Check(terraformVersion) { ctx.Log.Info("determined that we are running terraform with version >= 0.9.0. Running version %s", terraformVersion) - _, err := p.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), credsEnvVars, terraformVersion) + _, err := p.terraform.RunInitAndEnv(ctx.Log, absolutePath, tfEnv, config.GetExtraArguments("init"), terraformVersion) if err != nil { return ProjectResult{Error: err} } } else { ctx.Log.Info("determined that we are running terraform with version < 0.9.0. Running version %s", terraformVersion) terraformGetCmd := append([]string{"get", "-no-color"}, config.GetExtraArguments("get")...) - _, err := p.terraform.RunCommandWithVersion(ctx.Log, absolutePath, terraformGetCmd, nil, terraformVersion) + _, err := p.terraform.RunCommandWithVersion(ctx.Log, absolutePath, terraformGetCmd, terraformVersion) if err != nil { return ProjectResult{Error: err} } @@ -175,14 +148,14 @@ func (p *PlanExecutor) plan(ctx *CommandContext, repoDir string, project models. // Run terraform plan planFile := filepath.Join(repoDir, project.Path, fmt.Sprintf("%s.tfplan", tfEnv)) - tfPlanCmd := append([]string{"plan", "-refresh", "-no-color", "-out", planFile}, planExtraArgs...) + tfPlanCmd := append([]string{"plan", "-refresh", "-no-color", "-out", planFile, "-var", fmt.Sprintf("%s=%s", atlantisUserTFVar, ctx.User.Username)}, planExtraArgs...) // check if env/{environment}.tfvars exist tfEnvFileName := filepath.Join("env", tfEnv+".tfvars") if _, err := os.Stat(filepath.Join(repoDir, project.Path, tfEnvFileName)); err == nil { tfPlanCmd = append(tfPlanCmd, "-var-file", tfEnvFileName) } - output, err := p.terraform.RunCommandWithVersion(ctx.Log, filepath.Join(repoDir, project.Path), tfPlanCmd, credsEnvVars, terraformVersion) + output, err := p.terraform.RunCommandWithVersion(ctx.Log, filepath.Join(repoDir, project.Path), tfPlanCmd, terraformVersion) if err != nil { // plan failed so unlock the state if _, err := p.lockingClient.Unlock(lockAttempt.LockKey); err != nil { diff --git a/server/server.go b/server/server.go index 62a8e961d..3547307b8 100644 --- a/server/server.go +++ b/server/server.go @@ -15,7 +15,6 @@ import ( "github.com/elazarl/go-bindata-assetfs" gh "github.com/google/go-github/github" "github.com/gorilla/mux" - "github.com/hootsuite/atlantis/aws" "github.com/hootsuite/atlantis/github" "github.com/hootsuite/atlantis/locking" "github.com/hootsuite/atlantis/locking/boltdb" @@ -31,6 +30,9 @@ import ( const ( lockRoute = "lock-detail" + // atlantisUserTFVar is the name of the variable we execute terraform + // with containing the github username of who is running the command + atlantisUserTFVar = "atlantis_user" ) // Server listens for GitHub events and runs the necessary Atlantis command @@ -48,8 +50,6 @@ type Server struct { // the mapstructure tags correspond to flags in cmd/server.go type ServerConfig struct { - AWSRegion string `mapstructure:"aws-region"` - AssumeRole string `mapstructure:"aws-assume-role-arn"` AtlantisURL string `mapstructure:"atlantis-url"` DataDir string `mapstructure:"data-dir"` GithubHostname string `mapstructure:"gh-hostname"` @@ -92,16 +92,6 @@ func NewServer(config ServerConfig) (*Server, error) { } githubComments := &GithubCommentRenderer{} - // a nil awsConfig indicates that we won't be doing any AWS - // config in Atlantis - var awsConfig *aws.Config - if config.AssumeRole != "" { - awsConfig = &aws.Config{ - Region: config.AWSRegion, - RoleARN: config.AssumeRole, - } - } - boltdb, err := boltdb.New(config.DataDir) if err != nil { return nil, err @@ -116,7 +106,6 @@ func NewServer(config ServerConfig) (*Server, error) { applyExecutor := &ApplyExecutor{ github: githubClient, githubStatus: githubStatus, - awsConfig: awsConfig, terraform: terraformClient, githubCommentRenderer: githubComments, lockingClient: lockingClient, @@ -129,7 +118,6 @@ func NewServer(config ServerConfig) (*Server, error) { planExecutor := &PlanExecutor{ github: githubClient, githubStatus: githubStatus, - awsConfig: awsConfig, terraform: terraformClient, githubCommentRenderer: githubComments, lockingClient: lockingClient, @@ -308,9 +296,10 @@ func (s *Server) postEvents(w http.ResponseWriter, r *http.Request) { // webhook requests can either be application/json or application/x-www-form-urlencoded. // We accept both to make it easier on users that may choose x-www-form-urlencoded by mistake + // todo: use go-github's ValidatePayload method if https://github.com/google/go-github/pull/693 is merged if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { // GitHub stores the json payload as a form value - payloadForm := r.PostFormValue("payload") + payloadForm := r.FormValue("payload") if payloadForm == "" { s.respond(w, logging.Warn, http.StatusBadRequest, "request did not contain expected 'payload' form value") return diff --git a/terraform/terraform_client.go b/terraform/terraform_client.go index 748c35c02..9e8eb64b1 100644 --- a/terraform/terraform_client.go +++ b/terraform/terraform_client.go @@ -3,7 +3,6 @@ package terraform import ( "fmt" - "os" "os/exec" "regexp" @@ -52,8 +51,8 @@ func (c *Client) Version() *version.Version { } // RunCommandWithVersion executes the provided version of terraform with -// the provided args and envVars in path. -func (c *Client) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, envVars []string, v *version.Version) (string, error) { +// the provided args in path. +func (c *Client) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version) (string, error) { tfExecutable := "terraform" // if version is the same as the default, don't need to prepend the version name to the executable if !v.Equal(c.defaultVersion) { @@ -61,11 +60,6 @@ func (c *Client) RunCommandWithVersion(log *logging.SimpleLogger, path string, a } terraformCmd := exec.Command(tfExecutable, args...) terraformCmd.Dir = path - if len(envVars) > 0 { - // append current process's environment variables - // this is to prevent the $PATH variable being removed from the environment - terraformCmd.Env = append(os.Environ(), envVars...) - } out, err := terraformCmd.CombinedOutput() commandStr := strings.Join(terraformCmd.Args, " ") if err != nil { @@ -80,21 +74,21 @@ func (c *Client) RunCommandWithVersion(log *logging.SimpleLogger, path string, a // RunInitAndEnv executes "terraform init" and "terraform env select" in path. // env is the environment to select and extraInitArgs are additional arguments // applied to the init command. -func (c *Client) RunInitAndEnv(log *logging.SimpleLogger, path string, env string, extraInitArgs []string, envVars []string, version *version.Version) ([]string, error) { +func (c *Client) RunInitAndEnv(log *logging.SimpleLogger, path string, env string, extraInitArgs []string, version *version.Version) ([]string, error) { var outputs []string // run terraform init - output, err := c.RunCommandWithVersion(log, path, append([]string{"init", "-no-color"}, extraInitArgs...), envVars, version) + output, err := c.RunCommandWithVersion(log, path, append([]string{"init", "-no-color"}, extraInitArgs...), version) if err != nil { return nil, err } outputs = append(outputs, output) // run terraform env new and select - output, err = c.RunCommandWithVersion(log, path, []string{"env", "select", "-no-color", env}, envVars, version) + output, err = c.RunCommandWithVersion(log, path, []string{"env", "select", "-no-color", env}, version) if err != nil { // if terraform env select fails we will run terraform env new // to create a new environment - output, err = c.RunCommandWithVersion(log, path, []string{"env", "new", "-no-color", env}, envVars, version) + output, err = c.RunCommandWithVersion(log, path, []string{"env", "new", "-no-color", env}, version) if err != nil { return nil, err }