Implement not deleting repo when same sha

This commit is contained in:
Luke Kysow
2018-06-18 20:54:58 +01:00
parent d9a47361e5
commit e7d5bf54fb
29 changed files with 448 additions and 71 deletions

View File

@@ -18,6 +18,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
@@ -48,7 +49,9 @@ type FileWorkspace struct {
}
// Clone git clones headRepo, checks out the branch and then returns the absolute
// path to the root of the cloned repo.
// path to the root of the cloned repo. If the repo already exists and is at
// the right commit it does nothing. This is to support running commands in
// multiple dirs of the same repo without deleting existing plans.
func (w *FileWorkspace) Clone(
log *logging.SimpleLogger,
baseRepo models.Repo,
@@ -57,11 +60,27 @@ func (w *FileWorkspace) Clone(
workspace string) (string, error) {
cloneDir := w.cloneDir(baseRepo, p, workspace)
// This is safe to do because we lock runs on repo/pull/workspace so no one else
// is using this workspace.
log.Info("cleaning clone directory %q", cloneDir)
if err := os.RemoveAll(cloneDir); err != nil {
return "", errors.Wrap(err, "deleting old workspace")
// If the directory already exists, check if it's at the right commit.
// If so, then we do nothing.
if _, err := os.Stat(cloneDir); err == nil {
revParseCmd := exec.Command("git", "rev-parse", "HEAD") // #nosec
revParseCmd.Dir = cloneDir
output, err := revParseCmd.CombinedOutput()
if err != nil {
return "", errors.Wrapf(err, "running git rev-parse HEAD: %s", string(output))
}
currCommit := strings.Trim(string(output), "\n")
if string(currCommit) == p.HeadCommit {
log.Debug("repo is at correct commit %q so will not re-clone", p.HeadCommit)
return cloneDir, nil
}
log.Debug("repo was already cloned but is not at correct commit, wanted %q got %q", p.HeadCommit, string(currCommit))
// It's okay to delete all plans now since they're out of date.
log.Info("cleaning clone directory %q", cloneDir)
if err := os.RemoveAll(cloneDir); err != nil {
return "", errors.Wrap(err, "deleting old workspace")
}
}
// Create the directory and parents if necessary.

View File

@@ -195,7 +195,7 @@ func (c *CommandHandler) updatePull(ctx *CommandContext, res CommandResponse) {
if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, res); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
comment := c.MarkdownRenderer.Render(res, ctx.Command.Name, ctx.Log.History.String(), ctx.Command.Verbose)
comment := c.MarkdownRenderer.Render(res, ctx.Command.Name, ctx.Log.History.String(), ctx.Command.Verbose, ctx.Command.Autoplan)
c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment) // nolint: errcheck
}

View File

@@ -58,7 +58,7 @@ type ProjectResultTmplData struct {
// Render formats the data into a markdown string.
// nolint: interfacer
func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log string, verbose bool) string {
func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log string, verbose bool, autoplan bool) string {
commandStr := strings.Title(cmdName.String())
common := CommonData{commandStr, verbose, log}
if res.Error != nil {
@@ -67,6 +67,9 @@ func (m *MarkdownRenderer) Render(res CommandResponse, cmdName CommandName, log
if res.Failure != "" {
return m.renderTemplate(failureWithLogTmpl, FailureData{res.Failure, common})
}
if len(res.ProjectResults) == 0 && autoplan {
return m.renderTemplate(autoplanNoProjectsWithLogTmpl, common)
}
return m.renderProjectResults(res.ProjectResults, common)
}
@@ -145,9 +148,12 @@ var errTmplText = "**{{.Command}} Error**\n" +
"```\n" +
"{{.Error}}\n" +
"```\n"
var autoplanNoProjectsTmplText = "Ran `plan` in 0 projects because Atlantis detected no Terraform changes or could not determine where to run `plan`.\n"
var errTmpl = template.Must(template.New("").Parse(errTmplText))
var errWithLogTmpl = template.Must(template.New("").Parse(errTmplText + logTmpl))
var failureTmplText = "**{{.Command}} Failed**: {{.Failure}}\n"
var failureTmpl = template.Must(template.New("").Parse(failureTmplText))
var failureWithLogTmpl = template.Must(template.New("").Parse(failureTmplText + logTmpl))
var autoplanNoProjectsTmpl = template.Must(template.New("").Parse(autoplanNoProjectsTmplText))
var autoplanNoProjectsWithLogTmpl = template.Must(template.New("").Parse(autoplanNoProjectsTmplText + logTmpl))
var logTmpl = "{{if .Verbose}}\n<details><summary>Log</summary>\n <p>\n\n```\n{{.Log}}```\n</p></details>{{end}}\n"

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
@@ -58,40 +59,40 @@ func TestGitHubWorkflow(t *testing.T) {
ExpMergeCommentFile string
CommentAndReplies []string
}{
//{
// Description: "simple",
// RepoDir: "simple",
// ModifiedFiles: []string{"main.tf"},
// ExpAutoplanCommentFile: "exp-output-autoplan.txt",
// CommentAndReplies: []string{
// "atlantis apply", "exp-output-apply.txt",
// },
// ExpMergeCommentFile: "exp-output-merge.txt",
//},
//{
// Description: "simple with comment -var",
// RepoDir: "simple",
// ModifiedFiles: []string{"main.tf"},
// ExpAutoplanCommentFile: "exp-output-autoplan.txt",
// CommentAndReplies: []string{
// "atlantis plan -- -var var=overridden", "exp-output-atlantis-plan.txt",
// "atlantis apply", "exp-output-apply-var.txt",
// },
// ExpMergeCommentFile: "exp-output-merge.txt",
//},
//{
// Description: "simple with workspaces",
// RepoDir: "simple",
// ModifiedFiles: []string{"main.tf"},
// ExpAutoplanCommentFile: "exp-output-autoplan.txt",
// CommentAndReplies: []string{
// "atlantis plan -- -var var=default_workspace", "exp-output-atlantis-plan.txt",
// "atlantis plan -w new_workspace -- -var var=new_workspace", "exp-output-atlantis-plan-new-workspace.txt",
// "atlantis apply", "exp-output-apply-var-default-workspace.txt",
// "atlantis apply -w new_workspace", "exp-output-apply-var-new-workspace.txt",
// },
// ExpMergeCommentFile: "exp-output-merge-workspaces.txt",
//},
{
Description: "simple",
RepoDir: "simple",
ModifiedFiles: []string{"main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan.txt",
CommentAndReplies: []string{
"atlantis apply", "exp-output-apply.txt",
},
ExpMergeCommentFile: "exp-output-merge.txt",
},
{
Description: "simple with comment -var",
RepoDir: "simple",
ModifiedFiles: []string{"main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan.txt",
CommentAndReplies: []string{
"atlantis plan -- -var var=overridden", "exp-output-atlantis-plan.txt",
"atlantis apply", "exp-output-apply-var.txt",
},
ExpMergeCommentFile: "exp-output-merge.txt",
},
{
Description: "simple with workspaces",
RepoDir: "simple",
ModifiedFiles: []string{"main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan.txt",
CommentAndReplies: []string{
"atlantis plan -- -var var=default_workspace", "exp-output-atlantis-plan.txt",
"atlantis plan -w new_workspace -- -var var=new_workspace", "exp-output-atlantis-plan-new-workspace.txt",
"atlantis apply", "exp-output-apply-var-default-workspace.txt",
"atlantis apply -w new_workspace", "exp-output-apply-var-new-workspace.txt",
},
ExpMergeCommentFile: "exp-output-merge-workspaces.txt",
},
{
Description: "simple with atlantis.yaml",
RepoDir: "simple-yaml",
@@ -103,18 +104,52 @@ func TestGitHubWorkflow(t *testing.T) {
},
ExpMergeCommentFile: "exp-output-merge.txt",
},
{
Description: "modules staging only",
RepoDir: "modules",
ModifiedFiles: []string{"staging/main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan-only-staging.txt",
CommentAndReplies: []string{
"atlantis apply -d staging", "exp-output-apply-staging.txt",
},
ExpMergeCommentFile: "exp-output-merge-only-staging.txt",
},
{
Description: "modules modules only",
RepoDir: "modules",
ModifiedFiles: []string{"modules/null/main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan-only-modules.txt",
CommentAndReplies: []string{
"atlantis plan -d staging", "exp-output-plan-staging.txt",
"atlantis plan -d production", "exp-output-plan-production.txt",
"atlantis apply -d staging", "exp-output-apply-staging.txt",
"atlantis apply -d production", "exp-output-apply-production.txt",
},
ExpMergeCommentFile: "exp-output-merge-all-dirs.txt",
},
{
Description: "modules-yaml",
RepoDir: "modules-yaml",
ModifiedFiles: []string{"modules/null/main.tf"},
ExpAutoplanCommentFile: "exp-output-autoplan.txt",
CommentAndReplies: []string{
"atlantis apply -d staging", "exp-output-apply-staging.txt",
"atlantis apply -d production", "exp-output-apply-production.txt",
},
ExpMergeCommentFile: "exp-output-merge-all-dirs.txt",
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t)
// Set the repo to be cloned through the testing backdoor.
repoDir, cleanup := initializeRepo(t, c.RepoDir)
repoDir, headSHA, cleanup := initializeRepo(t, c.RepoDir)
defer cleanup()
atlantisWorkspace.TestingOverrideCloneURL = fmt.Sprintf("file://%s", repoDir)
// Setup test dependencies.
w := httptest.NewRecorder()
When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(), nil)
When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
When(vcsClient.GetModifiedFiles(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil)
// First, send the open pull request event and trigger an autoplan.
@@ -122,9 +157,7 @@ func TestGitHubWorkflow(t *testing.T) {
ctrl.Post(w, pullOpenedReq)
responseContains(t, w, 200, "Processing...")
_, _, autoplanComment := vcsClient.VerifyWasCalledOnce().CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
exp, err := ioutil.ReadFile(filepath.Join(repoDir, c.ExpAutoplanCommentFile))
Ok(t, err)
Equals(t, string(exp), autoplanComment)
assertCommentEquals(t, c.ExpAutoplanCommentFile, autoplanComment, c.RepoDir)
// Now send any other comments.
for i := 0; i < len(c.CommentAndReplies); i += 2 {
@@ -136,16 +169,7 @@ func TestGitHubWorkflow(t *testing.T) {
ctrl.Post(w, commentReq)
responseContains(t, w, 200, "Processing...")
_, _, atlantisComment := vcsClient.VerifyWasCalled(Times((i/2)+2)).CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
exp, err = ioutil.ReadFile(filepath.Join(repoDir, expOutputFile))
Ok(t, err)
// Replace all 'ID: 1111818181' strings with * so we can do a comparison.
idRegex := regexp.MustCompile(`\(ID: [0-9]+\)`)
atlantisComment = idRegex.ReplaceAllString(atlantisComment, "(ID: ******************)")
if string(exp) != atlantisComment {
t.Logf("comment: %s", comment)
}
Equals(t, string(exp), atlantisComment)
assertCommentEquals(t, expOutputFile, atlantisComment, c.RepoDir)
}
// Finally, send the pull request merged event.
@@ -155,9 +179,7 @@ func TestGitHubWorkflow(t *testing.T) {
responseContains(t, w, 200, "Pull request cleaned successfully")
numPrevComments := (len(c.CommentAndReplies) / 2) + 1
_, _, pullClosedComment := vcsClient.VerifyWasCalled(Times(numPrevComments+1)).CreateComment(AnyRepo(), AnyInt(), AnyString()).GetCapturedArguments()
exp, err = ioutil.ReadFile(filepath.Join(repoDir, c.ExpMergeCommentFile))
Ok(t, err)
Equals(t, string(exp), pullClosedComment)
assertCommentEquals(t, c.ExpMergeCommentFile, pullClosedComment, c.RepoDir)
})
}
}
@@ -166,8 +188,6 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy,
allowForkPRs := false
dataDir, cleanup := TempDir(t)
defer cleanup()
testRepoDir, err := filepath.Abs("testfixtures/test-repos/simple")
Ok(t, err)
// Mocks.
e2eVCSClient := vcsmocks.NewMockClientProxy()
@@ -199,7 +219,7 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy,
}
atlantisWorkspace := &events.FileWorkspace{
DataDir: dataDir,
TestingOverrideCloneURL: testRepoDir,
TestingOverrideCloneURL: "override-me",
}
defaultTFVersion := terraformClient.Version()
@@ -318,7 +338,11 @@ func GitHubPullRequestClosedEvent(t *testing.T) *http.Request {
return req
}
func GitHubPullRequestParsed() *github.PullRequest {
func GitHubPullRequestParsed(headSHA string) *github.PullRequest {
// headSHA can't be empty so default if not set.
if headSHA == "" {
headSHA = "13940d121be73f656e2132c6d7b4c8e87878ac8d"
}
return &github.PullRequest{
Number: github.Int(1),
State: github.String("open"),
@@ -328,7 +352,7 @@ func GitHubPullRequestParsed() *github.PullRequest {
FullName: github.String("runatlantis/atlantis-tests"),
CloneURL: github.String("/runatlantis/atlantis-tests.git"),
},
SHA: github.String("sha"),
SHA: github.String(headSHA),
Ref: github.String("branch"),
},
Base: &github.PullRequestBranch{
@@ -343,15 +367,21 @@ func GitHubPullRequestParsed() *github.PullRequest {
}
}
// absRepoPath returns the absolute path to the test repo under dir repoDir.
func absRepoPath(t *testing.T, repoDir string) string {
path, err := filepath.Abs(filepath.Join("testfixtures", "test-repos", repoDir))
Ok(t, err)
return path
}
// initializeRepo copies the repo data from testfixtures and initializes a new
// git repo in a temp directory. It returns that directory and a function
// to run in a defer that will delete the dir.
// The purpose of this function is to create a real git repository with a branch
// called 'branch' from the files under repoDir. This is so we can check in
// those files normally without needing a .git directory.
func initializeRepo(t *testing.T, repoDir string) (string, func()) {
originRepo, err := filepath.Abs(filepath.Join("testfixtures", "test-repos", repoDir))
Ok(t, err)
func initializeRepo(t *testing.T, repoDir string) (string, string, func()) {
originRepo := absRepoPath(t, repoDir)
// Copy the files to the temp dir.
destDir, cleanup := TempDir(t)
@@ -365,13 +395,37 @@ func initializeRepo(t *testing.T, repoDir string) (string, func()) {
runCmd(t, destDir, "git", "checkout", "-b", "branch")
runCmd(t, destDir, "git", "add", ".")
runCmd(t, destDir, "git", "commit", "-am", "branch commit")
headSHA := runCmd(t, destDir, "git", "rev-parse", "HEAD")
headSHA = strings.Trim(headSHA, "\n")
return destDir, cleanup
return destDir, headSHA, cleanup
}
func runCmd(t *testing.T, dir string, name string, args ...string) {
func runCmd(t *testing.T, dir string, name string, args ...string) string {
cpCmd := exec.Command(name, args...)
cpCmd.Dir = dir
cpOut, err := cpCmd.CombinedOutput()
Assert(t, err == nil, "err running %q: %s", strings.Join(append([]string{name}, args...), " "), cpOut)
return string(cpOut)
}
func assertCommentEquals(t *testing.T, expFile string, act string, repoDir string) {
t.Helper()
exp, err := ioutil.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))
Ok(t, err)
// Replace all 'ID: 1111818181' strings with * so we can do a comparison.
idRegex := regexp.MustCompile(`\(ID: [0-9]+\)`)
act = idRegex.ReplaceAllString(act, "(ID: ******************)")
if string(exp) != act {
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)
}
}

View File

@@ -0,0 +1,8 @@
version: 2
projects:
- dir: staging
autoplan:
when_modified: ["**/*.tf", "../modules/null/*"]
- dir: production
autoplan:
when_modified: ["**/*.tf", "../modules/null/*"]

View File

@@ -0,0 +1,13 @@
Ran Apply in dir: `production` workspace: `default`
```diff
module.null.null_resource.this: Creating...
module.null.null_resource.this: Creation complete after 0s (ID: ******************)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
var = production
```

View File

@@ -0,0 +1,13 @@
Ran Apply in dir: `staging` workspace: `default`
```diff
module.null.null_resource.this: Creating...
module.null.null_resource.this: Creation complete after 0s (ID: ******************)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
var = staging
```

View File

@@ -0,0 +1,51 @@
Ran Plan for 2 projects:
1. workspace: `default` path: `staging`
1. workspace: `default` path: `production`
### 1. workspace: `default` path: `staging`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).
---
### 2. workspace: `default` path: `production`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).
---

View File

@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/production` workspace: `default`
- path: `runatlantis/atlantis-tests/staging` workspace: `default`

View File

@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/staging` workspace: `default`

View File

@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/staging` workspace: `default`
- path: `runatlantis/atlantis-tests/.` workspace: `default`

View File

@@ -0,0 +1,23 @@
Ran Plan in dir: `production` workspace: `default`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).

View File

@@ -0,0 +1,23 @@
Ran Plan in dir: `staging` workspace: `default`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).

View File

@@ -0,0 +1,10 @@
variable "var" {}
resource "null_resource" "this" {
}
output "var" {
value = "${var.var}"
}
output "workspace" {
value = "${terraform.workspace}"
}

View File

@@ -0,0 +1,7 @@
module "null" {
source = "../modules/null"
var = "production"
}
output "var" {
value = "${module.null.var}"
}

View File

@@ -0,0 +1,7 @@
module "null" {
source = "../modules/null"
var = "staging"
}
output "var" {
value = "${module.null.var}"
}

View File

@@ -0,0 +1,13 @@
Ran Apply in dir: `production` workspace: `default`
```diff
module.null.null_resource.this: Creating...
module.null.null_resource.this: Creation complete after 0s (ID: ******************)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
var = production
```

View File

@@ -0,0 +1,13 @@
Ran Apply in dir: `staging` workspace: `default`
```diff
module.null.null_resource.this: Creating...
module.null.null_resource.this: Creation complete after 0s (ID: ******************)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
var = staging
```

View File

@@ -0,0 +1,2 @@
Ran `plan` in 0 projects because Atlantis detected no Terraform changes or could not determine where to run `plan`.

View File

@@ -0,0 +1,23 @@
Ran Plan in dir: `staging` workspace: `default`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).

View File

@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/production` workspace: `default`
- path: `runatlantis/atlantis-tests/staging` workspace: `default`

View File

@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/staging` workspace: `default`

View File

@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/staging` workspace: `default`
- path: `runatlantis/atlantis-tests/.` workspace: `default`

View File

@@ -0,0 +1,23 @@
Ran Plan in dir: `production` workspace: `default`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).

View File

@@ -0,0 +1,23 @@
Ran Plan in dir: `staging` workspace: `default`
```diff
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
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:
+ module.null.null_resource.this
id: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
```
* To **discard** this plan click [here](lock-url).

View File

@@ -0,0 +1,10 @@
variable "var" {}
resource "null_resource" "this" {
}
output "var" {
value = "${var.var}"
}
output "workspace" {
value = "${terraform.workspace}"
}

View File

@@ -0,0 +1,7 @@
module "null" {
source = "../modules/null"
var = "production"
}
output "var" {
value = "${module.null.var}"
}

View File

@@ -0,0 +1,7 @@
module "null" {
source = "../modules/null"
var = "staging"
}
output "var" {
value = "${module.null.var}"
}

View File

@@ -1,3 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- path: `runatlantis/atlantis-tests/.` workspaces: `default`, `new_workspace`
- path: `runatlantis/atlantis-tests/.` workspaces: `default`, `new_workspace`