mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-03 00:28:37 +00:00
Merge pull request #287 from runatlantis/tfvarsrebase
Execute tf with vars that give source of run.
This commit is contained in:
@@ -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.
|
||||
:::
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user