Skip to content

Commit

Permalink
Remove assume role functionality. Rely on Terraform. Fixes hootsuite#112
Browse files Browse the repository at this point in the history
  • Loading branch information
lkysow authored and Luke Kysow committed Aug 15, 2017
1 parent 45dda72 commit 6dc9de1
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 166 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))

Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### 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`.
### 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.

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`.
[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.

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).
### Assume Role Session Names
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.

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

```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
Expand Down
58 changes: 0 additions & 58 deletions aws/aws.go

This file was deleted.

11 changes: 0 additions & 11 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.",
Expand Down
33 changes: 3 additions & 30 deletions server/apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,7 +19,6 @@ import (
type ApplyExecutor struct {
github *github.Client
githubStatus *GithubStatus
awsConfig *aws.Config
terraform *terraform.Client
githubCommentRenderer *GithubCommentRenderer
lockingClient *locking.Client
Expand Down Expand Up @@ -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 {
Expand All @@ -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}
}
Expand All @@ -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)}
}
Expand Down
35 changes: 4 additions & 31 deletions server/plan_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -22,7 +21,6 @@ import (
type PlanExecutor struct {
github *github.Client
githubStatus *GithubStatus
awsConfig *aws.Config
s3Bucket string
terraform *terraform.Client
githubCommentRenderer *GithubCommentRenderer
Expand Down Expand Up @@ -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 {
Expand All @@ -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}
}
Expand All @@ -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 {
Expand Down
21 changes: 5 additions & 16 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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"`
Expand Down Expand Up @@ -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
Expand All @@ -116,7 +106,6 @@ func NewServer(config ServerConfig) (*Server, error) {
applyExecutor := &ApplyExecutor{
github: githubClient,
githubStatus: githubStatus,
awsConfig: awsConfig,
terraform: terraformClient,
githubCommentRenderer: githubComments,
lockingClient: lockingClient,
Expand All @@ -129,7 +118,6 @@ func NewServer(config ServerConfig) (*Server, error) {
planExecutor := &PlanExecutor{
github: githubClient,
githubStatus: githubStatus,
awsConfig: awsConfig,
terraform: terraformClient,
githubCommentRenderer: githubComments,
lockingClient: lockingClient,
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 6dc9de1

Please sign in to comment.