Remove assume role functionality. Rely on Terraform. Fixes #112

This commit is contained in:
Luke Kysow
2017-08-14 23:41:17 -07:00
committed by Luke Kysow
parent 45dda728fc
commit 6dc9de175d
8 changed files with 59 additions and 165 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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