diff --git a/server/events/command_runner.go b/server/events/command_runner.go
index e70604160..593ded93d 100644
--- a/server/events/command_runner.go
+++ b/server/events/command_runner.go
@@ -145,7 +145,14 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
ctx.Log.Warn("unable to update commit status: %s", err)
}
- result := c.runProjectCmdsParallel(projectCmds, models.PlanCommand)
+ // Only run commands in parallel if enabled
+ var result CommandResult
+ if c.parallelPlanEnabled(ctx, projectCmds) {
+ ctx.Log.Info("Running plans in parallel")
+ result = c.runProjectCmdsParallel(projectCmds, models.PlanCommand)
+ } else {
+ result = c.runProjectCmds(projectCmds, models.PlanCommand)
+ }
if c.automergeEnabled(ctx, projectCmds) && result.HasErrors() {
ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed")
@@ -257,14 +264,18 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
return
}
- // Only run applies in parallel if enabled
+ // Only run commands in parallel if enabled
var result CommandResult
- if cmd.Name == models.ApplyCommand && !c.parallelApplyEnabled(ctx, projectCmds) {
- result = c.runProjectCmds(projectCmds, cmd.Name)
- } else {
- ctx.Log.Info("Running commands in parallel")
+ if cmd.Name == models.ApplyCommand && c.parallelApplyEnabled(ctx, projectCmds) {
+ ctx.Log.Info("Running applies in parallel")
result = c.runProjectCmdsParallel(projectCmds, cmd.Name)
+ } else if cmd.Name == models.PlanCommand && c.parallelPlanEnabled(ctx, projectCmds) {
+ ctx.Log.Info("Running plans in parallel")
+ result = c.runProjectCmdsParallel(projectCmds, cmd.Name)
+ } else {
+ result = c.runProjectCmds(projectCmds, cmd.Name)
}
+
if cmd.Name == models.PlanCommand && c.automergeEnabled(ctx, projectCmds) && result.HasErrors() {
ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed")
c.deletePlans(ctx)
@@ -546,6 +557,11 @@ func (c *DefaultCommandRunner) parallelApplyEnabled(ctx *CommandContext, project
return len(projectCmds) > 0 && projectCmds[0].ParallelApplyEnabled
}
+// parallelPlanEnabled returns true if parallel plan is enabled in this context.
+func (c *DefaultCommandRunner) parallelPlanEnabled(ctx *CommandContext, projectCmds []models.ProjectCommandContext) bool {
+ return len(projectCmds) > 0 && projectCmds[0].ParallelPlanEnabled
+}
+
// automergeComment is the comment that gets posted when Atlantis automatically
// merges the PR.
var automergeComment = `Automatically merging because all plans have been successfully applied.`
diff --git a/server/events/models/models.go b/server/events/models/models.go
index b922d2245..4a49ba8dd 100644
--- a/server/events/models/models.go
+++ b/server/events/models/models.go
@@ -310,6 +310,8 @@ type ProjectCommandContext struct {
AutomergeEnabled bool
// ParallelApplyEnabled is true if parallel apply is enabled for this project.
ParallelApplyEnabled bool
+ // ParallelPlanEnabled is true if parallel plan is enabled for this project.
+ ParallelPlanEnabled bool
// AutoplanEnabled is true if autoplanning is enabled for this project.
AutoplanEnabled bool
// BaseRepo is the repository that the pull request will be merged into.
diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go
index 79c497595..6da0bd759 100644
--- a/server/events/project_command_builder.go
+++ b/server/events/project_command_builder.go
@@ -27,6 +27,8 @@ const (
DefaultAutomergeEnabled = false
// DefaultParallelApplyEnabled is the default for the parallel apply setting.
DefaultParallelApplyEnabled = false
+ // DefaultParallelPlanEnabled is the default for the parallel plan setting.
+ DefaultParallelPlanEnabled = false
)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder
@@ -143,7 +145,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
for _, mp := range matchingProjects {
ctx.Log.Debug("determining config for project at dir: %q workspace: %q", mp.Dir, mp.Workspace)
mergedCfg := p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.BaseRepo.ID(), mp, repoCfg)
- projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, repoCfg.ParallelApply, verbose, repoDir))
+ projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, repoCfg.ParallelApply, repoCfg.ParallelPlan, verbose, repoDir))
}
} else {
// If there is no config file, then we'll plan each project that
@@ -154,7 +156,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
for _, mp := range modifiedProjects {
ctx.Log.Debug("determining config for project at dir: %q", mp.Path)
pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.BaseRepo.ID(), mp.Path, DefaultWorkspace)
- projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, DefaultParallelApplyEnabled, verbose, repoDir))
+ projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, DefaultParallelApplyEnabled, DefaultParallelPlanEnabled, verbose, repoDir))
}
}
@@ -286,11 +288,13 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
+ parallelPlan := DefaultParallelPlanEnabled
if repoCfgPtr != nil {
automerge = repoCfgPtr.Automerge
parallelApply = repoCfgPtr.ParallelApply
+ parallelPlan = repoCfgPtr.ParallelPlan
}
- return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, parallelApply, verbose, repoDir), nil
+ return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, parallelApply, parallelPlan, verbose, repoDir), nil
}
// getCfg returns the atlantis.yaml config (if it exists) for this project. If
@@ -380,6 +384,7 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
commentArgs []string,
automergeEnabled bool,
parallelApplyEnabled bool,
+ parallelPlanEnabled bool,
verbose bool,
absRepoDir string) models.ProjectCommandContext {
@@ -403,6 +408,7 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
EscapedCommentArgs: p.escapeArgs(commentArgs),
AutomergeEnabled: automergeEnabled,
ParallelApplyEnabled: parallelApplyEnabled,
+ ParallelPlanEnabled: parallelPlanEnabled,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
diff --git a/server/events/terraform/terraform_client.go b/server/events/terraform/terraform_client.go
index 68e39d6ab..f196e7261 100644
--- a/server/events/terraform/terraform_client.go
+++ b/server/events/terraform/terraform_client.go
@@ -65,6 +65,9 @@ type DefaultClient struct {
// versionsLock is used to ensure versions isn't being concurrently written to.
versionsLock *sync.Mutex
+
+ // usePluginCache determines whether or not to set the TF_PLUGIN_CACHE_DIR env var
+ usePluginCache bool
}
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_downloader.go Downloader
@@ -107,7 +110,8 @@ func NewClient(
defaultVersionStr string,
defaultVersionFlagName string,
tfDownloadURL string,
- tfDownloader Downloader) (*DefaultClient, error) {
+ tfDownloader Downloader,
+ usePluginCache bool) (*DefaultClient, error) {
var finalDefaultVersion *version.Version
var localVersion *version.Version
versions := make(map[string]string)
@@ -179,6 +183,7 @@ func NewClient(
downloadBaseURL: tfDownloadURL,
versionsLock: &versionsLock,
versions: versions,
+ usePluginCache: usePluginCache,
}, nil
}
@@ -259,11 +264,13 @@ func (c *DefaultClient) prepCmd(log *logging.SimpleLogger, v *version.Version, w
// Will de-emphasize specific commands to run in output.
"TF_IN_AUTOMATION=true",
// Cache plugins so terraform init runs faster.
- fmt.Sprintf("TF_PLUGIN_CACHE_DIR=%s", c.terraformPluginCacheDir),
fmt.Sprintf("WORKSPACE=%s", workspace),
fmt.Sprintf("ATLANTIS_TERRAFORM_VERSION=%s", v.String()),
fmt.Sprintf("DIR=%s", path),
}
+ if c.usePluginCache {
+ envVars = append(envVars, fmt.Sprintf("TF_PLUGIN_CACHE_DIR=%s", c.terraformPluginCacheDir))
+ }
// Append current Atlantis process's environment variables, ex.
// AWS_ACCESS_KEY.
envVars = append(envVars, os.Environ()...)
diff --git a/server/events/terraform/terraform_client_internal_test.go b/server/events/terraform/terraform_client_internal_test.go
index 97c40795c..b2504f650 100644
--- a/server/events/terraform/terraform_client_internal_test.go
+++ b/server/events/terraform/terraform_client_internal_test.go
@@ -94,6 +94,7 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) {
defaultVersion: v,
terraformPluginCacheDir: tmp,
overrideTF: "echo",
+ usePluginCache: true,
}
args := []string{
@@ -143,6 +144,7 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) {
defaultVersion: v,
terraformPluginCacheDir: tmp,
overrideTF: "echo",
+ usePluginCache: true,
}
args := []string{
diff --git a/server/events/terraform/terraform_client_test.go b/server/events/terraform/terraform_client_test.go
index 0f5cec44a..18ac5525b 100644
--- a/server/events/terraform/terraform_client_test.go
+++ b/server/events/terraform/terraform_client_test.go
@@ -68,7 +68,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
- c, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ c, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
Ok(t, err)
Ok(t, err)
@@ -96,7 +96,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
- c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
Ok(t, err)
Ok(t, err)
@@ -116,7 +116,7 @@ func TestNewClient_NoTF(t *testing.T) {
// Set PATH to only include our empty directory.
defer tempSetEnv(t, "PATH", tmp)()
- _, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ _, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://www.terraform.io/downloads.html", err)
}
@@ -133,7 +133,7 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
- c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
Ok(t, err)
Ok(t, err)
@@ -157,7 +157,7 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()
- c, err := terraform.NewClient(logging.NewNoopLogger(), tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ c, err := terraform.NewClient(logging.NewNoopLogger(), tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
Ok(t, err)
Ok(t, err)
@@ -183,7 +183,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
err := ioutil.WriteFile(params[0].(string), []byte("#!/bin/sh\necho '\nTerraform v0.11.10\n'"), 0755)
return []pegomock.ReturnValue{err}
})
- c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, "https://my-mirror.releases.mycompany.com", mockDownloader)
+ c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, "https://my-mirror.releases.mycompany.com", mockDownloader, true)
Ok(t, err)
Ok(t, err)
@@ -207,7 +207,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
func TestNewClient_BadVersion(t *testing.T) {
tmp, cleanup := TempDir(t)
defer cleanup()
- _, err := terraform.NewClient(nil, tmp, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil)
+ _, err := terraform.NewClient(nil, tmp, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
ErrEquals(t, "Malformed version: malformed", err)
}
@@ -230,7 +230,7 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
return []pegomock.ReturnValue{err}
})
- c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader)
+ c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -249,7 +249,7 @@ func TestEnsureVersion_downloaded(t *testing.T) {
mockDownloader := mocks.NewMockDownloader()
- c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader)
+ c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
diff --git a/server/events/yaml/raw/repo_cfg.go b/server/events/yaml/raw/repo_cfg.go
index 5db42fb3c..cd5197727 100644
--- a/server/events/yaml/raw/repo_cfg.go
+++ b/server/events/yaml/raw/repo_cfg.go
@@ -13,6 +13,9 @@ const DefaultAutomerge = false
// DefaultParallelApply is the default setting for parallel apply
const DefaultParallelApply = false
+// DefaultParallelPlan is the default setting for parallel plan
+const DefaultParallelPlan = false
+
// RepoCfg is the raw schema for repo-level atlantis.yaml config.
type RepoCfg struct {
Version *int `yaml:"version,omitempty"`
@@ -20,6 +23,7 @@ type RepoCfg struct {
Workflows map[string]Workflow `yaml:"workflows,omitempty"`
Automerge *bool `yaml:"automerge,omitempty"`
ParallelApply *bool `yaml:"parallel_apply,omitempty"`
+ ParallelPlan *bool `yaml:"parallel_plan,omitempty"`
}
func (r RepoCfg) Validate() error {
@@ -61,11 +65,17 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
parallelApply = *r.ParallelApply
}
+ parallelPlan := DefaultParallelPlan
+ if r.ParallelPlan != nil {
+ parallelPlan = *r.ParallelPlan
+ }
+
return valid.RepoCfg{
Version: *r.Version,
Projects: validProjects,
Workflows: validWorkflows,
Automerge: automerge,
ParallelApply: parallelApply,
+ ParallelPlan: parallelPlan,
}
}
diff --git a/server/events/yaml/raw/repo_cfg_test.go b/server/events/yaml/raw/repo_cfg_test.go
index ed320c8e9..8f0dcaf45 100644
--- a/server/events/yaml/raw/repo_cfg_test.go
+++ b/server/events/yaml/raw/repo_cfg_test.go
@@ -127,6 +127,7 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
version: 3
automerge: true
parallel_apply: true
+parallel_plan: false
projects:
- dir: mydir
workspace: myworkspace
@@ -146,6 +147,7 @@ workflows:
Version: Int(3),
Automerge: Bool(true),
ParallelApply: Bool(true),
+ ParallelPlan: Bool(false),
Projects: []raw.Project{
{
Dir: String("mydir"),
diff --git a/server/events/yaml/valid/repo_cfg.go b/server/events/yaml/valid/repo_cfg.go
index cc4149895..b05b6b124 100644
--- a/server/events/yaml/valid/repo_cfg.go
+++ b/server/events/yaml/valid/repo_cfg.go
@@ -12,6 +12,7 @@ type RepoCfg struct {
Workflows map[string]Workflow
Automerge bool
ParallelApply bool
+ ParallelPlan bool
}
func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {
diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go
index be4fda14d..5cf9342f5 100644
--- a/server/events_controller_e2e_test.go
+++ b/server/events_controller_e2e_test.go
@@ -60,9 +60,12 @@ func TestGitHubWorkflow(t *testing.T) {
ExpAutomerge bool
// ExpAutoplan is true if we expect Atlantis to autoplan.
ExpAutoplan bool
+ // ExpParallel is true if we expect Atlantis to run parallel plans or applies.
+ ExpParallel bool
// ExpReplies is a list of files containing the expected replies that
- // Atlantis writes to the pull request in order.
- ExpReplies []string
+ // Atlantis writes to the pull request in order. A reply from a parallel operation
+ // will be matched using a substring check.
+ ExpReplies [][]string
}{
{
Description: "simple",
@@ -71,10 +74,10 @@ func TestGitHubWorkflow(t *testing.T) {
Comments: []string{
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply.txt"},
+ []string{"exp-output-merge.txt"},
},
ExpAutoplan: true,
},
@@ -87,11 +90,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis plan",
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-autoplan.txt",
- "exp-output-apply.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -103,11 +106,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis plan -- -var var=overridden",
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-atlantis-plan-var-overridden.txt",
- "exp-output-apply-var.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-atlantis-plan-var-overridden.txt"},
+ []string{"exp-output-apply-var.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -121,13 +124,13 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -w default",
"atlantis apply -w new_workspace",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-atlantis-plan.txt",
- "exp-output-atlantis-plan-new-workspace.txt",
- "exp-output-apply-var-default-workspace.txt",
- "exp-output-apply-var-new-workspace.txt",
- "exp-output-merge-workspaces.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-atlantis-plan.txt"},
+ []string{"exp-output-atlantis-plan-new-workspace.txt"},
+ []string{"exp-output-apply-var-default-workspace.txt"},
+ []string{"exp-output-apply-var-new-workspace.txt"},
+ []string{"exp-output-merge-workspaces.txt"},
},
},
{
@@ -140,12 +143,12 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis plan -w new_workspace -- -var var=new_workspace",
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-atlantis-plan.txt",
- "exp-output-atlantis-plan-new-workspace.txt",
- "exp-output-apply-var-all.txt",
- "exp-output-merge-workspaces.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-atlantis-plan.txt"},
+ []string{"exp-output-atlantis-plan-new-workspace.txt"},
+ []string{"exp-output-apply-var-all.txt"},
+ []string{"exp-output-merge-workspaces.txt"},
},
},
{
@@ -157,11 +160,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -w staging",
"atlantis apply -w default",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-staging.txt",
- "exp-output-apply-default.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-apply-default.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -172,10 +175,10 @@ func TestGitHubWorkflow(t *testing.T) {
Comments: []string{
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-all.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-all.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -187,11 +190,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis plan",
"atlantis apply",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-autoplan.txt",
- "exp-output-apply-all.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-all.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -202,10 +205,10 @@ func TestGitHubWorkflow(t *testing.T) {
Comments: []string{
"atlantis apply -d staging",
},
- ExpReplies: []string{
- "exp-output-autoplan-only-staging.txt",
- "exp-output-apply-staging.txt",
- "exp-output-merge-only-staging.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan-only-staging.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-merge-only-staging.txt"},
},
},
{
@@ -219,12 +222,12 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -d staging",
"atlantis apply -d production",
},
- ExpReplies: []string{
- "exp-output-plan-staging.txt",
- "exp-output-plan-production.txt",
- "exp-output-apply-staging.txt",
- "exp-output-apply-production.txt",
- "exp-output-merge-all-dirs.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-plan-staging.txt"},
+ []string{"exp-output-plan-production.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-apply-production.txt"},
+ []string{"exp-output-merge-all-dirs.txt"},
},
},
{
@@ -236,11 +239,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -d staging",
"atlantis apply -d production",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-staging.txt",
- "exp-output-apply-production.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-apply-production.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -252,11 +255,11 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -p staging",
"atlantis apply -p default",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-staging.txt",
- "exp-output-apply-default.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-apply-default.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -270,12 +273,12 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -p staging",
"atlantis apply -p default",
},
- ExpReplies: []string{
- "exp-output-plan-staging.txt",
- "exp-output-plan-default.txt",
- "exp-output-apply-staging.txt",
- "exp-output-apply-default.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-plan-staging.txt"},
+ []string{"exp-output-plan-default.txt"},
+ []string{"exp-output-apply-staging.txt"},
+ []string{"exp-output-apply-default.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -288,12 +291,12 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -d dir1",
"atlantis apply -d dir2",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-dir1.txt",
- "exp-output-apply-dir2.txt",
- "exp-output-automerge.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-dir1.txt"},
+ []string{"exp-output-apply-dir2.txt"},
+ []string{"exp-output-automerge.txt"},
+ []string{"exp-output-merge.txt"},
},
},
{
@@ -306,11 +309,26 @@ func TestGitHubWorkflow(t *testing.T) {
"atlantis apply -w staging",
"atlantis apply -w default",
},
- ExpReplies: []string{
- "exp-output-autoplan.txt",
- "exp-output-apply-staging-workspace.txt",
- "exp-output-apply-default-workspace.txt",
- "exp-output-merge.txt",
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan.txt"},
+ []string{"exp-output-apply-staging-workspace.txt"},
+ []string{"exp-output-apply-default-workspace.txt"},
+ []string{"exp-output-merge.txt"},
+ },
+ },
+ {
+ Description: "workspaces parallel with atlantis.yaml",
+ RepoDir: "workspace-parallel-yaml",
+ ModifiedFiles: []string{"production/main.tf", "staging/main.tf"},
+ ExpAutoplan: true,
+ ExpParallel: true,
+ Comments: []string{
+ "atlantis apply",
+ },
+ ExpReplies: [][]string{
+ []string{"exp-output-autoplan-staging.txt", "exp-output-autoplan-production.txt"},
+ []string{"exp-output-apply-all-staging.txt", "exp-output-apply-all-production.txt"},
+ []string{"exp-output-merge.txt"},
},
},
}
@@ -364,7 +382,7 @@ func TestGitHubWorkflow(t *testing.T) {
_, _, actReplies := vcsClient.VerifyWasCalled(Times(expNumReplies)).CreateComment(AnyRepo(), AnyInt(), AnyString()).GetAllCapturedArguments()
Assert(t, len(c.ExpReplies) == len(actReplies), "missing expected replies, got %d but expected %d", len(actReplies), len(c.ExpReplies))
for i, expReply := range c.ExpReplies {
- assertCommentEquals(t, expReply, actReplies[i], c.RepoDir)
+ assertCommentEquals(t, expReply, actReplies[i], c.RepoDir, c.ExpParallel)
}
if c.ExpAutomerge {
@@ -400,7 +418,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
GithubUser: "github-user",
GitlabUser: "gitlab-user",
}
- terraformClient, err := terraform.NewClient(logger, dataDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", &NoopTFDownloader{})
+ terraformClient, err := terraform.NewClient(logger, dataDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", &NoopTFDownloader{}, false)
Ok(t, err)
boltdb, err := db.New(dataDir)
Ok(t, err)
@@ -617,10 +635,8 @@ func runCmd(t *testing.T, dir string, name string, args ...string) string {
return string(cpOut)
}
-func assertCommentEquals(t *testing.T, expFile string, act string, repoDir string) {
+func assertCommentEquals(t *testing.T, expReplies []string, act string, repoDir string, parallel bool) {
t.Helper()
- exp, err := ioutil.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))
- Ok(t, err)
// Replace all 'Creation complete after 0s [id=2135833172528078362]' strings with
// 'Creation complete after *s [id=*******************]' so we can do a comparison.
@@ -633,29 +649,45 @@ func assertCommentEquals(t *testing.T, expFile string, act string, repoDir strin
resourceRegex := regexp.MustCompile(`null_resource\.simple(\[\d])?\d?:.*`)
act = resourceRegex.ReplaceAllString(act, "null_resource.simple:")
- expStr := string(exp)
- // My editor adds a newline to all the files, so if the actual comment
- // doesn't end with a newline then strip the last newline from the file's
- // contents.
- if !strings.HasSuffix(act, "\n") {
- expStr = strings.TrimSuffix(expStr, "\n")
+ // For parallel plans and applies, do a substring match since output may be out of order
+ var replyMatchesExpected func(string, string) bool
+ if parallel {
+ replyMatchesExpected = func(act string, expStr string) bool {
+ return strings.Contains(act, expStr)
+ }
+ } else {
+ replyMatchesExpected = func(act string, expStr string) bool {
+ return expStr == act
+ }
}
- if expStr != act {
- // If in CI, we write the diff to the console. Otherwise we write the diff
- // to file so we can use our local diff viewer.
- if os.Getenv("CI") == "true" {
- t.Logf("exp: %s, got: %s", expStr, act)
- t.FailNow()
- } else {
- actFile := filepath.Join(absRepoPath(t, repoDir), expFile+".act")
- err := ioutil.WriteFile(actFile, []byte(act), 0600)
- Ok(t, err)
- cwd, err := os.Getwd()
- Ok(t, err)
- rel, err := filepath.Rel(cwd, actFile)
- Ok(t, err)
- t.Errorf("%q was different, wrote actual comment to %q", expFile, rel)
+ for _, expFile := range expReplies {
+ exp, err := ioutil.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))
+ Ok(t, err)
+ expStr := string(exp)
+ // My editor adds a newline to all the files, so if the actual comment
+ // doesn't end with a newline then strip the last newline from the file's
+ // contents.
+ if !strings.HasSuffix(act, "\n") {
+ expStr = strings.TrimSuffix(expStr, "\n")
+ }
+
+ if !replyMatchesExpected(act, expStr) {
+ // If in CI, we write the diff to the console. Otherwise we write the diff
+ // to file so we can use our local diff viewer.
+ if os.Getenv("CI") == "true" {
+ t.Logf("exp: %s, got: %s", expStr, act)
+ t.FailNow()
+ } else {
+ actFile := filepath.Join(absRepoPath(t, repoDir), expFile+".act")
+ err := ioutil.WriteFile(actFile, []byte(act), 0600)
+ Ok(t, err)
+ cwd, err := os.Getwd()
+ Ok(t, err)
+ rel, err := filepath.Rel(cwd, actFile)
+ Ok(t, err)
+ t.Errorf("%q was different, wrote actual comment to %q", expFile, rel)
+ }
}
}
}
diff --git a/server/server.go b/server/server.go
index 831bf4462..6eff63ef7 100644
--- a/server/server.go
+++ b/server/server.go
@@ -221,7 +221,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.DefaultTFVersion,
config.DefaultTFVersionFlag,
userConfig.TFDownloadURL,
- &terraform.DefaultDownloader{})
+ &terraform.DefaultDownloader{},
+ true)
// The flag.Lookup call is to detect if we're running in a unit test. If we
// are, then we don't error out because we don't have/want terraform
// installed on our CI system where the unit tests run.
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/atlantis.yaml b/server/testfixtures/test-repos/workspace-parallel-yaml/atlantis.yaml
new file mode 100644
index 000000000..bd073fab3
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/atlantis.yaml
@@ -0,0 +1,12 @@
+version: 3
+parallel_plan: false
+parallel_apply: true
+projects:
+ - dir: production
+ workspace: production
+ autoplan:
+ when_modified: ["**/*.tf*"]
+ - dir: staging
+ workspace: staging
+ autoplan:
+ when_modified: ["**/*.tf*"]
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-production.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-production.txt
new file mode 100644
index 000000000..e7baee5ee
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-production.txt
@@ -0,0 +1,21 @@
+Show Output
+
+```diff
+null_resource.this: Creating...
+null_resource.this: Creation complete after *s [id=*******************]
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+The state of your infrastructure has been saved to the path
+below. This state is required to modify and destroy your
+infrastructure, so keep it safe. To inspect the complete state
+use the `terraform show` command.
+
+State path: terraform.tfstate
+
+Outputs:
+
+workspace = production
+
+```
+
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-staging.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-staging.txt
new file mode 100644
index 000000000..1694d3741
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-apply-all-staging.txt
@@ -0,0 +1,21 @@
+Show Output
+
+```diff
+null_resource.this: Creating...
+null_resource.this: Creation complete after *s [id=*******************]
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+The state of your infrastructure has been saved to the path
+below. This state is required to modify and destroy your
+infrastructure, so keep it safe. To inspect the complete state
+use the `terraform show` command.
+
+State path: terraform.tfstate
+
+Outputs:
+
+workspace = staging
+
+```
+
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt
new file mode 100644
index 000000000..136c895af
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt
@@ -0,0 +1,25 @@
+Show Output
+
+```diff
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
++ create
+
+Terraform will perform the following actions:
+
+ # null_resource.this will be created
++ resource "null_resource" "this" {
+ + id = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d production -w production`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To **plan** this project again, comment:
+ * `atlantis plan -d production -w production`
+
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt
new file mode 100644
index 000000000..8ba8f0312
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt
@@ -0,0 +1,25 @@
+Show Output
+
+```diff
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
++ create
+
+Terraform will perform the following actions:
+
+ # null_resource.this will be created
++ resource "null_resource" "this" {
+ + id = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d staging -w staging`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To **plan** this project again, comment:
+ * `atlantis plan -d staging -w staging`
+
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-merge.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-merge.txt
new file mode 100644
index 000000000..8d9962e01
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-merge.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- dir: `production` workspace: `production`
+- dir: `staging` workspace: `staging`
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/production/main.tf b/server/testfixtures/test-repos/workspace-parallel-yaml/production/main.tf
new file mode 100644
index 000000000..f69db6a26
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/production/main.tf
@@ -0,0 +1,5 @@
+resource "null_resource" "this" {
+}
+output "workspace" {
+ value = "${terraform.workspace}"
+}
diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/staging/main.tf b/server/testfixtures/test-repos/workspace-parallel-yaml/staging/main.tf
new file mode 100644
index 000000000..f69db6a26
--- /dev/null
+++ b/server/testfixtures/test-repos/workspace-parallel-yaml/staging/main.tf
@@ -0,0 +1,5 @@
+resource "null_resource" "this" {
+}
+output "workspace" {
+ value = "${terraform.workspace}"
+}