Merge pull request #287 from runatlantis/tfvarsrebase

Execute tf with vars that give source of run.
This commit is contained in:
Luke Kysow
2018-09-27 14:55:41 -05:00
committed by GitHub
3 changed files with 259 additions and 40 deletions

View File

@@ -47,27 +47,37 @@ won't work for multiple accounts since Atlantis wouldn't know which environment
Terraform with.
### 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 which would allow you to view the GitHub username associated with the AWS API calls
being made during a `plan` or `apply` in CloudWatch.
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}"
}
}
Atlantis injects 3 Terraform variables that can be used to dynamically name the assume role
session:
```bash
# Set to the VCS username of who is running the plan command, ex. lkysow
variable "atlantis_user" {
default = "atlantis_user"
}
# Set to the full name of the repo the pull request is in, ex. runatlantis/atlantis
variable "atlantis_repo" {
default = "atlantis_repo"
}
# Set to the pull request number, ex. 200
variable "atlantis_pull_num" {
default = "atlantis_pull_num"
}
# Can be used within the assume_role block for session_name.
provider "aws" {
assume_role {
role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
session_name = "${var.atlantis_user}:${var.atlantis_repo}:${var.atlantis_pull_num}"
}
}
```
Setting `session_name` allows you to trace where API calls made through Atlantis came from in
CloudWatch.
If you're also using the [S3 Backend](https://www.terraform.io/docs/backends/types/s3.html)
make sure to add the `role_arn` option:
@@ -85,8 +95,10 @@ terraform {
}
```
::: warning
Terraform doesn't support interpolations in backend config so you will not be
able to use `session_name = "${var.atlantis_user}"`. However, the backend assumed
role is only used for state-related API actions. Any other API actions will be performed using
the assumed role specified in the `aws` provider and will have the session named as the GitHub user.
:::

View File

@@ -10,9 +10,18 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
)
// atlantisUserTFVar is the name of the variable we execute terraform
// with, containing the vcs username of who is running the command
// atlantisUserTFVar is the name of the tf variable we execute terraform
// with set to the vcs username of who caused the plan command to run.
const atlantisUserTFVar = "atlantis_user"
// atlantisRepoTFVar is the name of the tf variable we execute terraform
// with set to the full name of the repository this pull request is from, ex.
// "runatlantis/atlantis", "repo/gitlab/subgroup".
const atlantisRepoTFVar = "atlantis_repo"
// atlantisPullNumTFVar is the name of the tf variable we execute terraform
// with set to the number of the pull request.
const atlantisPullNumTFVar = "atlantis_pull_num"
const defaultWorkspace = "default"
type PlanStepRunner struct {
@@ -32,20 +41,8 @@ func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
return "", err
}
planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectConfig))
userVar := fmt.Sprintf("%s=%s", atlantisUserTFVar, ctx.User.Username)
tfPlanCmd := append(append([]string{"plan", "-input=false", "-refresh", "-no-color", "-out", planFile, "-var", userVar}, extraArgs...), ctx.CommentArgs...)
// Check if env/{workspace}.tfvars exist and include it. This is a use-case
// from Hootsuite where Atlantis was first created so we're keeping this as
// an homage and a favor so they don't need to refactor all their repos.
// It's also a nice way to structure your repos to reduce duplication.
optionalEnvFile := filepath.Join(path, "env", ctx.Workspace+".tfvars")
if _, err := os.Stat(optionalEnvFile); err == nil {
tfPlanCmd = append(tfPlanCmd, "-var-file", optionalEnvFile)
}
return p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Join(path), tfPlanCmd, tfVersion, ctx.Workspace)
planCmd := p.buildPlanCmd(ctx, extraArgs, path)
return p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, tfVersion, ctx.Workspace)
}
// switchWorkspace changes the terraform workspace if necessary and will create
@@ -97,3 +94,53 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path
}
return nil
}
func (p *PlanStepRunner) buildPlanCmd(ctx models.ProjectCommandContext, extraArgs []string, path string) []string {
tfVars := p.tfVars(ctx.User.Username, ctx.BaseRepo.FullName, ctx.Pull.Num)
planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectConfig))
// Check if env/{workspace}.tfvars exist and include it. This is a use-case
// from Hootsuite where Atlantis was first created so we're keeping this as
// an homage and a favor so they don't need to refactor all their repos.
// It's also a nice way to structure your repos to reduce duplication.
var envFileArgs []string
envFile := filepath.Join(path, "env", ctx.Workspace+".tfvars")
if _, err := os.Stat(envFile); err == nil {
envFileArgs = []string{"-var-file", envFile}
}
argList := [][]string{
{"plan", "-input=false", "-refresh", "-no-color", "-out", planFile},
tfVars,
extraArgs,
ctx.CommentArgs,
envFileArgs,
}
return p.flatten(argList)
}
// tfVars returns a list of "-var", "key=value" pairs that identify who and which
// repo this command is running for. This can be used for naming the
// session name in AWS which will identify in CloudTrail the source of
// Atlantis API calls.
func (p *PlanStepRunner) tfVars(username string, baseRepoFullName string, pullNum int) []string {
// NOTE: not using maps and looping here because we need to keep the
// ordering for testing purposes.
return []string{
"-var",
fmt.Sprintf("%s=%s", atlantisUserTFVar, username),
"-var",
fmt.Sprintf("%s=%s", atlantisRepoTFVar, baseRepoFullName),
"-var",
fmt.Sprintf("%s=%d", atlantisPullNumTFVar, pullNum),
}
}
func (p *PlanStepRunner) flatten(slices [][]string) []string {
var flattened []string
for _, v := range slices {
flattened = append(flattened, v...)
}
return flattened
}

View File

@@ -40,15 +40,55 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
Workspace: workspace,
RepoRelDir: ".",
User: models.User{Username: "username"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, "/path")
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{"plan", "-input=false", "-refresh", "-no-color", "-out", "/path/default.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}, tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(
logger,
"/path",
[]string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
"/path/default.tfplan",
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args"},
tfVersion,
workspace)
// Verify that no env or workspace commands were run
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"env", "select", "-no-color", "workspace"}, tfVersion, workspace)
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, tfVersion, workspace)
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger,
"/path",
[]string{"env",
"select",
"-no-color",
"workspace"},
tfVersion,
workspace)
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger,
"/path",
[]string{"workspace",
"select",
"-no-color",
"workspace"},
tfVersion,
workspace)
}
func TestRun_ErrWorkspaceIn08(t *testing.T) {
@@ -121,13 +161,45 @@ func TestRun_SwitchesWorkspace(t *testing.T) {
RepoRelDir: ".",
User: models.User{Username: "username"},
CommentArgs: []string{"comment", "args"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, "/path")
Ok(t, err)
Equals(t, "output", output)
// Verify that env select was called as well as plan.
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{c.expWorkspaceCmd, "select", "-no-color", "workspace"}, tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{"plan", "-input=false", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}, tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger,
"/path",
[]string{c.expWorkspaceCmd,
"select",
"-no-color",
"workspace"},
tfVersion,
"workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger,
"/path",
[]string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
"/path/workspace.tfplan",
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args"},
tfVersion,
"workspace")
})
}
}
@@ -175,7 +247,22 @@ func TestRun_CreatesWorkspace(t *testing.T) {
expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "-no-color", "workspace"}
When(terraform.RunCommandWithVersion(logger, "/path", expWorkspaceArgs, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
expPlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}
expPlanArgs := []string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
"/path/workspace.tfplan",
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args"}
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
output, err := s.Run(models.ProjectCommandContext{
@@ -184,6 +271,12 @@ func TestRun_CreatesWorkspace(t *testing.T) {
RepoRelDir: ".",
User: models.User{Username: "username"},
CommentArgs: []string{"comment", "args"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, "/path")
Ok(t, err)
@@ -208,7 +301,22 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
}
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil)
expPlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}
expPlanArgs := []string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
"/path/workspace.tfplan",
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args"}
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
output, err := s.Run(models.ProjectCommandContext{
@@ -217,6 +325,12 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
RepoRelDir: ".",
User: models.User{Username: "username"},
CommentArgs: []string{"comment", "args"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, "/path")
Ok(t, err)
@@ -249,7 +363,25 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
DefaultTFVersion: tfVersion,
}
expPlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "-out", filepath.Join(tmpDir, "workspace.tfplan"), "-var", "atlantis_user=username", "extra", "args", "comment", "args", "-var-file", envVarsFile}
expPlanArgs := []string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
filepath.Join(tmpDir, "workspace.tfplan"),
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args",
"-var-file",
envVarsFile,
}
When(terraform.RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
output, err := s.Run(models.ProjectCommandContext{
@@ -258,6 +390,12 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
RepoRelDir: ".",
User: models.User{Username: "username"},
CommentArgs: []string{"comment", "args"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, tmpDir)
Ok(t, err)
@@ -280,7 +418,23 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
}
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil)
expPlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "-out", "/path/projectname-default.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}
expPlanArgs := []string{"plan",
"-input=false",
"-refresh",
"-no-color",
"-out",
"/path/projectname-default.tfplan",
"-var",
"atlantis_user=username",
"-var",
"atlantis_repo=owner/repo",
"-var",
"atlantis_pull_num=2",
"extra",
"args",
"comment",
"args",
}
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "default")).ThenReturn("output", nil)
projectName := "projectname"
@@ -293,6 +447,12 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
ProjectConfig: &valid.Project{
Name: &projectName,
},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
},
}, []string{"extra", "args"}, "/path")
Ok(t, err)
Equals(t, "output", output)