Execute tf with vars that give source of run.

Add new variables to execute TF with:
  -var atlantis_repo=owner/repo
  -var atlantis_pull_num=10

These can be used within TF to set the session_name of the AWS session
that Terraform creates. This allows all API calls to be traced back to a
specific pull request and user.
This commit is contained in:
Jeremy Olexa
2018-09-24 11:20:43 -05:00
committed by Luke Kysow
parent 411b8f87f1
commit c278bac6be
2 changed files with 232 additions and 25 deletions

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)