mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-05 06:18:54 +00:00
feat: streaming terraform logs in real-time (#1937)
This commit is contained in:
2
go.mod
2
go.mod
@@ -39,7 +39,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-getter v1.5.10
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
|
||||
|
||||
@@ -128,19 +128,19 @@ workflows:
|
||||
myworkflow:
|
||||
plan:
|
||||
steps:
|
||||
- run: terraform init -input=false -no-color
|
||||
- run: terraform init -input=false
|
||||
|
||||
# If you're using workspaces you need to select the workspace using the
|
||||
# $WORKSPACE environment variable.
|
||||
- run: terraform workspace select -no-color $WORKSPACE
|
||||
- run: terraform workspace select $WORKSPACE
|
||||
|
||||
# You MUST output the plan using -out $PLANFILE because Atlantis expects
|
||||
# plans to be in a specific location.
|
||||
- run: terraform plan -input=false -refresh -no-color -out $PLANFILE
|
||||
- run: terraform plan -input=false -refresh -out $PLANFILE
|
||||
apply:
|
||||
steps:
|
||||
# Again, you must use the $PLANFILE environment variable.
|
||||
- run: terraform apply -no-color $PLANFILE
|
||||
- run: terraform apply $PLANFILE
|
||||
```
|
||||
|
||||
### Terragrunt
|
||||
@@ -176,14 +176,14 @@ workflows:
|
||||
- env:
|
||||
name: TERRAGRUNT_TFPATH
|
||||
command: 'echo "terraform${ATLANTIS_TERRAFORM_VERSION}"'
|
||||
- run: terragrunt plan -no-color -out=$PLANFILE
|
||||
- run: terragrunt show -no-color -json $PLANFILE > $SHOWFILE
|
||||
- run: terragrunt plan -out=$PLANFILE
|
||||
- run: terragrunt show -json $PLANFILE > $SHOWFILE
|
||||
apply:
|
||||
steps:
|
||||
- env:
|
||||
name: TERRAGRUNT_TFPATH
|
||||
command: 'echo "terraform${ATLANTIS_TERRAFORM_VERSION}"'
|
||||
- run: terragrunt apply -no-color $PLANFILE
|
||||
- run: terragrunt apply $PLANFILE
|
||||
```
|
||||
|
||||
If using the repo's `atlantis.yaml` file you would use the following config:
|
||||
@@ -201,13 +201,13 @@ workflows:
|
||||
- env:
|
||||
name: TERRAGRUNT_TFPATH
|
||||
command: 'echo "terraform${ATLANTIS_TERRAFORM_VERSION}"'
|
||||
- run: terragrunt plan -no-color -out $PLANFILE
|
||||
- run: terragrunt plan -out $PLANFILE
|
||||
apply:
|
||||
steps:
|
||||
- env:
|
||||
name: TERRAGRUNT_TFPATH
|
||||
command: 'echo "terraform${ATLANTIS_TERRAFORM_VERSION}"'
|
||||
- run: terragrunt apply -no-color $PLANFILE
|
||||
- run: terragrunt apply $PLANFILE
|
||||
```
|
||||
|
||||
**NOTE:** If using the repo's `atlantis.yaml` file, you will need to specify each directory that is a Terragrunt project.
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
handlermocks "github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
@@ -816,6 +817,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
|
||||
e2eStatusUpdater := &events.DefaultCommitStatusUpdater{Client: e2eVCSClient, TitleBuilder: vcs.StatusTitleBuilder{TitlePrefix: "atlantis"}}
|
||||
e2eGithubGetter := mocks.NewMockGithubPullGetter()
|
||||
e2eGitlabGetter := mocks.NewMockGitlabMergeRequestGetter()
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
// Real dependencies.
|
||||
logger := logging.NewNoopLogger(t)
|
||||
@@ -830,7 +832,7 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
|
||||
GithubUser: "github-user",
|
||||
GitlabUser: "gitlab-user",
|
||||
}
|
||||
terraformClient, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", &NoopTFDownloader{}, false)
|
||||
terraformClient, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", &NoopTFDownloader{}, false, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
boltdb, err := db.New(dataDir)
|
||||
Ok(t, err)
|
||||
@@ -1057,10 +1059,12 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
|
||||
TestingMode: true,
|
||||
CommandRunner: commandRunner,
|
||||
PullCleaner: &events.PullClosedExecutor{
|
||||
Locker: lockingClient,
|
||||
VCSClient: e2eVCSClient,
|
||||
WorkingDir: workingDir,
|
||||
DB: boltdb,
|
||||
Locker: lockingClient,
|
||||
VCSClient: e2eVCSClient,
|
||||
WorkingDir: workingDir,
|
||||
DB: boltdb,
|
||||
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
||||
LogStreamResourceCleaner: projectCmdOutputHandler,
|
||||
},
|
||||
Logger: logger,
|
||||
Parser: eventParser,
|
||||
|
||||
@@ -6,5 +6,6 @@ null_resource.automerge[0]: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
Ran Apply for dir: `dir1` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.automerge[0]: Creating...
|
||||
null_resource.automerge[0]: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
```
|
||||
|
||||
@@ -6,5 +6,6 @@ null_resource.automerge[0]: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
Ran Apply for dir: `dir2` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.automerge[0]: Creating...
|
||||
null_resource.automerge[0]: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@ Terraform will perform the following actions:
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
@@ -50,6 +51,7 @@ Terraform will perform the following actions:
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `dir1` workspace: `default`
|
||||
1. dir: `dir2` workspace: `default`
|
||||
|
||||
### 1. dir: `dir1` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.automerge[0] will be created
|
||||
+ resource "null_resource" "automerge" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d dir1`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d dir1`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `dir2` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.automerge[0] will be created
|
||||
+ resource "null_resource" "automerge" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d dir2`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d dir2`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `production` workspace: `default`
|
||||
|
||||
```diff
|
||||
module.null.null_resource.this: Creating...
|
||||
module.null.null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "production"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `staging` workspace: `default`
|
||||
|
||||
```diff
|
||||
module.null.null_resource.this: Creating...
|
||||
module.null.null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "staging"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `staging` workspace: `default`
|
||||
1. dir: `production` workspace: `default`
|
||||
|
||||
### 1. dir: `staging` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# module.null.null_resource.this will be created
|
||||
+ resource "null_resource" "this" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d 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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `production` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# module.null.null_resource.this will be created
|
||||
+ resource "null_resource" "this" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "production"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d 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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `production` workspace: `default`
|
||||
|
||||
```diff
|
||||
module.null.null_resource.this: Creating...
|
||||
module.null.null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "production"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `staging` workspace: `default`
|
||||
|
||||
```diff
|
||||
module.null.null_resource.this: Creating...
|
||||
module.null.null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "staging"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
Ran Plan for dir: `staging` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# module.null.null_resource.this will be created
|
||||
+ resource "null_resource" "this" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d 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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,37 +0,0 @@
|
||||
Ran Plan for dir: `production` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# module.null.null_resource.this will be created
|
||||
+ resource "null_resource" "this" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "production"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d 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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,37 +0,0 @@
|
||||
Ran Plan for dir: `staging` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# module.null.null_resource.this will be created
|
||||
+ resource "null_resource" "this" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d 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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `staging`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
1. dir: `.` workspace: `staging`
|
||||
|
||||
### 1. dir: `.` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
preinit custom
|
||||
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "default"
|
||||
|
||||
postplan custom
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d .`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d .`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `.` workspace: `staging`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
preinit staging
|
||||
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -w staging`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -w staging`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,43 +0,0 @@
|
||||
Ran Apply for 2 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
1. dir: `.` workspace: `staging`
|
||||
|
||||
### 1. dir: `.` workspace: `default`
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "fromconfig"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
### 2. dir: `.` workspace: `staging`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
preapply
|
||||
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "fromfile"
|
||||
workspace = "staging"
|
||||
|
||||
postapply
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "fromconfig"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `staging`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
preapply
|
||||
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "fromfile"
|
||||
workspace = "staging"
|
||||
|
||||
postapply
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
1. dir: `.` workspace: `staging`
|
||||
|
||||
### 1. dir: `.` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
preinit
|
||||
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "fromconfig"
|
||||
+ workspace = "default"
|
||||
|
||||
postplan
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d .`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d .`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `.` workspace: `staging`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "fromfile"
|
||||
+ workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -w staging`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -w staging`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,50 +0,0 @@
|
||||
Ran Apply for 2 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
1. dir: `.` workspace: `new_workspace`
|
||||
|
||||
### 1. dir: `.` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "default_workspace"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
### 2. dir: `.` workspace: `new_workspace`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "new_workspace"
|
||||
workspace = "new_workspace"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "default_workspace"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `new_workspace`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "new_workspace"
|
||||
workspace = "new_workspace"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "overridden"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "default"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
Ran Plan for dir: `.` workspace: `new_workspace`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple2 will be created
|
||||
+ resource "null_resource" "simple2" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple3 will be created
|
||||
+ resource "null_resource" "simple3" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "new_workspace"
|
||||
+ workspace = "new_workspace"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -w new_workspace`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -w new_workspace -- -var var=new_workspace`
|
||||
</details>
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,48 +0,0 @@
|
||||
Ran Plan for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple2 will be created
|
||||
+ resource "null_resource" "simple2" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple3 will be created
|
||||
+ resource "null_resource" "simple3" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "overridden"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d .`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d . -- -var var=overridden`
|
||||
</details>
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,48 +0,0 @@
|
||||
Ran Plan for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple2 will be created
|
||||
+ resource "null_resource" "simple2" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple3 will be created
|
||||
+ resource "null_resource" "simple3" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "default_workspace"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d .`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d . -- -var var=default_workspace`
|
||||
</details>
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,48 +0,0 @@
|
||||
Ran Plan for dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple2 will be created
|
||||
+ resource "null_resource" "simple2" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
# null_resource.simple3 will be created
|
||||
+ resource "null_resource" "simple3" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "default"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -d .`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -d .`
|
||||
</details>
|
||||
Plan: 3 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,15 +0,0 @@
|
||||
Ran Apply for project: `default` dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "default"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Ran Apply for project: `staging` dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "staging"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
Ran Plan for project: `default` dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "default"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -p default`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -p default`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,38 +0,0 @@
|
||||
Ran Plan for project: `staging` dir: `.` workspace: `default`
|
||||
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "staging"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -p staging`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -p staging`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,15 +0,0 @@
|
||||
Ran Apply for project: `default` dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "default"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Ran Apply for project: `staging` dir: `.` workspace: `default`
|
||||
|
||||
```diff
|
||||
null_resource.simple:
|
||||
null_resource.simple:
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
var = "staging"
|
||||
workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. project: `default` dir: `.` workspace: `default`
|
||||
1. project: `staging` dir: `.` workspace: `default`
|
||||
|
||||
### 1. project: `default` dir: `.` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "default"
|
||||
+ workspace = "default"
|
||||
|
||||
workspace=default
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -p default`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -p default`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. project: `staging` dir: `.` workspace: `default`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. Resource actions are indicated with the following symbols:
|
||||
+ create
|
||||
|
||||
Terraform will perform the following actions:
|
||||
|
||||
# null_resource.simple[0] will be created
|
||||
+ resource "null_resource" "simple" {
|
||||
+ id = (known after apply)
|
||||
}
|
||||
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
Changes to Outputs:
|
||||
+ var = "staging"
|
||||
+ workspace = "default"
|
||||
|
||||
```
|
||||
|
||||
* :arrow_forward: To **apply** this plan, comment:
|
||||
* `atlantis apply -p staging`
|
||||
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
|
||||
* :repeat: To **plan** this project again, comment:
|
||||
* `atlantis plan -p staging`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,34 +0,0 @@
|
||||
Ran Apply for 2 projects:
|
||||
|
||||
1. dir: `production` workspace: `production`
|
||||
1. dir: `staging` workspace: `staging`
|
||||
|
||||
### 1. dir: `production` workspace: `production`
|
||||
```diff
|
||||
null_resource.this: Creating...
|
||||
null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "production"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
### 2. dir: `staging` workspace: `staging`
|
||||
```diff
|
||||
null_resource.this: Creating...
|
||||
null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
Ran Apply for 2 projects:
|
||||
|
||||
1. dir: `production` workspace: `production`
|
||||
1. dir: `staging` workspace: `staging`
|
||||
|
||||
### 1. dir: `production` workspace: `production`
|
||||
```diff
|
||||
null_resource.this: Creating...
|
||||
null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "production"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
### 2. dir: `staging` workspace: `staging`
|
||||
```diff
|
||||
null_resource.this: Creating...
|
||||
null_resource.this: Creation complete after *s [id=*******************]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `production` workspace: `production`
|
||||
1. dir: `staging` workspace: `staging`
|
||||
|
||||
### 1. dir: `production` workspace: `production`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. 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.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "production"
|
||||
|
||||
```
|
||||
|
||||
* :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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `staging` workspace: `staging`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. 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.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
@@ -1,73 +0,0 @@
|
||||
Ran Plan for 2 projects:
|
||||
|
||||
1. dir: `production` workspace: `production`
|
||||
1. dir: `staging` workspace: `staging`
|
||||
|
||||
### 1. dir: `production` workspace: `production`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. 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.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "production"
|
||||
|
||||
```
|
||||
|
||||
* :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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
### 2. dir: `staging` workspace: `staging`
|
||||
<details><summary>Show Output</summary>
|
||||
|
||||
```diff
|
||||
|
||||
Terraform used the selected providers to generate the following execution
|
||||
plan. 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.
|
||||
|
||||
Changes to Outputs:
|
||||
+ workspace = "staging"
|
||||
|
||||
```
|
||||
|
||||
* :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`
|
||||
</details>
|
||||
Plan: 1 to add, 0 to change, 0 to destroy.
|
||||
|
||||
---
|
||||
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
|
||||
* `atlantis apply`
|
||||
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
|
||||
* `atlantis unlock`
|
||||
144
server/controllers/jobs_controller.go
Normal file
144
server/controllers/jobs_controller.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/controllers/templates"
|
||||
"github.com/runatlantis/atlantis/server/controllers/websocket"
|
||||
"github.com/runatlantis/atlantis/server/core/db"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
type JobsController struct {
|
||||
AtlantisVersion string
|
||||
AtlantisURL *url.URL
|
||||
Logger logging.SimpleLogging
|
||||
ProjectJobsTemplate templates.TemplateWriter
|
||||
ProjectJobsErrorTemplate templates.TemplateWriter
|
||||
Db *db.BoltDB
|
||||
WsMux *websocket.Multiplexor
|
||||
}
|
||||
|
||||
type ProjectInfoKeyGenerator struct{}
|
||||
|
||||
func (g ProjectInfoKeyGenerator) Generate(r *http.Request) (string, error) {
|
||||
projectInfo, err := newProjectInfo(r)
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "creating project info")
|
||||
}
|
||||
|
||||
return projectInfo.String(), nil
|
||||
}
|
||||
|
||||
type pullInfo struct {
|
||||
org string
|
||||
repo string
|
||||
pull int
|
||||
}
|
||||
|
||||
func (p *pullInfo) String() string {
|
||||
return fmt.Sprintf("%s/%s/%d", p.org, p.repo, p.pull)
|
||||
}
|
||||
|
||||
type projectInfo struct {
|
||||
projectName string
|
||||
workspace string
|
||||
pullInfo
|
||||
}
|
||||
|
||||
func (p *projectInfo) String() string {
|
||||
return fmt.Sprintf("%s/%s/%d/%s/%s", p.org, p.repo, p.pull, p.projectName, p.workspace)
|
||||
}
|
||||
|
||||
func newPullInfo(r *http.Request) (*pullInfo, error) {
|
||||
org, ok := mux.Vars(r)["org"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Internal error: no org in route")
|
||||
}
|
||||
repo, ok := mux.Vars(r)["repo"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Internal error: no repo in route")
|
||||
}
|
||||
pull, ok := mux.Vars(r)["pull"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Internal error: no pull in route")
|
||||
}
|
||||
pullNum, err := strconv.Atoi(pull)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pullInfo{
|
||||
org: org,
|
||||
repo: repo,
|
||||
pull: pullNum,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Gets the PR information from the HTTP request params
|
||||
func newProjectInfo(r *http.Request) (*projectInfo, error) {
|
||||
pullInfo, err := newPullInfo(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, ok := mux.Vars(r)["project"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Internal error: no project in route")
|
||||
}
|
||||
|
||||
workspace, ok := mux.Vars(r)["workspace"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Internal error: no workspace in route")
|
||||
}
|
||||
|
||||
return &projectInfo{
|
||||
pullInfo: *pullInfo,
|
||||
projectName: project,
|
||||
workspace: workspace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (j *JobsController) GetProjectJobs(w http.ResponseWriter, r *http.Request) {
|
||||
projectInfo, err := newProjectInfo(r)
|
||||
if err != nil {
|
||||
j.respond(w, logging.Error, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
viewData := templates.ProjectJobData{
|
||||
AtlantisVersion: j.AtlantisVersion,
|
||||
ProjectPath: projectInfo.String(),
|
||||
CleanedBasePath: j.AtlantisURL.Path,
|
||||
ClearMsg: models.LogStreamingClearMsg,
|
||||
}
|
||||
|
||||
err = j.ProjectJobsTemplate.Execute(w, viewData)
|
||||
if err != nil {
|
||||
j.Logger.Err(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobsController) GetProjectJobsWS(w http.ResponseWriter, r *http.Request) {
|
||||
err := j.WsMux.Handle(w, r)
|
||||
|
||||
if err != nil {
|
||||
j.respond(w, logging.Error, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JobsController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...interface{}) {
|
||||
response := fmt.Sprintf(format, args...)
|
||||
j.Logger.Log(lvl, response)
|
||||
w.WriteHeader(responseCode)
|
||||
fmt.Fprintln(w, response)
|
||||
}
|
||||
@@ -352,6 +352,168 @@ v{{ .AtlantisVersion }}
|
||||
</html>
|
||||
`))
|
||||
|
||||
// ProjectJobData holds the data needed to stream the current PR information
|
||||
type ProjectJobData struct {
|
||||
AtlantisVersion string
|
||||
ProjectPath string
|
||||
CleanedBasePath string
|
||||
ClearMsg string
|
||||
}
|
||||
|
||||
var ProjectJobsTemplate = template.Must(template.New("blank.html.tmpl").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>atlantis</title>
|
||||
<meta name="description" content>
|
||||
<meta name="author" content>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css"
|
||||
integrity="sha256-uTIrmf95e6IHlacC0wpDaPS58eWF314UC7OgdrD6AdU="
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
|
||||
<style>
|
||||
#terminal {
|
||||
position: fixed;
|
||||
top: 200px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
overflow: auto;
|
||||
border: 5px solid white;
|
||||
}
|
||||
|
||||
.terminal.xterm {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="header">
|
||||
<a title="atlantis" href="/"><img class="hero" src="/static/images/atlantis-icon_512.png"/></a>
|
||||
<p class="title-heading">atlantis</p>
|
||||
<p class="title-heading"><strong></strong></p>
|
||||
</section>
|
||||
<div class="spacer"></div>
|
||||
<br>
|
||||
<section>
|
||||
<div id="terminal"></div>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/xterm@4.9.0/lib/xterm.js"></script>
|
||||
<script src="https://unpkg.com/xterm-addon-attach@0.6.0/lib/xterm-addon-attach.js"></script>
|
||||
<script src="https://unpkg.com/xterm-addon-fit@0.4.0/lib/xterm-addon-fit.js"></script>
|
||||
|
||||
<script>
|
||||
var term = new Terminal({scrollback: 15000});
|
||||
var socket = new WebSocket(
|
||||
(document.location.protocol === "http:" ? "ws://" : "wss://") +
|
||||
document.location.host +
|
||||
document.location.pathname +
|
||||
"/ws");
|
||||
socket.onmessage = function(event) {
|
||||
var msg = String.fromCharCode.apply(null, new Uint8Array(event.data))
|
||||
if (msg.trim() === "-----Starting New Process-----") {
|
||||
term.clear()
|
||||
return
|
||||
}
|
||||
}
|
||||
var attachAddon = new AttachAddon.AttachAddon(socket);
|
||||
var fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(attachAddon);
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById("terminal"));
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", () => fitAddon.fit());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
type ProjectJobsError struct {
|
||||
AtlantisVersion string
|
||||
ProjectPath string
|
||||
CleanedBasePath string
|
||||
}
|
||||
|
||||
var ProjectJobsErrorTemplate = template.Must(template.New("blank.html.tmpl").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>atlantis</title>
|
||||
<meta name="description" content>
|
||||
<meta name="author" content>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.css"
|
||||
integrity="sha256-uTIrmf95e6IHlacC0wpDaPS58eWF314UC7OgdrD6AdU="
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link rel="stylesheet" href="/static/css/normalize.css">
|
||||
<link rel="stylesheet" href="/static/css/skeleton.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
<link rel="icon" type="image/png" href="/static/images/atlantis-icon.png">
|
||||
<style>
|
||||
#terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="header">
|
||||
<a title="atlantis" href="/"><img class="hero" src="/static/images/atlantis-icon_512.png"/></a>
|
||||
<p class="title-heading">atlantis</p>
|
||||
<p class="title-heading"><strong></strong></p>
|
||||
</section>
|
||||
<div class="spacer"></div>
|
||||
<br>
|
||||
<section>
|
||||
<div id="terminal"></div>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/xterm@4.9.0/lib/xterm.js"></script>
|
||||
<script src="https://unpkg.com/xterm-addon-attach@0.6.0/lib/xterm-addon-attach.js"></script>
|
||||
<script src="https://unpkg.com/xterm-addon-fit@0.4.0/lib/xterm-addon-fit.js"></script>
|
||||
|
||||
<script>
|
||||
var term = new Terminal();
|
||||
var socket = new WebSocket(
|
||||
(document.location.protocol === "http:" ? "ws://" : "wss://") +
|
||||
document.location.host +
|
||||
document.location.pathname +
|
||||
"/ws");
|
||||
var attachAddon = new AttachAddon.AttachAddon(socket);
|
||||
var fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(attachAddon);
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(document.getElementById("terminal"));
|
||||
term.write('Project Does Not Exist in PR')
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", () => fitAddon.fit());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
// GithubSetupData holds the data for rendering the github app setup page
|
||||
type GithubSetupData struct {
|
||||
Target string
|
||||
|
||||
63
server/controllers/websocket/mux.go
Normal file
63
server/controllers/websocket/mux.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
// PartitionKeyGenerator generates partition keys for the multiplexor
|
||||
type PartitionKeyGenerator interface {
|
||||
Generate(r *http.Request) (string, error)
|
||||
}
|
||||
|
||||
// PartitionRegistry is the registry holding each partition
|
||||
// and is responsible for registering/deregistering new buffers
|
||||
type PartitionRegistry interface {
|
||||
Register(key string, buffer chan string)
|
||||
Deregister(key string, buffer chan string)
|
||||
}
|
||||
|
||||
// Multiplexor is responsible for handling the data transfer between the storage layer
|
||||
// and the registry. Note this is still a WIP as right now the registry is assumed to handle
|
||||
// everything.
|
||||
type Multiplexor struct {
|
||||
writer *Writer
|
||||
keyGenerator PartitionKeyGenerator
|
||||
registry PartitionRegistry
|
||||
}
|
||||
|
||||
func NewMultiplexor(log logging.SimpleLogging, keyGenerator PartitionKeyGenerator, registry PartitionRegistry) *Multiplexor {
|
||||
upgrader := websocket.Upgrader{}
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
return &Multiplexor{
|
||||
writer: &Writer{
|
||||
upgrader: upgrader,
|
||||
log: log,
|
||||
},
|
||||
keyGenerator: keyGenerator,
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle should be called for a given websocket request. It blocks
|
||||
// while writing to the websocket until the buffer is closed.
|
||||
func (m *Multiplexor) Handle(w http.ResponseWriter, r *http.Request) error {
|
||||
key, err := m.keyGenerator.Generate(r)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "generating partition key")
|
||||
}
|
||||
|
||||
// Buffer size set to 1000 to ensure messages get queued.
|
||||
// TODO: make buffer size configurable
|
||||
buffer := make(chan string, 1000)
|
||||
|
||||
// spinning up a goroutine for this since we are attempting to block on the read side.
|
||||
go m.registry.Register(key, buffer)
|
||||
defer m.registry.Deregister(key, buffer)
|
||||
|
||||
return errors.Wrapf(m.writer.Write(w, r, buffer), "writing to ws %s", key)
|
||||
}
|
||||
68
server/controllers/websocket/writer.go
Normal file
68
server/controllers/websocket/writer.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
func NewWriter(log logging.SimpleLogging) *Writer {
|
||||
upgrader := websocket.Upgrader{}
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
return &Writer{
|
||||
upgrader: upgrader,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
upgrader websocket.Upgrader
|
||||
log logging.SimpleLogging
|
||||
}
|
||||
|
||||
func (w *Writer) Write(rw http.ResponseWriter, r *http.Request, input chan string) error {
|
||||
conn, err := w.upgrader.Upgrade(rw, r, nil)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "upgrading websocket connection")
|
||||
}
|
||||
|
||||
conn.SetCloseHandler(func(code int, text string) error {
|
||||
// Close the channnel after websocket connection closed.
|
||||
// Will gracefully exit the ProjectCommandOutputHandler.Register() call and cleanup.
|
||||
// is it good practice to close at the receiver? Probably not, we should figure out a better
|
||||
// way to handle this case
|
||||
close(input)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Add a reader goroutine to listen for socket.close() events.
|
||||
go w.setReadHandler(conn)
|
||||
|
||||
// block on reading our input channel
|
||||
for msg := range input {
|
||||
if err := conn.WriteMessage(websocket.BinaryMessage, []byte("\r"+msg+"\n")); err != nil {
|
||||
w.log.Warn("Failed to write ws message: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Writer) setReadHandler(c *websocket.Conn) {
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
// CloseGoingAway (1001) when a browser tab is closed.
|
||||
// Expected behaviour since we have a CloseHandler(), log warning if not a CloseGoingAway
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
|
||||
w.log.Warn("Failed to read WS message: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,11 +22,6 @@ type ApplyStepRunner struct {
|
||||
}
|
||||
|
||||
func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) {
|
||||
tfVersion := a.DefaultTFVersion
|
||||
if ctx.TerraformVersion != nil {
|
||||
tfVersion = ctx.TerraformVersion
|
||||
}
|
||||
|
||||
if a.hasTargetFlag(ctx, extraArgs) {
|
||||
return "", errors.New("cannot run apply with -target because we are applying an already generated plan. Instead, run -target with atlantis plan")
|
||||
}
|
||||
@@ -45,16 +40,16 @@ func (a *ApplyStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []stri
|
||||
|
||||
// TODO: Leverage PlanTypeStepRunnerDelegate here
|
||||
if IsRemotePlan(contents) {
|
||||
args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...)
|
||||
out, err = a.runRemoteApply(ctx, args, path, planPath, tfVersion, envs)
|
||||
args := append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...)
|
||||
out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs)
|
||||
if err == nil {
|
||||
out = a.cleanRemoteApplyOutput(out)
|
||||
}
|
||||
} else {
|
||||
// NOTE: we need to quote the plan path because Bitbucket Server can
|
||||
// have spaces in its repo owner names which is part of the path.
|
||||
args := append(append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath))
|
||||
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, args, envs, tfVersion, ctx.Workspace)
|
||||
args := append(append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath))
|
||||
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, ctx.TerraformVersion, ctx.Workspace)
|
||||
}
|
||||
|
||||
// If the apply was successful, delete the plan.
|
||||
@@ -137,7 +132,7 @@ func (a *ApplyStepRunner) runRemoteApply(
|
||||
|
||||
// Start the async command execution.
|
||||
ctx.Log.Debug("starting async tf remote operation")
|
||||
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace)
|
||||
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace)
|
||||
var lines []string
|
||||
nextLineIsRunURL := false
|
||||
var runURL string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package runtime_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
. "github.com/petergtz/pegomock"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/core/runtime"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
|
||||
@@ -19,7 +20,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
logging_matchers "github.com/runatlantis/atlantis/server/logging/mocks/matchers"
|
||||
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
@@ -51,7 +52,14 @@ func TestRun_Success(t *testing.T) {
|
||||
tmpDir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
planPath := filepath.Join(tmpDir, "workspace.tfplan")
|
||||
err := os.WriteFile(planPath, nil, 0600)
|
||||
err := ioutil.WriteFile(planPath, nil, 0600)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
}
|
||||
Ok(t, err)
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
@@ -59,19 +67,13 @@ func TestRun_Success(t *testing.T) {
|
||||
o := runtime.ApplyStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := o.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace")
|
||||
_, err = os.Stat(planPath)
|
||||
Assert(t, os.IsNotExist(err), "planfile should be deleted")
|
||||
}
|
||||
@@ -81,7 +83,16 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) {
|
||||
tmpDir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
planPath := filepath.Join(tmpDir, "projectname-default.tfplan")
|
||||
err := os.WriteFile(planPath, nil, 0600)
|
||||
err := ioutil.WriteFile(planPath, nil, 0600)
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
ProjectName: "projectname",
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
}
|
||||
Ok(t, err)
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
@@ -89,20 +100,13 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) {
|
||||
o := runtime.ApplyStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := o.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
ProjectName: "projectname",
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default")
|
||||
_, err = os.Stat(planPath)
|
||||
Assert(t, os.IsNotExist(err), "planfile should be deleted")
|
||||
}
|
||||
@@ -114,26 +118,28 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) {
|
||||
err := os.WriteFile(planPath, nil, 0600)
|
||||
Ok(t, err)
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
o := runtime.ApplyStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tfVersion, _ := version.NewVersion("0.11.0")
|
||||
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := o.Run(models.ProjectCommandContext{
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
TerraformVersion: tfVersion,
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
}
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
o := runtime.ApplyStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace")
|
||||
_, err = os.Stat(planPath)
|
||||
Assert(t, os.IsNotExist(err), "planfile should be deleted")
|
||||
}
|
||||
@@ -267,7 +273,7 @@ null_resource.dir2[1]: Destruction complete after 0s
|
||||
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
|
||||
`, output)
|
||||
|
||||
Equals(t, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args"}, tfExec.CalledArgs)
|
||||
Equals(t, []string{"apply", "-input=false", "extra", "args", "comment", "args"}, tfExec.CalledArgs)
|
||||
_, err = os.Stat(planPath)
|
||||
Assert(t, os.IsNotExist(err), "planfile should be deleted")
|
||||
|
||||
@@ -364,7 +370,7 @@ type remoteApplyMock struct {
|
||||
}
|
||||
|
||||
// RunCommandAsync fakes out running terraform async.
|
||||
func (r *remoteApplyMock) RunCommandAsync(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) {
|
||||
func (r *remoteApplyMock) RunCommandAsync(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) {
|
||||
r.CalledArgs = args
|
||||
|
||||
in := make(chan string)
|
||||
|
||||
@@ -49,8 +49,6 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
|
||||
terraformInitArgs = []string{}
|
||||
}
|
||||
|
||||
terraformInitArgs = append(terraformInitArgs, "-no-color")
|
||||
|
||||
if MustConstraint("< 0.14.0").Check(tfVersion) || !common.FileExists(terraformLockfilePath) {
|
||||
terraformInitArgs = append(terraformInitArgs, "-upgrade")
|
||||
}
|
||||
@@ -59,7 +57,7 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
|
||||
|
||||
terraformInitCmd := append(terraformInitVerb, finalArgs...)
|
||||
|
||||
out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, envs, tfVersion, ctx.Workspace)
|
||||
out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfVersion, ctx.Workspace)
|
||||
// Only include the init output if there was an error. Otherwise it's
|
||||
// unnecessary and lengthens the comment.
|
||||
if err != nil {
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/core/runtime"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
|
||||
matchers2 "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
logging_matchers "github.com/runatlantis/atlantis/server/logging/mocks/matchers"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
@@ -49,30 +49,31 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}
|
||||
|
||||
tfVersion, _ := version.NewVersion(c.version)
|
||||
iso := runtime.InitStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
// If using init then we specify -input=false but not for get.
|
||||
expArgs := []string{c.expCmd, "-input=false", "-no-color", "-upgrade", "extra", "args"}
|
||||
expArgs := []string{c.expCmd, "-input=false", "-upgrade", "extra", "args"}
|
||||
if c.expCmd == "get" {
|
||||
expArgs = []string{c.expCmd, "-no-color", "-upgrade", "extra", "args"}
|
||||
expArgs = []string{c.expCmd, "-upgrade", "extra", "args"}
|
||||
}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -82,7 +83,7 @@ func TestRun_ShowInitOutputOnError(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
tfClient := mocks.NewMockClient()
|
||||
logger := logging.NewNoopLogger(t)
|
||||
When(tfClient.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(tfClient.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", errors.New("error"))
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.11.0")
|
||||
@@ -112,30 +113,31 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) {
|
||||
runCmd(t, repoDir, "git", "add", ".terraform.lock.hcl")
|
||||
runCmd(t, repoDir, "git", "commit", "-m", "add .terraform.lock.hcl")
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.14.0")
|
||||
iso := runtime.InitStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, repoDir, map[string]string(nil))
|
||||
output, err := iso.Run(ctx, []string{"extra", "args"}, repoDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
expectedArgs := []string{"init", "-input=false", "-no-color", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
expectedArgs := []string{"init", "-input=false", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) {
|
||||
@@ -144,28 +146,28 @@ func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) {
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.14.0")
|
||||
iso := runtime.InitStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
expectedArgs := []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) {
|
||||
@@ -179,26 +181,27 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.13.0")
|
||||
iso := runtime.InitStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
expectedArgs := []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func TestRun_InitExtraArgsDeDupe(t *testing.T) {
|
||||
@@ -211,32 +214,32 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) {
|
||||
{
|
||||
"No extra args",
|
||||
[]string{},
|
||||
[]string{"init", "-input=false", "-no-color", "-upgrade"},
|
||||
[]string{"init", "-input=false", "-upgrade"},
|
||||
},
|
||||
{
|
||||
"Override -upgrade",
|
||||
[]string{"-upgrade=false"},
|
||||
[]string{"init", "-input=false", "-no-color", "-upgrade=false"},
|
||||
[]string{"init", "-input=false", "-upgrade=false"},
|
||||
},
|
||||
{
|
||||
"Override -input",
|
||||
[]string{"-input=true"},
|
||||
[]string{"init", "-input=true", "-no-color", "-upgrade"},
|
||||
[]string{"init", "-input=true", "-upgrade"},
|
||||
},
|
||||
{
|
||||
"Override -input and -upgrade",
|
||||
[]string{"-input=true", "-upgrade=false"},
|
||||
[]string{"init", "-input=true", "-no-color", "-upgrade=false"},
|
||||
[]string{"init", "-input=true", "-upgrade=false"},
|
||||
},
|
||||
{
|
||||
"Non duplicate extra args",
|
||||
[]string{"extra", "args"},
|
||||
[]string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"},
|
||||
[]string{"init", "-input=false", "-upgrade", "extra", "args"},
|
||||
},
|
||||
{
|
||||
"Override upgrade with extra args",
|
||||
[]string{"extra", "args", "-upgrade=false"},
|
||||
[]string{"init", "-input=false", "-no-color", "-upgrade=false", "extra", "args"},
|
||||
[]string{"init", "-input=false", "-upgrade=false", "extra", "args"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -245,25 +248,26 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.10.0")
|
||||
iso := runtime.InitStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, c.extraArgs, "/path", map[string]string(nil))
|
||||
output, err := iso.Run(ctx, c.extraArgs, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -287,20 +291,21 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) {
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
|
||||
output, err := iso.Run(models.ProjectCommandContext{
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
Log: logger,
|
||||
}, []string{"extra", "args"}, repoDir, map[string]string(nil))
|
||||
}
|
||||
output, err := iso.Run(ctx, []string{"extra", "args"}, repoDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
// When there is no error, should not return init output to PR.
|
||||
Equals(t, "", output)
|
||||
|
||||
expectedArgs := []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, dir string, name string, args ...string) string {
|
||||
|
||||
@@ -45,7 +45,7 @@ func (p *PlanStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
|
||||
|
||||
planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))
|
||||
planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile)
|
||||
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace)
|
||||
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace)
|
||||
if p.isRemoteOpsErr(output, err) {
|
||||
ctx.Log.Debug("detected that this project is using TFE remote ops")
|
||||
return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile, envs)
|
||||
@@ -69,7 +69,7 @@ func (p *PlanStepRunner) isRemoteOpsErr(output string, err error) bool {
|
||||
// operations.
|
||||
func (p *PlanStepRunner) remotePlan(ctx models.ProjectCommandContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) {
|
||||
argList := [][]string{
|
||||
{"plan", "-input=false", "-refresh", "-no-color"},
|
||||
{"plan", "-input=false", "-refresh"},
|
||||
extraArgs,
|
||||
ctx.EscapedCommentArgs,
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path
|
||||
// already in the right workspace then no need to switch. This will save us
|
||||
// about ten seconds. This command is only available in > 0.10.
|
||||
if !runningZeroPointNine {
|
||||
workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace)
|
||||
workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,11 +139,11 @@ func (p *PlanStepRunner) switchWorkspace(ctx models.ProjectCommandContext, path
|
||||
// To do this we can either select and catch the error or use list and then
|
||||
// look for the workspace. Both commands take the same amount of time so
|
||||
// that's why we're running select here.
|
||||
_, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
|
||||
_, err := p.TerraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
|
||||
if err != nil {
|
||||
// If terraform workspace select fails we run terraform workspace
|
||||
// new to create a new workspace automatically.
|
||||
out, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
|
||||
out, err := p.TerraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", err, out)
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (p *PlanStepRunner) buildPlanCmd(ctx models.ProjectCommandContext, extraArg
|
||||
argList := [][]string{
|
||||
// NOTE: we need to quote the plan filename because Bitbucket Server can
|
||||
// have spaces in its repo owner names.
|
||||
{"plan", "-input=false", "-refresh", "-no-color", "-out", fmt.Sprintf("%q", planFile)},
|
||||
{"plan", "-input=false", "-refresh", "-out", fmt.Sprintf("%q", planFile)},
|
||||
tfVars,
|
||||
extraArgs,
|
||||
ctx.EscapedCommentArgs,
|
||||
@@ -252,7 +252,7 @@ func (p *PlanStepRunner) runRemotePlan(
|
||||
|
||||
// Start the async command execution.
|
||||
ctx.Log.Debug("starting async tf remote operation")
|
||||
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx.Log, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace)
|
||||
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace)
|
||||
var lines []string
|
||||
nextLineIsRunURL := false
|
||||
var runURL string
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
logging_matchers "github.com/runatlantis/atlantis/server/logging/mocks/matchers"
|
||||
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
@@ -29,16 +29,10 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.8")
|
||||
logger := logging.NewNoopLogger(t)
|
||||
workspace := "default"
|
||||
s := runtime.PlanStepRunner{
|
||||
DefaultTFVersion: tfVersion,
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
workspace := "default"
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Workspace: workspace,
|
||||
@@ -52,17 +46,24 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
}
|
||||
s := runtime.PlanStepRunner{
|
||||
DefaultTFVersion: tfVersion,
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(
|
||||
logger,
|
||||
ctx,
|
||||
"/path",
|
||||
[]string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
"\"/path/default.tfplan\"",
|
||||
"-var",
|
||||
@@ -84,20 +85,18 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
|
||||
workspace)
|
||||
|
||||
// Verify that no env or workspace commands were run
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger,
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx,
|
||||
"/path",
|
||||
[]string{"env",
|
||||
"select",
|
||||
"-no-color",
|
||||
"workspace"},
|
||||
map[string]string(nil),
|
||||
tfVersion,
|
||||
workspace)
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger,
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx,
|
||||
"/path",
|
||||
[]string{"workspace",
|
||||
"select",
|
||||
"-no-color",
|
||||
"workspace"},
|
||||
map[string]string(nil),
|
||||
tfVersion,
|
||||
@@ -118,7 +117,7 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) {
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
_, err := s.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
@@ -160,15 +159,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) {
|
||||
|
||||
tfVersion, _ := version.NewVersion(c.tfVersion)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
s := runtime.PlanStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
@@ -182,26 +173,32 @@ func TestRun_SwitchesWorkspace(t *testing.T) {
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
}
|
||||
s := runtime.PlanStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
// Verify that env select was called as well as plan.
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger,
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx,
|
||||
"/path",
|
||||
[]string{c.expWorkspaceCmd,
|
||||
"select",
|
||||
"-no-color",
|
||||
"workspace"},
|
||||
map[string]string(nil),
|
||||
tfVersion,
|
||||
"workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger,
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx,
|
||||
"/path",
|
||||
[]string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
"\"/path/workspace.tfplan\"",
|
||||
"-var",
|
||||
@@ -256,6 +253,21 @@ func TestRun_CreatesWorkspace(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
tfVersion, _ := version.NewVersion(c.tfVersion)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
s := runtime.PlanStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
@@ -263,15 +275,14 @@ func TestRun_CreatesWorkspace(t *testing.T) {
|
||||
|
||||
// Ensure that we actually try to switch workspaces by making the
|
||||
// output of `workspace show` to be a different name.
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil)
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil)
|
||||
|
||||
expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "-no-color", "workspace"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
|
||||
expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "workspace"}
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
|
||||
|
||||
expPlanArgs := []string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
"\"/path/workspace.tfplan\"",
|
||||
"-var",
|
||||
@@ -288,29 +299,15 @@ func TestRun_CreatesWorkspace(t *testing.T) {
|
||||
"args",
|
||||
"comment",
|
||||
"args"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
// Verify that env select was called as well as plan.
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,16 +319,30 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
tfVersion, _ := version.NewVersion("0.10.0")
|
||||
logger := logging.NewNoopLogger(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
s := runtime.PlanStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
|
||||
|
||||
expPlanArgs := []string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
"\"/path/workspace.tfplan\"",
|
||||
"-var",
|
||||
@@ -348,30 +359,16 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
|
||||
"args",
|
||||
"comment",
|
||||
"args"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
|
||||
// Verify that workspace select was never called.
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func TestRun_AddsEnvVarFile(t *testing.T) {
|
||||
@@ -399,7 +396,6 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
|
||||
expPlanArgs := []string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
fmt.Sprintf("%q", filepath.Join(tmpDir, "workspace.tfplan")),
|
||||
"-var",
|
||||
@@ -419,9 +415,7 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
|
||||
"-var-file",
|
||||
envVarsFile,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logger, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
RepoRelDir: ".",
|
||||
@@ -435,12 +429,15 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
|
||||
// Verify that env select was never called since we're in version >= 0.10
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, tmpDir, []string{"env", "select", "-no-color", "workspace"}, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")
|
||||
Equals(t, "output", output)
|
||||
}
|
||||
|
||||
@@ -455,12 +452,27 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
|
||||
|
||||
expPlanArgs := []string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
"\"/path/projectname-default.tfplan\"",
|
||||
"-var",
|
||||
@@ -478,24 +490,9 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
|
||||
"comment",
|
||||
"args",
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil)
|
||||
When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, "output", output)
|
||||
}
|
||||
@@ -536,7 +533,7 @@ Terraform will perform the following actions:
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
When(terraform.RunCommandWithVersion(
|
||||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||||
matchers.AnyModelsProjectCommandContext(),
|
||||
AnyString(),
|
||||
AnyStringSlice(),
|
||||
matchers2.AnyMapOfStringToString(),
|
||||
@@ -590,7 +587,7 @@ func TestRun_OutputOnErr(t *testing.T) {
|
||||
expOutput := "expected output"
|
||||
expErrMsg := "error!"
|
||||
When(terraform.RunCommandWithVersion(
|
||||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||||
matchers.AnyModelsProjectCommandContext(),
|
||||
AnyString(),
|
||||
AnyStringSlice(),
|
||||
matchers2.AnyMapOfStringToString(),
|
||||
@@ -623,7 +620,6 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
|
||||
"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
fmt.Sprintf("%q", "/path/default.tfplan"),
|
||||
"extra",
|
||||
@@ -650,7 +646,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
When(terraform.RunCommandWithVersion(
|
||||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||||
matchers.AnyModelsProjectCommandContext(),
|
||||
AnyString(),
|
||||
AnyStringSlice(),
|
||||
matchers2.AnyMapOfStringToString(),
|
||||
@@ -662,8 +658,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
}
|
||||
|
||||
output, err := s.Run(models.ProjectCommandContext{
|
||||
ctx := models.ProjectCommandContext{
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
@@ -676,11 +671,13 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
}
|
||||
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, "output", output)
|
||||
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -706,13 +703,28 @@ locally at this time.
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
|
||||
// Now that mocking is set up, we're ready to run the plan.
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
asyncTf := &remotePlanMock{}
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.11.12")
|
||||
updater := mocks2.NewMockCommitStatusUpdater()
|
||||
asyncTf := &remotePlanMock{}
|
||||
s := runtime.PlanStepRunner{
|
||||
TerraformExecutor: terraform,
|
||||
DefaultTFVersion: tfVersion,
|
||||
@@ -724,7 +736,7 @@ locally at this time.
|
||||
|
||||
// First, terraform workspace gets run.
|
||||
When(terraform.RunCommandWithVersion(
|
||||
logger,
|
||||
ctx,
|
||||
absProjectPath,
|
||||
[]string{"workspace", "show"},
|
||||
map[string]string(nil),
|
||||
@@ -735,7 +747,6 @@ locally at this time.
|
||||
expPlanArgs := []string{"plan",
|
||||
"-input=false",
|
||||
"-refresh",
|
||||
"-no-color",
|
||||
"-out",
|
||||
fmt.Sprintf("%q", filepath.Join(absProjectPath, "default.tfplan")),
|
||||
"-var",
|
||||
@@ -757,25 +768,9 @@ locally at this time.
|
||||
planErr := errors.New("exit status 1: err")
|
||||
planOutput := "\n" + remoteOpsErr
|
||||
asyncTf.LinesToSend = remotePlanOutput
|
||||
When(terraform.RunCommandWithVersion(logger, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")).
|
||||
When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")).
|
||||
ThenReturn(planOutput, planErr)
|
||||
|
||||
// Now that mocking is set up, we're ready to run the plan.
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil))
|
||||
Ok(t, err)
|
||||
Equals(t, `
|
||||
@@ -790,7 +785,7 @@ Terraform will perform the following actions:
|
||||
|
||||
Plan: 0 to add, 0 to change, 1 to destroy.`, output)
|
||||
|
||||
expRemotePlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "extra", "args", "comment", "args"}
|
||||
expRemotePlanArgs := []string{"plan", "-input=false", "-refresh", "extra", "args", "comment", "args"}
|
||||
Equals(t, expRemotePlanArgs, asyncTf.CalledArgs)
|
||||
|
||||
// Verify that the fake plan file we write has the correct contents.
|
||||
@@ -889,7 +884,7 @@ type remotePlanMock struct {
|
||||
CalledArgs []string
|
||||
}
|
||||
|
||||
func (r *remotePlanMock) RunCommandAsync(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) {
|
||||
func (r *remotePlanMock) RunCommandAsync(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line) {
|
||||
r.CalledArgs = args
|
||||
in := make(chan string)
|
||||
out := make(chan terraform.Line)
|
||||
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
// TerraformExec brings the interface from TerraformClient into this package
|
||||
// without causing circular imports.
|
||||
type TerraformExec interface {
|
||||
RunCommandWithVersion(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error)
|
||||
RunCommandWithVersion(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error)
|
||||
EnsureVersion(log logging.SimpleLogging, v *version.Version) error
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type AsyncTFExec interface {
|
||||
// Callers can use the input channel to pass stdin input to the command.
|
||||
// If any error is passed on the out channel, there will be no
|
||||
// further output (so callers are free to exit).
|
||||
RunCommandAsync(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line)
|
||||
RunCommandAsync(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan terraform.Line)
|
||||
}
|
||||
|
||||
// StatusUpdater brings the interface from CommitStatusUpdater into this package
|
||||
|
||||
@@ -39,9 +39,9 @@ func (p *ShowStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin
|
||||
showResultFile := filepath.Join(path, ctx.GetShowResultFileName())
|
||||
|
||||
output, err := p.TerraformExecutor.RunCommandWithVersion(
|
||||
ctx.Log,
|
||||
ctx,
|
||||
path,
|
||||
[]string{"show", "-no-color", "-json", filepath.Clean(planFile)},
|
||||
[]string{"show", "-json", filepath.Clean(planFile)},
|
||||
envs,
|
||||
tfVersion,
|
||||
ctx.Workspace,
|
||||
|
||||
@@ -2,6 +2,7 @@ package runtime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -38,7 +39,7 @@ func TestShowStepRunnner(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
|
||||
When(mockExecutor.RunCommandWithVersion(
|
||||
logger, path, []string{"show", "-no-color", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
|
||||
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
|
||||
)).ThenReturn("success", nil)
|
||||
|
||||
r, err := subject.Run(context, []string{}, path, envs)
|
||||
@@ -48,8 +49,8 @@ func TestShowStepRunnner(t *testing.T) {
|
||||
actual, _ := os.ReadFile(resultPath)
|
||||
|
||||
actualStr := string(actual)
|
||||
Assert(t, actualStr == "success", "got expected result")
|
||||
Assert(t, r == "success", "returned expected result")
|
||||
Assert(t, actualStr == "success", fmt.Sprintf("expected '%s' to be success", actualStr))
|
||||
Assert(t, r == "success", fmt.Sprintf("expected '%s' to be success", r))
|
||||
|
||||
})
|
||||
|
||||
@@ -65,7 +66,7 @@ func TestShowStepRunnner(t *testing.T) {
|
||||
}
|
||||
|
||||
When(mockExecutor.RunCommandWithVersion(
|
||||
logger, path, []string{"show", "-no-color", "-json", filepath.Join(path, "test-default.tfplan")}, envs, v, context.Workspace,
|
||||
contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, v, context.Workspace,
|
||||
)).ThenReturn("success", nil)
|
||||
|
||||
r, err := subject.Run(contextWithVersionOverride, []string{}, path, envs)
|
||||
@@ -82,7 +83,7 @@ func TestShowStepRunnner(t *testing.T) {
|
||||
|
||||
t.Run("failure running command", func(t *testing.T) {
|
||||
When(mockExecutor.RunCommandWithVersion(
|
||||
logger, path, []string{"show", "-no-color", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
|
||||
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
|
||||
)).ThenReturn("success", errors.New("error"))
|
||||
|
||||
_, err := subject.Run(context, []string{}, path, envs)
|
||||
|
||||
@@ -21,5 +21,5 @@ func (v *VersionStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []st
|
||||
}
|
||||
|
||||
versionCmd := []string{"version"}
|
||||
return v.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Clean(path), versionCmd, envs, tfVersion, ctx.Workspace)
|
||||
return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfVersion, ctx.Workspace)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestRunVersionStep(t *testing.T) {
|
||||
|
||||
t.Run("ensure runs", func(t *testing.T) {
|
||||
_, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, []string{"version"}, map[string]string(nil), tfVersion, "default")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfVersion, "default")
|
||||
Ok(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
go_version "github.com/hashicorp/go-version"
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
logging "github.com/runatlantis/atlantis/server/logging"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
go_version "github.com/hashicorp/go-version"
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
logging "github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
type MockClient struct {
|
||||
@@ -26,11 +29,11 @@ func NewMockClient(options ...pegomock.Option) *MockClient {
|
||||
func (mock *MockClient) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockClient) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockClient) RunCommandWithVersion(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) {
|
||||
func (mock *MockClient) RunCommandWithVersion(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockClient().")
|
||||
}
|
||||
params := []pegomock.Param{log, path, args, envs, v, workspace}
|
||||
params := []pegomock.Param{ctx, path, args, envs, v, workspace}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||
var ret0 string
|
||||
var ret1 error
|
||||
@@ -45,6 +48,16 @@ func (mock *MockClient) RunCommandWithVersion(log logging.SimpleLogging, path st
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
func (mock *MockClient) RunCommandAsync(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (chan<- string, <-chan terraform.Line) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockClient().")
|
||||
}
|
||||
outCh := make(chan terraform.Line)
|
||||
inCh := make(chan string)
|
||||
|
||||
return inCh, outCh
|
||||
}
|
||||
|
||||
func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) error {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockClient().")
|
||||
@@ -97,8 +110,8 @@ type VerifierMockClient struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockClient) RunCommandWithVersion(log logging.SimpleLogging, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {
|
||||
params := []pegomock.Param{log, path, args, envs, v, workspace}
|
||||
func (verifier *VerifierMockClient) RunCommandWithVersion(ctx models.ProjectCommandContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {
|
||||
params := []pegomock.Param{ctx, path, args, envs, v, workspace}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", params, verifier.timeout)
|
||||
return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,18 @@ import (
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform/ansi"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
var LogStreamingValidCmds = [...]string{"init", "plan", "apply"}
|
||||
|
||||
// Setting the buffer size to 10mb
|
||||
const BufioScannerBufferSize = 10 * 1024 * 1024
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_terraform_client.go Client
|
||||
|
||||
type Client interface {
|
||||
@@ -69,6 +78,8 @@ type DefaultClient struct {
|
||||
|
||||
// usePluginCache determines whether or not to set the TF_PLUGIN_CACHE_DIR env var
|
||||
usePluginCache bool
|
||||
|
||||
projectCmdOutputHandler handlers.ProjectCommandOutputHandler
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_downloader.go Downloader
|
||||
@@ -100,6 +111,7 @@ func NewClientWithDefaultVersion(
|
||||
tfDownloader Downloader,
|
||||
usePluginCache bool,
|
||||
fetchAsync bool,
|
||||
projectCmdOutputHandler handlers.ProjectCommandOutputHandler,
|
||||
) (*DefaultClient, error) {
|
||||
var finalDefaultVersion *version.Version
|
||||
var localVersion *version.Version
|
||||
@@ -157,7 +169,6 @@ func NewClientWithDefaultVersion(
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &DefaultClient{
|
||||
defaultVersion: finalDefaultVersion,
|
||||
terraformPluginCacheDir: cacheDir,
|
||||
@@ -167,6 +178,7 @@ func NewClientWithDefaultVersion(
|
||||
versionsLock: &versionsLock,
|
||||
versions: versions,
|
||||
usePluginCache: usePluginCache,
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -181,7 +193,9 @@ func NewTestClient(
|
||||
defaultVersionFlagName string,
|
||||
tfDownloadURL string,
|
||||
tfDownloader Downloader,
|
||||
usePluginCache bool) (*DefaultClient, error) {
|
||||
usePluginCache bool,
|
||||
projectCmdOutputHandler handlers.ProjectCommandOutputHandler,
|
||||
) (*DefaultClient, error) {
|
||||
return NewClientWithDefaultVersion(
|
||||
log,
|
||||
binDir,
|
||||
@@ -194,6 +208,7 @@ func NewTestClient(
|
||||
tfDownloader,
|
||||
usePluginCache,
|
||||
false,
|
||||
projectCmdOutputHandler,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,7 +230,9 @@ func NewClient(
|
||||
defaultVersionFlagName string,
|
||||
tfDownloadURL string,
|
||||
tfDownloader Downloader,
|
||||
usePluginCache bool) (*DefaultClient, error) {
|
||||
usePluginCache bool,
|
||||
projectCmdOutputHandler handlers.ProjectCommandOutputHandler,
|
||||
) (*DefaultClient, error) {
|
||||
return NewClientWithDefaultVersion(
|
||||
log,
|
||||
binDir,
|
||||
@@ -228,6 +245,7 @@ func NewClient(
|
||||
tfDownloader,
|
||||
usePluginCache,
|
||||
true,
|
||||
projectCmdOutputHandler,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -260,8 +278,25 @@ func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Vers
|
||||
}
|
||||
|
||||
// See Client.RunCommandWithVersion.
|
||||
func (c *DefaultClient) RunCommandWithVersion(log logging.SimpleLogging, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) {
|
||||
tfCmd, cmd, err := c.prepCmd(log, v, workspace, path, args)
|
||||
func (c *DefaultClient) RunCommandWithVersion(ctx models.ProjectCommandContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) {
|
||||
if isAsyncEligibleCommand(args[0]) {
|
||||
_, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, v, workspace)
|
||||
var lines []string
|
||||
var err error
|
||||
for line := range outCh {
|
||||
if line.Err != nil {
|
||||
err = line.Err
|
||||
break
|
||||
}
|
||||
lines = append(lines, line.Line)
|
||||
}
|
||||
output := strings.Join(lines, "\n")
|
||||
|
||||
// sanitize output by stripping out any ansi characters.
|
||||
output = ansi.Strip(output)
|
||||
return fmt.Sprintf("%s\n", output), err
|
||||
}
|
||||
tfCmd, cmd, err := c.prepCmd(ctx.Log, v, workspace, path, args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -273,11 +308,12 @@ func (c *DefaultClient) RunCommandWithVersion(log logging.SimpleLogging, path st
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "running %q in %q", tfCmd, path)
|
||||
log.Err(err.Error())
|
||||
return string(out), err
|
||||
ctx.Log.Err(err.Error())
|
||||
return ansi.Strip(string(out)), err
|
||||
}
|
||||
log.Info("successfully ran %q in %q", tfCmd, path)
|
||||
return string(out), nil
|
||||
ctx.Log.Info("successfully ran %q in %q", tfCmd, path)
|
||||
|
||||
return ansi.Strip(string(out)), nil
|
||||
}
|
||||
|
||||
// prepCmd builds a ready to execute command based on the version of terraform
|
||||
@@ -340,7 +376,7 @@ type Line struct {
|
||||
// Callers can use the input channel to pass stdin input to the command.
|
||||
// If any error is passed on the out channel, there will be no
|
||||
// further output (so callers are free to exit).
|
||||
func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan Line) {
|
||||
func (c *DefaultClient) RunCommandAsync(ctx models.ProjectCommandContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan Line) {
|
||||
outCh := make(chan Line)
|
||||
inCh := make(chan string)
|
||||
|
||||
@@ -354,9 +390,9 @@ func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string,
|
||||
close(inCh)
|
||||
}()
|
||||
|
||||
tfCmd, cmd, err := c.prepCmd(log, v, workspace, path, args)
|
||||
tfCmd, cmd, err := c.prepCmd(ctx.Log, v, workspace, path, args)
|
||||
if err != nil {
|
||||
log.Err(err.Error())
|
||||
ctx.Log.Err(err.Error())
|
||||
outCh <- Line{Err: err}
|
||||
return
|
||||
}
|
||||
@@ -369,11 +405,11 @@ func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string,
|
||||
}
|
||||
cmd.Env = envVars
|
||||
|
||||
log.Debug("starting %q in %q", tfCmd, path)
|
||||
ctx.Log.Debug("starting %q in %q", tfCmd, path)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "running %q in %q", tfCmd, path)
|
||||
log.Err(err.Error())
|
||||
ctx.Log.Err(err.Error())
|
||||
outCh <- Line{Err: err}
|
||||
return
|
||||
}
|
||||
@@ -382,10 +418,10 @@ func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string,
|
||||
// This function will exit when inCh is closed which we do in our defer.
|
||||
go func() {
|
||||
for line := range inCh {
|
||||
log.Debug("writing %q to remote command's stdin", line)
|
||||
ctx.Log.Debug("writing %q to remote command's stdin", line)
|
||||
_, err := io.WriteString(stdin, line)
|
||||
if err != nil {
|
||||
log.Err(errors.Wrapf(err, "writing %q to process", line).Error())
|
||||
ctx.Log.Err(errors.Wrapf(err, "writing %q to process", line).Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -393,19 +429,25 @@ func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string,
|
||||
// Use a waitgroup to block until our stdout/err copying is complete.
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(2)
|
||||
|
||||
// Asynchronously copy from stdout/err to outCh.
|
||||
go func() {
|
||||
s := bufio.NewScanner(stdout)
|
||||
buf := []byte{}
|
||||
s.Buffer(buf, BufioScannerBufferSize)
|
||||
|
||||
for s.Scan() {
|
||||
outCh <- Line{Line: s.Text()}
|
||||
message := s.Text()
|
||||
outCh <- Line{Line: message}
|
||||
c.projectCmdOutputHandler.Send(ctx, message)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
s := bufio.NewScanner(stderr)
|
||||
for s.Scan() {
|
||||
outCh <- Line{Line: s.Text()}
|
||||
message := s.Text()
|
||||
outCh <- Line{Line: message}
|
||||
c.projectCmdOutputHandler.Send(ctx, message)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -420,10 +462,10 @@ func (c *DefaultClient) RunCommandAsync(log logging.SimpleLogging, path string,
|
||||
// We're done now. Send an error if there was one.
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "running %q in %q", tfCmd, path)
|
||||
log.Err(err.Error())
|
||||
ctx.Log.Err(err.Error())
|
||||
outCh <- Line{Err: err}
|
||||
} else {
|
||||
log.Info("successfully ran %q in %q", tfCmd, path)
|
||||
ctx.Log.Info("successfully ran %q in %q", tfCmd, path)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -508,6 +550,15 @@ func generateRCFile(tfeToken string, tfeHostname string, home string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAsyncEligibleCommand(cmd string) bool {
|
||||
for _, validCmd := range LogStreamingValidCmds {
|
||||
if validCmd == cmd {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getVersion(tfBinary string) (*version.Version, error) {
|
||||
versionOutBytes, err := exec.Command(tfBinary, "version").Output() // #nosec
|
||||
versionOutput := string(versionOutBytes)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
handlermocks "github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
@@ -88,12 +90,32 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "echo",
|
||||
usePluginCache: true,
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
|
||||
args := []string{
|
||||
@@ -103,8 +125,8 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) {
|
||||
"ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION",
|
||||
"DIR=$DIR",
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
out, err := client.RunCommandWithVersion(log, tmp, args, map[string]string{}, nil, "workspace")
|
||||
customEnvVars := map[string]string{}
|
||||
out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, nil, "workspace")
|
||||
Ok(t, err)
|
||||
exp := fmt.Sprintf("TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\n", tmp, tmp)
|
||||
Equals(t, exp, out)
|
||||
@@ -115,11 +137,31 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "echo",
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
|
||||
args := []string{
|
||||
@@ -128,8 +170,7 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) {
|
||||
"exit",
|
||||
"1",
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
out, err := client.RunCommandWithVersion(log, tmp, args, map[string]string{}, nil, "workspace")
|
||||
out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, nil, "workspace")
|
||||
ErrEquals(t, fmt.Sprintf(`running "echo dying && exit 1" in %q: exit status 1`, tmp), err)
|
||||
// Test that we still get our output.
|
||||
Equals(t, "dying\n", out)
|
||||
@@ -139,12 +180,32 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "echo",
|
||||
usePluginCache: true,
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
|
||||
args := []string{
|
||||
@@ -154,8 +215,7 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) {
|
||||
"ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION",
|
||||
"DIR=$DIR",
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
_, outCh := client.RunCommandAsync(log, tmp, args, map[string]string{}, nil, "workspace")
|
||||
_, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, nil, "workspace")
|
||||
|
||||
out, err := waitCh(outCh)
|
||||
Ok(t, err)
|
||||
@@ -167,11 +227,31 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "cat",
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
filename := filepath.Join(tmp, "data")
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
@@ -184,8 +264,7 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) {
|
||||
_, err = f.WriteString(s)
|
||||
Ok(t, err)
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
_, outCh := client.RunCommandAsync(log, tmp, []string{filename}, map[string]string{}, nil, "workspace")
|
||||
_, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, nil, "workspace")
|
||||
|
||||
out, err := waitCh(outCh)
|
||||
Ok(t, err)
|
||||
@@ -196,14 +275,33 @@ func TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "echo",
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
_, outCh := client.RunCommandAsync(log, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace")
|
||||
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace")
|
||||
|
||||
out, err := waitCh(outCh)
|
||||
Ok(t, err)
|
||||
@@ -214,14 +312,33 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "echo",
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
_, outCh := client.RunCommandAsync(log, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace")
|
||||
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace")
|
||||
|
||||
out, err := waitCh(outCh)
|
||||
ErrEquals(t, fmt.Sprintf(`running "echo dying && exit 1" in %q: exit status 1`, tmp), err)
|
||||
@@ -233,14 +350,34 @@ func TestDefaultClient_RunCommandAsync_Input(t *testing.T) {
|
||||
v, err := version.NewVersion("0.11.11")
|
||||
Ok(t, err)
|
||||
tmp, cleanup := TempDir(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
User: models.User{Username: "username"},
|
||||
EscapedCommentArgs: []string{"comment", "args"},
|
||||
ProjectName: "projectname",
|
||||
Pull: models.PullRequest{
|
||||
Num: 2,
|
||||
},
|
||||
BaseRepo: models.Repo{
|
||||
FullName: "owner/repo",
|
||||
Owner: "owner",
|
||||
Name: "repo",
|
||||
},
|
||||
}
|
||||
defer cleanup()
|
||||
client := &DefaultClient{
|
||||
defaultVersion: v,
|
||||
terraformPluginCacheDir: tmp,
|
||||
overrideTF: "read",
|
||||
projectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}
|
||||
log := logging.NewNoopLogger(t)
|
||||
inCh, outCh := client.RunCommandAsync(log, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace")
|
||||
|
||||
inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace")
|
||||
inCh <- "echo me\n"
|
||||
|
||||
out, err := waitCh(outCh)
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"github.com/runatlantis/atlantis/cmd"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform"
|
||||
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
handlermocks "github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
@@ -59,6 +61,12 @@ Your version of Terraform is out of date! The latest version
|
||||
is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
|
||||
`
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
logger := logging.NewNoopLogger(t)
|
||||
@@ -69,13 +77,13 @@ 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(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Ok(t, err)
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{"test": "123"}, nil, "")
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, nil, "")
|
||||
Ok(t, err)
|
||||
Equals(t, fakeBinOut+"\n", output)
|
||||
}
|
||||
@@ -90,6 +98,12 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
|
||||
`
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// We're testing this by adding our own "fake" terraform binary to path that
|
||||
@@ -98,13 +112,13 @@ 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(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Ok(t, err)
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{}, nil, "")
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
|
||||
Ok(t, err)
|
||||
Equals(t, fakeBinOut+"\n", output)
|
||||
}
|
||||
@@ -114,12 +128,13 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
|
||||
func TestNewClient_NoTF(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
defer cleanup()
|
||||
|
||||
// Set PATH to only include our empty directory.
|
||||
defer tempSetEnv(t, "PATH", tmp)()
|
||||
|
||||
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://www.terraform.io/downloads.html", err)
|
||||
}
|
||||
|
||||
@@ -129,6 +144,12 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
|
||||
fakeBinOut := "Terraform v0.11.10\n"
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// We're testing this by adding our own "fake" terraform binary to path that
|
||||
@@ -137,13 +158,13 @@ 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(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Ok(t, err)
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{}, nil, "")
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
|
||||
Ok(t, err)
|
||||
Equals(t, fakeBinOut+"\n", output)
|
||||
}
|
||||
@@ -152,8 +173,13 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
|
||||
// bin dir that we use it.
|
||||
func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {
|
||||
fakeBinOut := "Terraform v0.11.10\n"
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Add our fake binary to {datadir}/bin/terraform{version}.
|
||||
@@ -161,13 +187,13 @@ 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(t), binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
c, err := terraform.NewClient(logging.NewNoopLogger(t), binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Ok(t, err)
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{}, nil, "")
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
|
||||
Ok(t, err)
|
||||
Equals(t, fakeBinOut+"\n", output)
|
||||
}
|
||||
@@ -177,6 +203,12 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Set PATH to empty so there's no TF available.
|
||||
@@ -188,7 +220,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
|
||||
err := os.WriteFile(params[0].(string), []byte("#!/bin/sh\necho '\nTerraform v0.11.10\n'"), 0700) // #nosec G306
|
||||
return []pegomock.ReturnValue{err}
|
||||
})
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, "https://my-mirror.releases.mycompany.com", mockDownloader, true)
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, "https://my-mirror.releases.mycompany.com", mockDownloader, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Ok(t, err)
|
||||
@@ -203,7 +235,8 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
|
||||
|
||||
// Reset PATH so that it has sh.
|
||||
Ok(t, os.Setenv("PATH", orig))
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{}, nil, "")
|
||||
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
|
||||
Ok(t, err)
|
||||
Equals(t, "\nTerraform v0.11.10\n\n", output)
|
||||
}
|
||||
@@ -212,8 +245,9 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
|
||||
func TestNewClient_BadVersion(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
_, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
defer cleanup()
|
||||
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true)
|
||||
_, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, nil, true, projectCmdOutputHandler)
|
||||
ErrEquals(t, "Malformed version: malformed", err)
|
||||
}
|
||||
|
||||
@@ -222,6 +256,12 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
mockDownloader := mocks.NewMockDownloader()
|
||||
@@ -237,13 +277,15 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
|
||||
return []pegomock.ReturnValue{err}
|
||||
})
|
||||
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true)
|
||||
c, err := terraform.NewClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
v, err := version.NewVersion("99.99.99")
|
||||
Ok(t, err)
|
||||
output, err := c.RunCommandWithVersion(logger, tmp, nil, map[string]string{}, v, "")
|
||||
|
||||
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, v, "")
|
||||
|
||||
Assert(t, err == nil, "err: %s: %s", err, output)
|
||||
Equals(t, "\nTerraform v99.99.99\n\n", output)
|
||||
}
|
||||
@@ -253,11 +295,12 @@ func TestEnsureVersion_downloaded(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
tmp, binDir, cacheDir, cleanup := mkSubDirs(t)
|
||||
projectCmdOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
defer cleanup()
|
||||
|
||||
mockDownloader := mocks.NewMockDownloader()
|
||||
|
||||
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true)
|
||||
c, err := terraform.NewTestClient(logger, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, mockDownloader, true, projectCmdOutputHandler)
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "0.11.10", c.DefaultVersion().String())
|
||||
|
||||
@@ -78,7 +78,6 @@ func setup(t *testing.T) *vcsmocks.MockClient {
|
||||
workingDir = mocks.NewMockWorkingDir()
|
||||
pendingPlanFinder = mocks.NewMockPendingPlanFinder()
|
||||
commitUpdater = mocks.NewMockCommitStatusUpdater()
|
||||
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
defaultBoltDB, err := db.New(tmp)
|
||||
|
||||
@@ -43,18 +43,9 @@ type DefaultCommitStatusUpdater struct {
|
||||
TitleBuilder vcs.StatusTitleBuilder
|
||||
}
|
||||
|
||||
func (d *DefaultCommitStatusUpdater) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, cmdName models.CommandName) error {
|
||||
src := d.TitleBuilder.Build(cmdName.String())
|
||||
var descripWords string
|
||||
switch status {
|
||||
case models.PendingCommitStatus:
|
||||
descripWords = "in progress..."
|
||||
case models.FailedCommitStatus:
|
||||
descripWords = "failed."
|
||||
case models.SuccessCommitStatus:
|
||||
descripWords = "succeeded."
|
||||
}
|
||||
descrip := fmt.Sprintf("%s %s", strings.Title(cmdName.String()), descripWords)
|
||||
func (d *DefaultCommitStatusUpdater) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error {
|
||||
src := d.TitleBuilder.Build(command.String())
|
||||
descrip := fmt.Sprintf("%s %s", strings.Title(command.String()), d.statusDescription(status))
|
||||
return d.Client.UpdateStatus(repo, pull, status, src, descrip, "")
|
||||
}
|
||||
|
||||
@@ -79,10 +70,14 @@ func (d *DefaultCommitStatusUpdater) UpdateProject(ctx models.ProjectCommandCont
|
||||
if projectID == "" {
|
||||
projectID = fmt.Sprintf("%s/%s", ctx.RepoRelDir, ctx.Workspace)
|
||||
}
|
||||
|
||||
src := d.TitleBuilder.Build(cmdName.String(), vcs.StatusTitleOptions{
|
||||
ProjectName: projectID,
|
||||
})
|
||||
descrip := fmt.Sprintf("%s %s", strings.Title(cmdName.String()), d.statusDescription(status))
|
||||
return d.Client.UpdateStatus(ctx.BaseRepo, ctx.Pull, status, src, descrip, url)
|
||||
}
|
||||
|
||||
func (d *DefaultCommitStatusUpdater) statusDescription(status models.CommitStatus) string {
|
||||
var descripWords string
|
||||
switch status {
|
||||
case models.PendingCommitStatus:
|
||||
@@ -92,6 +87,6 @@ func (d *DefaultCommitStatusUpdater) UpdateProject(ctx models.ProjectCommandCont
|
||||
case models.SuccessCommitStatus:
|
||||
descripWords = "succeeded."
|
||||
}
|
||||
descrip := fmt.Sprintf("%s %s", strings.Title(cmdName.String()), descripWords)
|
||||
return d.Client.UpdateStatus(ctx.BaseRepo, ctx.Pull, status, src, descrip, url)
|
||||
|
||||
return descripWords
|
||||
}
|
||||
|
||||
109
server/events/mocks/mock_log_stream_url_generator.go
Normal file
109
server/events/mocks/mock_log_stream_url_generator.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/events (interfaces: JobsUrlGenerator)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockJobsUrlGenerator struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockJobsUrlGenerator(options ...pegomock.Option) *MockJobsUrlGenerator {
|
||||
mock := &MockJobsUrlGenerator{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockJobsUrlGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockJobsUrlGenerator) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockJobsUrlGenerator) GenerateProjectJobsUrl(pull models.PullRequest, p models.ProjectCommandContext) string {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockJobsUrlGenerator().")
|
||||
}
|
||||
params := []pegomock.Param{pull, p}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("GenerateProjectJobsUrl", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
|
||||
var ret0 string
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(string)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockJobsUrlGenerator) VerifyWasCalledOnce() *VerifierMockJobsUrlGenerator {
|
||||
return &VerifierMockJobsUrlGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJobsUrlGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockJobsUrlGenerator {
|
||||
return &VerifierMockJobsUrlGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJobsUrlGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockJobsUrlGenerator {
|
||||
return &VerifierMockJobsUrlGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJobsUrlGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockJobsUrlGenerator {
|
||||
return &VerifierMockJobsUrlGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockJobsUrlGenerator struct {
|
||||
mock *MockJobsUrlGenerator
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockJobsUrlGenerator) GenerateProjectJobsUrl(pull models.PullRequest, p models.ProjectCommandContext) *MockJobsUrlGenerator_GenerateProjectJobsUrl_OngoingVerification {
|
||||
params := []pegomock.Param{pull, p}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GenerateProjectJobsUrl", params, verifier.timeout)
|
||||
return &MockJobsUrlGenerator_GenerateProjectJobsUrl_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockJobsUrlGenerator_GenerateProjectJobsUrl_OngoingVerification struct {
|
||||
mock *MockJobsUrlGenerator
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockJobsUrlGenerator_GenerateProjectJobsUrl_OngoingVerification) GetCapturedArguments() (models.PullRequest, models.ProjectCommandContext) {
|
||||
pull, p := c.GetAllCapturedArguments()
|
||||
return pull[len(pull)-1], p[len(p)-1]
|
||||
}
|
||||
|
||||
func (c *MockJobsUrlGenerator_GenerateProjectJobsUrl_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest, _param1 []models.ProjectCommandContext) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.PullRequest, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.PullRequest)
|
||||
}
|
||||
_param1 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -13,7 +13,12 @@
|
||||
|
||||
package fixtures
|
||||
|
||||
import "github.com/runatlantis/atlantis/server/events/models"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
)
|
||||
|
||||
var Pull = models.PullRequest{
|
||||
Num: 1,
|
||||
@@ -50,3 +55,11 @@ var GitlabRepo = models.Repo{
|
||||
var User = models.User{
|
||||
Username: "lkysow",
|
||||
}
|
||||
|
||||
var projectName = "test-project"
|
||||
|
||||
var Project = valid.Project{
|
||||
Name: &projectName,
|
||||
}
|
||||
|
||||
var PullInfo = fmt.Sprintf("%s/%d/%s", GithubRepo.FullName, Pull.Num, *Project.Name)
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
const (
|
||||
planfileSlashReplace = "::"
|
||||
LogStreamingClearMsg = "\n-----Starting New Process-----"
|
||||
)
|
||||
|
||||
type PullReqStatus struct {
|
||||
@@ -422,6 +423,23 @@ func (p ProjectCommandContext) GetShowResultFileName() string {
|
||||
return fmt.Sprintf("%s-%s.json", projName, p.Workspace)
|
||||
}
|
||||
|
||||
// Gets a unique identifier for the current pull request as a single string
|
||||
func (p ProjectCommandContext) PullInfo() string {
|
||||
return BuildPullInfo(p.BaseRepo.FullName, p.Pull.Num, p.ProjectName, p.RepoRelDir, p.Workspace)
|
||||
}
|
||||
|
||||
func BuildPullInfo(repoName string, pullNum int, projectName string, relDir string, workspace string) string {
|
||||
projectIdentifier := GetProjectIdentifier(relDir, projectName)
|
||||
return fmt.Sprintf("%s/%d/%s/%s", repoName, pullNum, projectIdentifier, workspace)
|
||||
}
|
||||
|
||||
func GetProjectIdentifier(relRepoDir string, projectName string) string {
|
||||
if projectName != "" {
|
||||
return projectName
|
||||
}
|
||||
return strings.ReplaceAll(relRepoDir, "/", "-")
|
||||
}
|
||||
|
||||
// SplitRepoFullName splits a repo full name up into its owner and repo
|
||||
// name segments. If the repoFullName is malformed, may return empty
|
||||
// strings for owner or repo.
|
||||
@@ -665,6 +683,14 @@ func (c CommandName) TitleString() string {
|
||||
return strings.Title(strings.ReplaceAll(strings.ToLower(c.String()), "_", " "))
|
||||
}
|
||||
|
||||
type ProjectCmdOutputLine struct {
|
||||
ProjectInfo string
|
||||
|
||||
Line string
|
||||
|
||||
ClearBuffBefore bool
|
||||
}
|
||||
|
||||
// String returns the string representation of c.
|
||||
func (c CommandName) String() string {
|
||||
switch c {
|
||||
|
||||
@@ -20,9 +20,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/core/runtime"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
@@ -113,6 +115,50 @@ type ProjectCommandRunner interface {
|
||||
ProjectVersionCommandRunner
|
||||
}
|
||||
|
||||
// ProjectOutputWrapper is a decorator that creates a new PR status check per project.
|
||||
// The status contains a url that outputs current progress of the terraform plan/apply command.
|
||||
type ProjectOutputWrapper struct {
|
||||
ProjectCommandRunner
|
||||
ProjectCmdOutputHandler handlers.ProjectCommandOutputHandler
|
||||
}
|
||||
|
||||
func (p *ProjectOutputWrapper) Plan(ctx models.ProjectCommandContext) models.ProjectResult {
|
||||
// Reset the buffer when running the plan. We only need to do this for plan,
|
||||
// apply is a continuation of the same workflow
|
||||
p.ProjectCmdOutputHandler.Clear(ctx)
|
||||
return p.updateProjectPRStatus(models.PlanCommand, ctx, p.ProjectCommandRunner.Plan)
|
||||
}
|
||||
|
||||
func (p *ProjectOutputWrapper) Apply(ctx models.ProjectCommandContext) models.ProjectResult {
|
||||
return p.updateProjectPRStatus(models.ApplyCommand, ctx, p.ProjectCommandRunner.Apply)
|
||||
}
|
||||
|
||||
func (p *ProjectOutputWrapper) updateProjectPRStatus(commandName models.CommandName, ctx models.ProjectCommandContext, execute func(ctx models.ProjectCommandContext) models.ProjectResult) models.ProjectResult {
|
||||
// Create a PR status to track project's plan status. The status will
|
||||
// include a link to view the progress of atlantis plan command in real
|
||||
// time
|
||||
if err := p.ProjectCmdOutputHandler.SetJobURLWithStatus(ctx, commandName, models.PendingCommitStatus); err != nil {
|
||||
ctx.Log.Err("updating project PR status", err)
|
||||
}
|
||||
|
||||
// ensures we are differentiating between project level command and overall command
|
||||
result := execute(ctx)
|
||||
|
||||
if result.Error != nil || result.Failure != "" {
|
||||
if err := p.ProjectCmdOutputHandler.SetJobURLWithStatus(ctx, commandName, models.FailedCommitStatus); err != nil {
|
||||
ctx.Log.Err("updating project PR status", err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if err := p.ProjectCmdOutputHandler.SetJobURLWithStatus(ctx, commandName, models.SuccessCommitStatus); err != nil {
|
||||
ctx.Log.Err("updating project PR status", err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// DefaultProjectCommandRunner implements ProjectCommandRunner.
|
||||
type DefaultProjectCommandRunner struct {
|
||||
Locker ProjectLocker
|
||||
@@ -125,6 +171,7 @@ type DefaultProjectCommandRunner struct {
|
||||
VersionStepRunner StepRunner
|
||||
RunStepRunner CustomStepRunner
|
||||
EnvStepRunner EnvStepRunner
|
||||
PullApprovedChecker runtime.PullApprovedChecker
|
||||
WorkingDir WorkingDir
|
||||
Webhooks WebhooksSender
|
||||
WorkingDirLocker WorkingDirLocker
|
||||
@@ -312,6 +359,7 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) (
|
||||
}
|
||||
|
||||
outputs, err := p.runSteps(ctx.Steps, ctx, projAbsPath)
|
||||
|
||||
if err != nil {
|
||||
if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {
|
||||
ctx.Log.Err("error unlocking state after plan error: %v", unlockErr)
|
||||
@@ -354,6 +402,7 @@ func (p *DefaultProjectCommandRunner) doApply(ctx models.ProjectCommandContext)
|
||||
defer unlockFn()
|
||||
|
||||
outputs, err := p.runSteps(ctx.Steps, ctx, absPath)
|
||||
|
||||
p.Webhooks.Send(ctx.Log, webhooks.ApplyResult{ // nolint: errcheck
|
||||
Workspace: ctx.Workspace,
|
||||
User: ctx.User,
|
||||
@@ -362,9 +411,11 @@ func (p *DefaultProjectCommandRunner) doApply(ctx models.ProjectCommandContext)
|
||||
Success: err == nil,
|
||||
Directory: ctx.RepoRelDir,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("%s\n%s", err, strings.Join(outputs, "\n"))
|
||||
}
|
||||
|
||||
return strings.Join(outputs, "\n"), "", nil
|
||||
}
|
||||
|
||||
@@ -398,6 +449,7 @@ func (p *DefaultProjectCommandRunner) doVersion(ctx models.ProjectCommandContext
|
||||
|
||||
func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) {
|
||||
var outputs []string
|
||||
|
||||
envs := make(map[string]string)
|
||||
for _, step := range steps {
|
||||
var out string
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -26,6 +28,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
handlermocks "github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
@@ -50,6 +53,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
|
||||
ApplyStepRunner: mockApply,
|
||||
RunStepRunner: mockRun,
|
||||
EnvStepRunner: &realEnv,
|
||||
PullApprovedChecker: nil,
|
||||
WorkingDir: mockWorkingDir,
|
||||
Webhooks: nil,
|
||||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||||
@@ -112,7 +116,6 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
|
||||
Assert(t, res.PlanSuccess != nil, "exp plan success")
|
||||
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
|
||||
Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput)
|
||||
|
||||
expSteps := []string{"run", "apply", "plan", "init", "env"}
|
||||
for _, step := range expSteps {
|
||||
switch step {
|
||||
@@ -128,6 +131,112 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectOutputWrapper(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Steps: []valid.Step{
|
||||
{
|
||||
StepName: "plan",
|
||||
},
|
||||
},
|
||||
Workspace: "default",
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Description string
|
||||
Failure bool
|
||||
Error bool
|
||||
Success bool
|
||||
CommandName models.CommandName
|
||||
}{
|
||||
{
|
||||
Description: "plan success",
|
||||
Success: true,
|
||||
CommandName: models.PlanCommand,
|
||||
},
|
||||
{
|
||||
Description: "plan failure",
|
||||
Failure: true,
|
||||
CommandName: models.PlanCommand,
|
||||
},
|
||||
{
|
||||
Description: "plan error",
|
||||
Error: true,
|
||||
CommandName: models.PlanCommand,
|
||||
},
|
||||
{
|
||||
Description: "apply success",
|
||||
Success: true,
|
||||
CommandName: models.ApplyCommand,
|
||||
},
|
||||
{
|
||||
Description: "apply failure",
|
||||
Failure: true,
|
||||
CommandName: models.ApplyCommand,
|
||||
},
|
||||
{
|
||||
Description: "apply error",
|
||||
Error: true,
|
||||
CommandName: models.ApplyCommand,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Description, func(t *testing.T) {
|
||||
var prjResult models.ProjectResult
|
||||
var expCommitStatus models.CommitStatus
|
||||
|
||||
mockProjectCommandOutputHandler := handlermocks.NewMockProjectCommandOutputHandler()
|
||||
mockProjectCommandRunner := mocks.NewMockProjectCommandRunner()
|
||||
|
||||
runner := &events.ProjectOutputWrapper{
|
||||
ProjectCmdOutputHandler: mockProjectCommandOutputHandler,
|
||||
ProjectCommandRunner: mockProjectCommandRunner,
|
||||
}
|
||||
|
||||
if c.Success {
|
||||
prjResult = models.ProjectResult{
|
||||
PlanSuccess: &models.PlanSuccess{},
|
||||
ApplySuccess: "exists",
|
||||
}
|
||||
expCommitStatus = models.SuccessCommitStatus
|
||||
} else if c.Failure {
|
||||
prjResult = models.ProjectResult{
|
||||
Failure: "failure",
|
||||
}
|
||||
expCommitStatus = models.FailedCommitStatus
|
||||
} else if c.Error {
|
||||
prjResult = models.ProjectResult{
|
||||
Error: errors.New("error"),
|
||||
}
|
||||
expCommitStatus = models.FailedCommitStatus
|
||||
}
|
||||
|
||||
When(mockProjectCommandRunner.Plan(matchers.AnyModelsProjectCommandContext())).ThenReturn(prjResult)
|
||||
When(mockProjectCommandRunner.Apply(matchers.AnyModelsProjectCommandContext())).ThenReturn(prjResult)
|
||||
|
||||
switch c.CommandName {
|
||||
case models.PlanCommand:
|
||||
runner.Plan(ctx)
|
||||
case models.ApplyCommand:
|
||||
runner.Apply(ctx)
|
||||
}
|
||||
|
||||
mockProjectCommandOutputHandler.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, models.PendingCommitStatus)
|
||||
mockProjectCommandOutputHandler.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, expCommitStatus)
|
||||
|
||||
switch c.CommandName {
|
||||
case models.PlanCommand:
|
||||
mockProjectCommandRunner.VerifyWasCalledOnce().Plan(ctx)
|
||||
case models.ApplyCommand:
|
||||
mockProjectCommandRunner.VerifyWasCalledOnce().Apply(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test what happens if there's no working dir. This signals that the project
|
||||
// was never planned.
|
||||
func TestDefaultProjectCommandRunner_ApplyNotCloned(t *testing.T) {
|
||||
@@ -370,6 +479,54 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test that it runs the expected apply steps.
|
||||
func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
mockApply := mocks.NewMockStepRunner()
|
||||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||||
mockLocker := mocks.NewMockProjectLocker()
|
||||
mockSender := mocks.NewMockWebhooksSender()
|
||||
applyReqHandler := &events.AggregateApplyRequirements{
|
||||
WorkingDir: mockWorkingDir,
|
||||
}
|
||||
|
||||
runner := events.DefaultProjectCommandRunner{
|
||||
Locker: mockLocker,
|
||||
LockURLGenerator: mockURLGenerator{},
|
||||
ApplyStepRunner: mockApply,
|
||||
WorkingDir: mockWorkingDir,
|
||||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||||
AggregateApplyRequirements: applyReqHandler,
|
||||
Webhooks: mockSender,
|
||||
}
|
||||
repoDir, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
When(mockWorkingDir.GetWorkingDir(
|
||||
matchers.AnyModelsRepo(),
|
||||
matchers.AnyModelsPullRequest(),
|
||||
AnyString(),
|
||||
)).ThenReturn(repoDir, nil)
|
||||
|
||||
ctx := models.ProjectCommandContext{
|
||||
Log: logging.NewNoopLogger(t),
|
||||
Steps: []valid.Step{
|
||||
{
|
||||
StepName: "apply",
|
||||
},
|
||||
},
|
||||
Workspace: "default",
|
||||
ApplyRequirements: []string{},
|
||||
RepoRelDir: ".",
|
||||
}
|
||||
expEnvs := map[string]string{}
|
||||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", fmt.Errorf("something went wrong"))
|
||||
|
||||
res := runner.Apply(ctx)
|
||||
Assert(t, res.ApplySuccess == "", "exp apply failure")
|
||||
|
||||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||||
}
|
||||
|
||||
// Test run and env steps. We don't use mocks for this test since we're
|
||||
// not running any Terraform.
|
||||
func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {
|
||||
|
||||
@@ -16,6 +16,7 @@ package events
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
@@ -28,6 +29,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/core/locking"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_pull_cleaner.go PullCleaner
|
||||
@@ -42,11 +44,13 @@ type PullCleaner interface {
|
||||
// PullClosedExecutor executes the tasks required to clean up a closed pull
|
||||
// request.
|
||||
type PullClosedExecutor struct {
|
||||
Locker locking.Locker
|
||||
VCSClient vcs.Client
|
||||
WorkingDir WorkingDir
|
||||
Logger logging.SimpleLogging
|
||||
DB *db.BoltDB
|
||||
Locker locking.Locker
|
||||
VCSClient vcs.Client
|
||||
WorkingDir WorkingDir
|
||||
Logger logging.SimpleLogging
|
||||
DB *db.BoltDB
|
||||
PullClosedTemplate PullCleanupTemplate
|
||||
LogStreamResourceCleaner handlers.ResourceCleaner
|
||||
}
|
||||
|
||||
type templatedProject struct {
|
||||
@@ -59,8 +63,31 @@ var pullClosedTemplate = template.Must(template.New("").Parse(
|
||||
"{{ range . }}\n" +
|
||||
"- dir: `{{ .RepoRelDir }}` {{ .Workspaces }}{{ end }}"))
|
||||
|
||||
type PullCleanupTemplate interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
}
|
||||
|
||||
type PullClosedEventTemplate struct{}
|
||||
|
||||
func (t *PullClosedEventTemplate) Execute(wr io.Writer, data interface{}) error {
|
||||
return pullClosedTemplate.Execute(wr, data)
|
||||
}
|
||||
|
||||
// CleanUpPull cleans up after a closed pull request.
|
||||
func (p *PullClosedExecutor) CleanUpPull(repo models.Repo, pull models.PullRequest) error {
|
||||
pullStatus, err := p.DB.GetPullStatus(pull)
|
||||
if err != nil {
|
||||
// Log and continue to clean up other resources.
|
||||
p.Logger.Err("retrieving pull status: %s", err)
|
||||
}
|
||||
|
||||
if pullStatus != nil {
|
||||
for _, project := range pullStatus.Projects {
|
||||
projectKey := models.BuildPullInfo(pullStatus.Pull.BaseRepo.FullName, pull.Num, project.ProjectName, project.RepoRelDir, project.Workspace)
|
||||
p.LogStreamResourceCleaner.CleanUp(projectKey)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.WorkingDir.Delete(repo, pull); err != nil {
|
||||
return errors.Wrap(err, "cleaning workspace")
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/core/db"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
. "github.com/petergtz/pegomock"
|
||||
lockmocks "github.com/runatlantis/atlantis/server/core/locking/mocks"
|
||||
@@ -27,6 +31,8 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/models/fixtures"
|
||||
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
|
||||
handlermocks "github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
loggermocks "github.com/runatlantis/atlantis/server/logging/mocks"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
@@ -34,10 +40,16 @@ func TestCleanUpPullWorkspaceErr(t *testing.T) {
|
||||
t.Log("when workspace.Delete returns an error, we return it")
|
||||
RegisterMockTestingT(t)
|
||||
w := mocks.NewMockWorkingDir()
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
db, err := db.New(tmp)
|
||||
Ok(t, err)
|
||||
pce := events.PullClosedExecutor{
|
||||
WorkingDir: w,
|
||||
WorkingDir: w,
|
||||
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
||||
DB: db,
|
||||
}
|
||||
err := errors.New("err")
|
||||
err = errors.New("err")
|
||||
When(w.Delete(fixtures.GithubRepo, fixtures.Pull)).ThenReturn(err)
|
||||
actualErr := pce.CleanUpPull(fixtures.GithubRepo, fixtures.Pull)
|
||||
Equals(t, "cleaning workspace: err", actualErr.Error())
|
||||
@@ -48,11 +60,17 @@ func TestCleanUpPullUnlockErr(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
w := mocks.NewMockWorkingDir()
|
||||
l := lockmocks.NewMockLocker()
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
db, err := db.New(tmp)
|
||||
Ok(t, err)
|
||||
pce := events.PullClosedExecutor{
|
||||
Locker: l,
|
||||
WorkingDir: w,
|
||||
Locker: l,
|
||||
WorkingDir: w,
|
||||
DB: db,
|
||||
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
||||
}
|
||||
err := errors.New("err")
|
||||
err = errors.New("err")
|
||||
When(l.UnlockByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(nil, err)
|
||||
actualErr := pce.CleanUpPull(fixtures.GithubRepo, fixtures.Pull)
|
||||
Equals(t, "cleaning up locks: err", actualErr.Error())
|
||||
@@ -171,3 +189,100 @@ func TestCleanUpPullComments(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanUpLogStreaming(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
t.Run("Should Clean Up Log Streaming Resources When PR is closed", func(t *testing.T) {
|
||||
prjStatusUpdater := handlermocks.NewMockProjectStatusUpdater()
|
||||
prjJobURLGenerator := handlermocks.NewMockProjectJobURLGenerator()
|
||||
|
||||
// Create Log streaming resources
|
||||
prjCmdOutput := make(chan *models.ProjectCmdOutputLine)
|
||||
prjCmdOutHandler := handlers.NewAsyncProjectCommandOutputHandler(prjCmdOutput, prjStatusUpdater, prjJobURLGenerator, logger)
|
||||
ctx := models.ProjectCommandContext{
|
||||
BaseRepo: fixtures.GithubRepo,
|
||||
Pull: fixtures.Pull,
|
||||
ProjectName: *fixtures.Project.Name,
|
||||
Workspace: "default",
|
||||
}
|
||||
|
||||
go prjCmdOutHandler.Handle()
|
||||
prjCmdOutHandler.Send(ctx, "Test Message")
|
||||
|
||||
// Create boltdb and add pull request.
|
||||
var lockBucket = "bucket"
|
||||
var configBucket = "configBucket"
|
||||
var pullsBucketName = "pulls"
|
||||
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "failed to create temp file"))
|
||||
}
|
||||
path := f.Name()
|
||||
f.Close() // nolint: errcheck
|
||||
|
||||
// Open the database.
|
||||
boltDB, err := bolt.Open(path, 0600, nil)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not start bolt DB"))
|
||||
}
|
||||
if err := boltDB.Update(func(tx *bolt.Tx) error {
|
||||
if _, err := tx.CreateBucketIfNotExists([]byte(pullsBucketName)); err != nil {
|
||||
return errors.Wrap(err, "failed to create bucket")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
panic(errors.Wrap(err, "could not create bucket"))
|
||||
}
|
||||
db, _ := db.NewWithDB(boltDB, lockBucket, configBucket)
|
||||
result := []models.ProjectResult{
|
||||
{
|
||||
RepoRelDir: fixtures.GithubRepo.FullName,
|
||||
Workspace: "default",
|
||||
ProjectName: *fixtures.Project.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new record for pull
|
||||
_, err = db.UpdatePullWithResults(fixtures.Pull, result)
|
||||
Ok(t, err)
|
||||
|
||||
workingDir := mocks.NewMockWorkingDir()
|
||||
locker := lockmocks.NewMockLocker()
|
||||
client := vcsmocks.NewMockClient()
|
||||
logger := loggermocks.NewMockSimpleLogging()
|
||||
|
||||
pullClosedExecutor := events.PullClosedExecutor{
|
||||
Locker: locker,
|
||||
WorkingDir: workingDir,
|
||||
DB: db,
|
||||
VCSClient: client,
|
||||
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
||||
LogStreamResourceCleaner: prjCmdOutHandler,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
locks := []models.ProjectLock{
|
||||
{
|
||||
Project: models.NewProject(fixtures.GithubRepo.FullName, ""),
|
||||
Workspace: "default",
|
||||
},
|
||||
}
|
||||
When(locker.UnlockByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(locks, nil)
|
||||
|
||||
// Clean up.
|
||||
err = pullClosedExecutor.CleanUpPull(fixtures.GithubRepo, fixtures.Pull)
|
||||
Ok(t, err)
|
||||
|
||||
close(prjCmdOutput)
|
||||
_, _, comment, _ := client.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString()).GetCapturedArguments()
|
||||
expectedComment := "Locks and plans deleted for the projects and workspaces modified in this pull request:\n\n" + "- dir: `.` workspace: `default`"
|
||||
Equals(t, expectedComment, comment)
|
||||
|
||||
// Assert log streaming resources are cleaned up.
|
||||
dfPrjCmdOutputHandler := prjCmdOutHandler.(*handlers.AsyncProjectCommandOutputHandler)
|
||||
assert.Empty(t, dfPrjCmdOutputHandler.GetProjectOutputBuffer(ctx.PullInfo()))
|
||||
assert.Empty(t, dfPrjCmdOutputHandler.GetReceiverBufferForPull(ctx.PullInfo()))
|
||||
})
|
||||
}
|
||||
|
||||
13
server/events/terraform/ansi/strip.go
Normal file
13
server/events/terraform/ansi/strip.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package ansi
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
|
||||
|
||||
var re = regexp.MustCompile(ansi)
|
||||
|
||||
func Strip(str string) string {
|
||||
return re.ReplaceAllString(str, "")
|
||||
}
|
||||
31
server/handlers/mocks/matchers/chan_of_string.go
Normal file
31
server/handlers/mocks/matchers/chan_of_string.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func AnyChanOfString() chan string {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(chan string))(nil)).Elem()))
|
||||
var nullValue chan string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqChanOfString(value chan string) chan string {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue chan string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqChanOfString(value chan string) chan string {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue chan string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func ChanOfStringThat(matcher pegomock.ArgumentMatcher) chan string {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue chan string
|
||||
return nullValue
|
||||
}
|
||||
33
server/handlers/mocks/matchers/http_header.go
Normal file
33
server/handlers/mocks/matchers/http_header.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
http "net/http"
|
||||
)
|
||||
|
||||
func AnyHttpHeader() http.Header {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(http.Header))(nil)).Elem()))
|
||||
var nullValue http.Header
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqHttpHeader(value http.Header) http.Header {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue http.Header
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqHttpHeader(value http.Header) http.Header {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue http.Header
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func HttpHeaderThat(matcher pegomock.ArgumentMatcher) http.Header {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue http.Header
|
||||
return nullValue
|
||||
}
|
||||
33
server/handlers/mocks/matchers/http_responsewriter.go
Normal file
33
server/handlers/mocks/matchers/http_responsewriter.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
http "net/http"
|
||||
)
|
||||
|
||||
func AnyHttpResponseWriter() http.ResponseWriter {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(http.ResponseWriter))(nil)).Elem()))
|
||||
var nullValue http.ResponseWriter
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqHttpResponseWriter(value http.ResponseWriter) http.ResponseWriter {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue http.ResponseWriter
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqHttpResponseWriter(value http.ResponseWriter) http.ResponseWriter {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue http.ResponseWriter
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func HttpResponseWriterThat(matcher pegomock.ArgumentMatcher) http.ResponseWriter {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue http.ResponseWriter
|
||||
return nullValue
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func AnyMapOfChanOfStringToBool() map[chan string]bool {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(map[chan string]bool))(nil)).Elem()))
|
||||
var nullValue map[chan string]bool
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqMapOfChanOfStringToBool(value map[chan string]bool) map[chan string]bool {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue map[chan string]bool
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqMapOfChanOfStringToBool(value map[chan string]bool) map[chan string]bool {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue map[chan string]bool
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func MapOfChanOfStringToBoolThat(matcher pegomock.ArgumentMatcher) map[chan string]bool {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue map[chan string]bool
|
||||
return nullValue
|
||||
}
|
||||
33
server/handlers/mocks/matchers/models_commandname.go
Normal file
33
server/handlers/mocks/matchers/models_commandname.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
func AnyModelsCommandName() models.CommandName {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.CommandName))(nil)).Elem()))
|
||||
var nullValue models.CommandName
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqModelsCommandName(value models.CommandName) models.CommandName {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue models.CommandName
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqModelsCommandName(value models.CommandName) models.CommandName {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue models.CommandName
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func ModelsCommandNameThat(matcher pegomock.ArgumentMatcher) models.CommandName {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue models.CommandName
|
||||
return nullValue
|
||||
}
|
||||
33
server/handlers/mocks/matchers/models_commitstatus.go
Normal file
33
server/handlers/mocks/matchers/models_commitstatus.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
func AnyModelsCommitStatus() models.CommitStatus {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.CommitStatus))(nil)).Elem()))
|
||||
var nullValue models.CommitStatus
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqModelsCommitStatus(value models.CommitStatus) models.CommitStatus {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue models.CommitStatus
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqModelsCommitStatus(value models.CommitStatus) models.CommitStatus {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue models.CommitStatus
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func ModelsCommitStatusThat(matcher pegomock.ArgumentMatcher) models.CommitStatus {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue models.CommitStatus
|
||||
return nullValue
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
func AnyModelsProjectCommandContext() models.ProjectCommandContext {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.ProjectCommandContext))(nil)).Elem()))
|
||||
var nullValue models.ProjectCommandContext
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqModelsProjectCommandContext(value models.ProjectCommandContext) models.ProjectCommandContext {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue models.ProjectCommandContext
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqModelsProjectCommandContext(value models.ProjectCommandContext) models.ProjectCommandContext {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue models.ProjectCommandContext
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func ModelsProjectCommandContextThat(matcher pegomock.ArgumentMatcher) models.ProjectCommandContext {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue models.ProjectCommandContext
|
||||
return nullValue
|
||||
}
|
||||
33
server/handlers/mocks/matchers/ptr_to_http_request.go
Normal file
33
server/handlers/mocks/matchers/ptr_to_http_request.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
|
||||
http "net/http"
|
||||
)
|
||||
|
||||
func AnyPtrToHttpRequest() *http.Request {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*http.Request))(nil)).Elem()))
|
||||
var nullValue *http.Request
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqPtrToHttpRequest(value *http.Request) *http.Request {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue *http.Request
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqPtrToHttpRequest(value *http.Request) *http.Request {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue *http.Request
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func PtrToHttpRequestThat(matcher pegomock.ArgumentMatcher) *http.Request {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue *http.Request
|
||||
return nullValue
|
||||
}
|
||||
31
server/handlers/mocks/matchers/slice_of_byte.go
Normal file
31
server/handlers/mocks/matchers/slice_of_byte.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func AnySliceOfByte() []byte {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]byte))(nil)).Elem()))
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqSliceOfByte(value []byte) []byte {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqSliceOfByte(value []byte) []byte {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func SliceOfByteThat(matcher pegomock.ArgumentMatcher) []byte {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
31
server/handlers/mocks/matchers/slice_of_string.go
Normal file
31
server/handlers/mocks/matchers/slice_of_string.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func AnySliceOfString() []string {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]string))(nil)).Elem()))
|
||||
var nullValue []string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqSliceOfString(value []string) []string {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue []string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func NotEqSliceOfString(value []string) []string {
|
||||
pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
|
||||
var nullValue []string
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func SliceOfStringThat(matcher pegomock.ArgumentMatcher) []string {
|
||||
pegomock.RegisterMatcher(matcher)
|
||||
var nullValue []string
|
||||
return nullValue
|
||||
}
|
||||
325
server/handlers/mocks/mock_project_command_output_handler.go
Normal file
325
server/handlers/mocks/mock_project_command_output_handler.go
Normal file
@@ -0,0 +1,325 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/handlers (interfaces: ProjectCommandOutputHandler)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockProjectCommandOutputHandler struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockProjectCommandOutputHandler(options ...pegomock.Option) *MockProjectCommandOutputHandler {
|
||||
mock := &MockProjectCommandOutputHandler{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockProjectCommandOutputHandler) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) CleanUp(_param0 string) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("CleanUp", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) Clear(_param0 models.ProjectCommandContext) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Clear", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) Deregister(_param0 string, _param1 chan string) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Deregister", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) Handle() {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Handle", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) Register(_param0 string, _param1 chan string) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Register", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) Send(_param0 models.ProjectCommandContext, _param1 string) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Send", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) SetJobURLWithStatus(_param0 models.ProjectCommandContext, _param1 models.CommandName, _param2 models.CommitStatus) error {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectCommandOutputHandler().")
|
||||
}
|
||||
params := []pegomock.Param{_param0, _param1, _param2}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SetJobURLWithStatus", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||
var ret0 error
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(error)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) VerifyWasCalledOnce() *VerifierMockProjectCommandOutputHandler {
|
||||
return &VerifierMockProjectCommandOutputHandler{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectCommandOutputHandler {
|
||||
return &VerifierMockProjectCommandOutputHandler{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectCommandOutputHandler {
|
||||
return &VerifierMockProjectCommandOutputHandler{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectCommandOutputHandler) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectCommandOutputHandler {
|
||||
return &VerifierMockProjectCommandOutputHandler{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockProjectCommandOutputHandler struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) CleanUp(_param0 string) *MockProjectCommandOutputHandler_CleanUp_OngoingVerification {
|
||||
params := []pegomock.Param{_param0}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CleanUp", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_CleanUp_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_CleanUp_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_CleanUp_OngoingVerification) GetCapturedArguments() string {
|
||||
_param0 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_CleanUp_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) Clear(_param0 models.ProjectCommandContext) *MockProjectCommandOutputHandler_Clear_OngoingVerification {
|
||||
params := []pegomock.Param{_param0}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Clear", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_Clear_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_Clear_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Clear_OngoingVerification) GetCapturedArguments() models.ProjectCommandContext {
|
||||
_param0 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Clear_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) Deregister(_param0 string, _param1 chan string) *MockProjectCommandOutputHandler_Deregister_OngoingVerification {
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Deregister", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_Deregister_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_Deregister_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Deregister_OngoingVerification) GetCapturedArguments() (string, chan string) {
|
||||
_param0, _param1 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Deregister_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []chan string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(string)
|
||||
}
|
||||
_param1 = make([]chan string, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(chan string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) Handle() *MockProjectCommandOutputHandler_Handle_OngoingVerification {
|
||||
params := []pegomock.Param{}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Handle", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_Handle_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_Handle_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Handle_OngoingVerification) GetCapturedArguments() {
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Handle_OngoingVerification) GetAllCapturedArguments() {
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) Register(_param0 string, _param1 chan string) *MockProjectCommandOutputHandler_Register_OngoingVerification {
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Register", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_Register_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_Register_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Register_OngoingVerification) GetCapturedArguments() (string, chan string) {
|
||||
_param0, _param1 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Register_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []chan string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(string)
|
||||
}
|
||||
_param1 = make([]chan string, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(chan string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) Send(_param0 models.ProjectCommandContext, _param1 string) *MockProjectCommandOutputHandler_Send_OngoingVerification {
|
||||
params := []pegomock.Param{_param0, _param1}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Send", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_Send_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Send_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string) {
|
||||
_param0, _param1 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1], _param1[len(_param1)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
_param1 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectCommandOutputHandler) SetJobURLWithStatus(_param0 models.ProjectCommandContext, _param1 models.CommandName, _param2 models.CommitStatus) *MockProjectCommandOutputHandler_SetJobURLWithStatus_OngoingVerification {
|
||||
params := []pegomock.Param{_param0, _param1, _param2}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SetJobURLWithStatus", params, verifier.timeout)
|
||||
return &MockProjectCommandOutputHandler_SetJobURLWithStatus_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectCommandOutputHandler_SetJobURLWithStatus_OngoingVerification struct {
|
||||
mock *MockProjectCommandOutputHandler
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_SetJobURLWithStatus_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, models.CommandName, models.CommitStatus) {
|
||||
_param0, _param1, _param2 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectCommandOutputHandler_SetJobURLWithStatus_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []models.CommandName, _param2 []models.CommitStatus) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
_param1 = make([]models.CommandName, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(models.CommandName)
|
||||
}
|
||||
_param2 = make([]models.CommitStatus, len(c.methodInvocations))
|
||||
for u, param := range params[2] {
|
||||
_param2[u] = param.(models.CommitStatus)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
109
server/handlers/mocks/mock_project_job_url_generator.go
Normal file
109
server/handlers/mocks/mock_project_job_url_generator.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/handlers (interfaces: ProjectJobURLGenerator)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockProjectJobURLGenerator struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockProjectJobURLGenerator(options ...pegomock.Option) *MockProjectJobURLGenerator {
|
||||
mock := &MockProjectJobURLGenerator{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockProjectJobURLGenerator) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) GenerateProjectJobURL(p models.ProjectCommandContext) (string, error) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectJobURLGenerator().")
|
||||
}
|
||||
params := []pegomock.Param{p}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("GenerateProjectJobURL", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||
var ret0 string
|
||||
var ret1 error
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(string)
|
||||
}
|
||||
if result[1] != nil {
|
||||
ret1 = result[1].(error)
|
||||
}
|
||||
}
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) VerifyWasCalledOnce() *VerifierMockProjectJobURLGenerator {
|
||||
return &VerifierMockProjectJobURLGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectJobURLGenerator {
|
||||
return &VerifierMockProjectJobURLGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectJobURLGenerator {
|
||||
return &VerifierMockProjectJobURLGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectJobURLGenerator) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectJobURLGenerator {
|
||||
return &VerifierMockProjectJobURLGenerator{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockProjectJobURLGenerator struct {
|
||||
mock *MockProjectJobURLGenerator
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectJobURLGenerator) GenerateProjectJobURL(p models.ProjectCommandContext) *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification {
|
||||
params := []pegomock.Param{p}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GenerateProjectJobURL", params, verifier.timeout)
|
||||
return &MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification struct {
|
||||
mock *MockProjectJobURLGenerator
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification) GetCapturedArguments() models.ProjectCommandContext {
|
||||
p := c.GetAllCapturedArguments()
|
||||
return p[len(p)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectJobURLGenerator_GenerateProjectJobURL_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
117
server/handlers/mocks/mock_project_status_updater.go
Normal file
117
server/handlers/mocks/mock_project_status_updater.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/handlers (interfaces: ProjectStatusUpdater)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockProjectStatusUpdater struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockProjectStatusUpdater(options ...pegomock.Option) *MockProjectStatusUpdater {
|
||||
mock := &MockProjectStatusUpdater{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockProjectStatusUpdater) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockProjectStatusUpdater) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockProjectStatusUpdater) UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) error {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockProjectStatusUpdater().")
|
||||
}
|
||||
params := []pegomock.Param{ctx, cmdName, status, url}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateProject", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
|
||||
var ret0 error
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(error)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockProjectStatusUpdater) VerifyWasCalledOnce() *VerifierMockProjectStatusUpdater {
|
||||
return &VerifierMockProjectStatusUpdater{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectStatusUpdater) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockProjectStatusUpdater {
|
||||
return &VerifierMockProjectStatusUpdater{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectStatusUpdater) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockProjectStatusUpdater {
|
||||
return &VerifierMockProjectStatusUpdater{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockProjectStatusUpdater) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockProjectStatusUpdater {
|
||||
return &VerifierMockProjectStatusUpdater{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockProjectStatusUpdater struct {
|
||||
mock *MockProjectStatusUpdater
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockProjectStatusUpdater) UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) *MockProjectStatusUpdater_UpdateProject_OngoingVerification {
|
||||
params := []pegomock.Param{ctx, cmdName, status, url}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateProject", params, verifier.timeout)
|
||||
return &MockProjectStatusUpdater_UpdateProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockProjectStatusUpdater_UpdateProject_OngoingVerification struct {
|
||||
mock *MockProjectStatusUpdater
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockProjectStatusUpdater_UpdateProject_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, models.CommandName, models.CommitStatus, string) {
|
||||
ctx, cmdName, status, url := c.GetAllCapturedArguments()
|
||||
return ctx[len(ctx)-1], cmdName[len(cmdName)-1], status[len(status)-1], url[len(url)-1]
|
||||
}
|
||||
|
||||
func (c *MockProjectStatusUpdater_UpdateProject_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []models.CommandName, _param2 []models.CommitStatus, _param3 []string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.ProjectCommandContext, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.ProjectCommandContext)
|
||||
}
|
||||
_param1 = make([]models.CommandName, len(c.methodInvocations))
|
||||
for u, param := range params[1] {
|
||||
_param1[u] = param.(models.CommandName)
|
||||
}
|
||||
_param2 = make([]models.CommitStatus, len(c.methodInvocations))
|
||||
for u, param := range params[2] {
|
||||
_param2[u] = param.(models.CommitStatus)
|
||||
}
|
||||
_param3 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[3] {
|
||||
_param3[u] = param.(string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
97
server/handlers/mocks/mock_resource_cleaner.go
Normal file
97
server/handlers/mocks/mock_resource_cleaner.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/handlers (interfaces: ResourceCleaner)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockResourceCleaner struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockResourceCleaner(options ...pegomock.Option) *MockResourceCleaner {
|
||||
mock := &MockResourceCleaner{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockResourceCleaner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockResourceCleaner) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockResourceCleaner) CleanUp(_param0 string) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockResourceCleaner().")
|
||||
}
|
||||
params := []pegomock.Param{_param0}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("CleanUp", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockResourceCleaner) VerifyWasCalledOnce() *VerifierMockResourceCleaner {
|
||||
return &VerifierMockResourceCleaner{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockResourceCleaner) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockResourceCleaner {
|
||||
return &VerifierMockResourceCleaner{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockResourceCleaner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockResourceCleaner {
|
||||
return &VerifierMockResourceCleaner{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockResourceCleaner) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockResourceCleaner {
|
||||
return &VerifierMockResourceCleaner{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockResourceCleaner struct {
|
||||
mock *MockResourceCleaner
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockResourceCleaner) CleanUp(_param0 string) *MockResourceCleaner_CleanUp_OngoingVerification {
|
||||
params := []pegomock.Param{_param0}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CleanUp", params, verifier.timeout)
|
||||
return &MockResourceCleaner_CleanUp_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockResourceCleaner_CleanUp_OngoingVerification struct {
|
||||
mock *MockResourceCleaner
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockResourceCleaner_CleanUp_OngoingVerification) GetCapturedArguments() string {
|
||||
_param0 := c.GetAllCapturedArguments()
|
||||
return _param0[len(_param0)-1]
|
||||
}
|
||||
|
||||
func (c *MockResourceCleaner_CleanUp_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]string, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(string)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
232
server/handlers/project_command_output_handler.go
Normal file
232
server/handlers/project_command_output_handler.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
// AsyncProjectCommandOutputHandler is a handler to transport terraform client
|
||||
// outputs to the front end.
|
||||
type AsyncProjectCommandOutputHandler struct {
|
||||
projectCmdOutput chan *models.ProjectCmdOutputLine
|
||||
|
||||
projectOutputBuffers map[string][]string
|
||||
projectOutputBuffersLock sync.RWMutex
|
||||
|
||||
receiverBuffers map[string]map[chan string]bool
|
||||
receiverBuffersLock sync.RWMutex
|
||||
|
||||
projectStatusUpdater ProjectStatusUpdater
|
||||
projectJobURLGenerator ProjectJobURLGenerator
|
||||
|
||||
logger logging.SimpleLogging
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_job_url_generator.go ProjectJobURLGenerator
|
||||
|
||||
// ProjectJobURLGenerator generates urls to view project's progress.
|
||||
type ProjectJobURLGenerator interface {
|
||||
GenerateProjectJobURL(p models.ProjectCommandContext) (string, error)
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_status_updater.go ProjectStatusUpdater
|
||||
|
||||
type ProjectStatusUpdater interface {
|
||||
// UpdateProject sets the commit status for the project represented by
|
||||
// ctx.
|
||||
UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) error
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_output_handler.go ProjectCommandOutputHandler
|
||||
|
||||
type ProjectCommandOutputHandler interface {
|
||||
// Clear clears the buffer from previous terraform output lines
|
||||
Clear(ctx models.ProjectCommandContext)
|
||||
|
||||
// Send will enqueue the msg and wait for Handle() to receive the message.
|
||||
Send(ctx models.ProjectCommandContext, msg string)
|
||||
|
||||
// Register registers a channel and blocks until it is caught up. Callers should call this asynchronously when attempting
|
||||
// to read the channel in the same goroutine
|
||||
Register(projectInfo string, receiver chan string)
|
||||
|
||||
// Deregister removes a channel from successive updates and closes it.
|
||||
Deregister(projectInfo string, receiver chan string)
|
||||
|
||||
// Listens for msg from channel
|
||||
Handle()
|
||||
|
||||
// SetJobURLWithStatus sets the commit status for the project represented by
|
||||
// ctx and updates the status with and url to a job.
|
||||
SetJobURLWithStatus(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus) error
|
||||
|
||||
ResourceCleaner
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_resource_cleaner.go ResourceCleaner
|
||||
|
||||
type ResourceCleaner interface {
|
||||
CleanUp(pull string)
|
||||
}
|
||||
|
||||
func NewAsyncProjectCommandOutputHandler(
|
||||
projectCmdOutput chan *models.ProjectCmdOutputLine,
|
||||
projectStatusUpdater ProjectStatusUpdater,
|
||||
projectJobURLGenerator ProjectJobURLGenerator,
|
||||
logger logging.SimpleLogging,
|
||||
) ProjectCommandOutputHandler {
|
||||
return &AsyncProjectCommandOutputHandler{
|
||||
projectCmdOutput: projectCmdOutput,
|
||||
logger: logger,
|
||||
receiverBuffers: map[string]map[chan string]bool{},
|
||||
projectStatusUpdater: projectStatusUpdater,
|
||||
projectJobURLGenerator: projectJobURLGenerator,
|
||||
projectOutputBuffers: map[string][]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) Send(ctx models.ProjectCommandContext, msg string) {
|
||||
p.projectCmdOutput <- &models.ProjectCmdOutputLine{
|
||||
ProjectInfo: ctx.PullInfo(),
|
||||
Line: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) Register(projectInfo string, receiver chan string) {
|
||||
p.addChan(receiver, projectInfo)
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) Handle() {
|
||||
for msg := range p.projectCmdOutput {
|
||||
if msg.ClearBuffBefore {
|
||||
p.clearLogLines(msg.ProjectInfo)
|
||||
}
|
||||
p.writeLogLine(msg.ProjectInfo, msg.Line)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) Clear(ctx models.ProjectCommandContext) {
|
||||
p.projectCmdOutput <- &models.ProjectCmdOutputLine{
|
||||
ProjectInfo: ctx.PullInfo(),
|
||||
Line: models.LogStreamingClearMsg,
|
||||
ClearBuffBefore: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) SetJobURLWithStatus(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus) error {
|
||||
url, err := p.projectJobURLGenerator.GenerateProjectJobURL(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.projectStatusUpdater.UpdateProject(ctx, cmdName, status, url)
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) clearLogLines(pull string) {
|
||||
p.projectOutputBuffersLock.Lock()
|
||||
delete(p.projectOutputBuffers, pull)
|
||||
p.projectOutputBuffersLock.Unlock()
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) addChan(ch chan string, pull string) {
|
||||
p.projectOutputBuffersLock.RLock()
|
||||
buffer := p.projectOutputBuffers[pull]
|
||||
p.projectOutputBuffersLock.RUnlock()
|
||||
|
||||
for _, line := range buffer {
|
||||
ch <- line
|
||||
}
|
||||
|
||||
// add the channel to our registry after we backfill the contents of the buffer,
|
||||
// to prevent new messages coming in interleaving with this backfill.
|
||||
p.receiverBuffersLock.Lock()
|
||||
if p.receiverBuffers[pull] == nil {
|
||||
p.receiverBuffers[pull] = map[chan string]bool{}
|
||||
}
|
||||
p.receiverBuffers[pull][ch] = true
|
||||
p.receiverBuffersLock.Unlock()
|
||||
}
|
||||
|
||||
//Add log line to buffer and send to all current channels
|
||||
func (p *AsyncProjectCommandOutputHandler) writeLogLine(pull string, line string) {
|
||||
p.receiverBuffersLock.Lock()
|
||||
for ch := range p.receiverBuffers[pull] {
|
||||
select {
|
||||
case ch <- line:
|
||||
default:
|
||||
// Client ws conn could be closed in two ways:
|
||||
// 1. Client closes the conn gracefully -> the closeHandler() is executed which
|
||||
// closes the channel and cleans up resources.
|
||||
// 2. Client does not close the conn and the closeHandler() is not executed -> the
|
||||
// receiverChan will be blocking for N number of messages (equal to buffer size)
|
||||
// before we delete the channel and clean up the resources.
|
||||
delete(p.receiverBuffers[pull], ch)
|
||||
}
|
||||
}
|
||||
p.receiverBuffersLock.Unlock()
|
||||
|
||||
// No need to write to projectOutputBuffers if clear msg.
|
||||
if line == models.LogStreamingClearMsg {
|
||||
return
|
||||
}
|
||||
|
||||
p.projectOutputBuffersLock.Lock()
|
||||
if p.projectOutputBuffers[pull] == nil {
|
||||
p.projectOutputBuffers[pull] = []string{}
|
||||
}
|
||||
p.projectOutputBuffers[pull] = append(p.projectOutputBuffers[pull], line)
|
||||
p.projectOutputBuffersLock.Unlock()
|
||||
}
|
||||
|
||||
//Remove channel, so client no longer receives Terraform output
|
||||
func (p *AsyncProjectCommandOutputHandler) Deregister(pull string, ch chan string) {
|
||||
p.logger.Debug("Removing channel for %s", pull)
|
||||
p.receiverBuffersLock.Lock()
|
||||
delete(p.receiverBuffers[pull], ch)
|
||||
p.receiverBuffersLock.Unlock()
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) GetReceiverBufferForPull(pull string) map[chan string]bool {
|
||||
return p.receiverBuffers[pull]
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) GetProjectOutputBuffer(pull string) []string {
|
||||
return p.projectOutputBuffers[pull]
|
||||
}
|
||||
|
||||
func (p *AsyncProjectCommandOutputHandler) CleanUp(pull string) {
|
||||
p.projectOutputBuffersLock.Lock()
|
||||
delete(p.projectOutputBuffers, pull)
|
||||
p.projectOutputBuffersLock.Unlock()
|
||||
|
||||
// Only delete the pull record from receiver buffers.
|
||||
// WS channel will be closed when the user closes the browser tab
|
||||
// in closeHanlder().
|
||||
p.receiverBuffersLock.Lock()
|
||||
delete(p.receiverBuffers, pull)
|
||||
p.receiverBuffersLock.Unlock()
|
||||
}
|
||||
|
||||
// NoopProjectOutputHandler is a mock that doesn't do anything
|
||||
type NoopProjectOutputHandler struct{}
|
||||
|
||||
func (p *NoopProjectOutputHandler) Send(ctx models.ProjectCommandContext, msg string) {
|
||||
}
|
||||
|
||||
func (p *NoopProjectOutputHandler) Register(projectInfo string, receiver chan string) {}
|
||||
func (p *NoopProjectOutputHandler) Deregister(projectInfo string, receiver chan string) {}
|
||||
|
||||
func (p *NoopProjectOutputHandler) Handle() {
|
||||
}
|
||||
|
||||
func (p *NoopProjectOutputHandler) Clear(ctx models.ProjectCommandContext) {
|
||||
}
|
||||
|
||||
func (p *NoopProjectOutputHandler) SetJobURLWithStatus(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NoopProjectOutputHandler) CleanUp(pull string) {
|
||||
}
|
||||
216
server/handlers/project_command_output_handler_test.go
Normal file
216
server/handlers/project_command_output_handler_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
"github.com/runatlantis/atlantis/server/handlers/mocks"
|
||||
"github.com/runatlantis/atlantis/server/handlers/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
. "github.com/petergtz/pegomock"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
func createTestProjectCmdContext(t *testing.T) models.ProjectCommandContext {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
return models.ProjectCommandContext{
|
||||
BaseRepo: models.Repo{
|
||||
Name: "test-repo",
|
||||
Owner: "test-org",
|
||||
},
|
||||
HeadRepo: models.Repo{
|
||||
Name: "test-repo",
|
||||
Owner: "test-org",
|
||||
},
|
||||
Pull: models.PullRequest{
|
||||
Num: 1,
|
||||
HeadBranch: "master",
|
||||
BaseBranch: "master",
|
||||
Author: "test-user",
|
||||
},
|
||||
User: models.User{
|
||||
Username: "test-user",
|
||||
},
|
||||
Log: logger,
|
||||
Workspace: "myworkspace",
|
||||
RepoRelDir: "test-dir",
|
||||
ProjectName: "test-project",
|
||||
}
|
||||
}
|
||||
|
||||
func createProjectCommandOutputHandler(t *testing.T) handlers.ProjectCommandOutputHandler {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
prjCmdOutputChan := make(chan *models.ProjectCmdOutputLine)
|
||||
projectStatusUpdater := mocks.NewMockProjectStatusUpdater()
|
||||
projectJobURLGenerator := mocks.NewMockProjectJobURLGenerator()
|
||||
prjCmdOutputHandler := handlers.NewAsyncProjectCommandOutputHandler(
|
||||
prjCmdOutputChan,
|
||||
projectStatusUpdater,
|
||||
projectJobURLGenerator,
|
||||
logger,
|
||||
)
|
||||
|
||||
go func() {
|
||||
prjCmdOutputHandler.Handle()
|
||||
}()
|
||||
|
||||
return prjCmdOutputHandler
|
||||
}
|
||||
|
||||
func TestProjectCommandOutputHandler(t *testing.T) {
|
||||
Msg := "Test Terraform Output"
|
||||
ctx := createTestProjectCmdContext(t)
|
||||
|
||||
t.Run("receive message from main channel", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
var expectedMsg string
|
||||
projectOutputHandler := createProjectCommandOutputHandler(t)
|
||||
|
||||
ch := make(chan string)
|
||||
|
||||
// register channel and backfill from buffer
|
||||
// Note: We call this synchronously because otherwise
|
||||
// there could be a race where we are unable to register the channel
|
||||
// before sending messages due to the way we lock our buffer memory cache
|
||||
projectOutputHandler.Register(ctx.PullInfo(), ch)
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
// read from channel
|
||||
go func() {
|
||||
for msg := range ch {
|
||||
expectedMsg = msg
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
projectOutputHandler.Send(ctx, Msg)
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
// Wait for the msg to be read.
|
||||
wg.Wait()
|
||||
Equals(t, expectedMsg, Msg)
|
||||
})
|
||||
|
||||
t.Run("clear buffer", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
projectOutputHandler := createProjectCommandOutputHandler(t)
|
||||
|
||||
ch := make(chan string)
|
||||
|
||||
// register channel and backfill from buffer
|
||||
// Note: We call this synchronously because otherwise
|
||||
// there could be a race where we are unable to register the channel
|
||||
// before sending messages due to the way we lock our buffer memory cache
|
||||
projectOutputHandler.Register(ctx.PullInfo(), ch)
|
||||
|
||||
wg.Add(1)
|
||||
// read from channel asynchronously
|
||||
go func() {
|
||||
for msg := range ch {
|
||||
// we are done once we receive the clear message.
|
||||
// prior message doesn't matter for this test.
|
||||
if msg == models.LogStreamingClearMsg {
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// send regular message followed by clear message
|
||||
projectOutputHandler.Send(ctx, Msg)
|
||||
projectOutputHandler.Clear(ctx)
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
dfProjectOutputHandler, ok := projectOutputHandler.(*handlers.AsyncProjectCommandOutputHandler)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.Empty(t, dfProjectOutputHandler.GetProjectOutputBuffer(ctx.PullInfo()))
|
||||
})
|
||||
|
||||
t.Run("copies buffer to new channels", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
projectOutputHandler := createProjectCommandOutputHandler(t)
|
||||
|
||||
// send first message to populated the buffer
|
||||
projectOutputHandler.Send(ctx, Msg)
|
||||
|
||||
ch := make(chan string)
|
||||
|
||||
receivedMsgs := []string{}
|
||||
|
||||
wg.Add(1)
|
||||
// read from channel asynchronously
|
||||
go func() {
|
||||
for msg := range ch {
|
||||
receivedMsgs = append(receivedMsgs, msg)
|
||||
|
||||
// we're only expecting two messages here.
|
||||
if len(receivedMsgs) >= 2 {
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
}()
|
||||
// register channel and backfill from buffer
|
||||
// Note: We call this synchronously because otherwise
|
||||
// there could be a race where we are unable to register the channel
|
||||
// before sending messages due to the way we lock our buffer memory cache
|
||||
projectOutputHandler.Register(ctx.PullInfo(), ch)
|
||||
|
||||
projectOutputHandler.Send(ctx, Msg)
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
|
||||
expectedMsgs := []string{Msg, Msg}
|
||||
assert.Equal(t, len(expectedMsgs), len(receivedMsgs))
|
||||
for i := range expectedMsgs {
|
||||
assert.Equal(t, expectedMsgs[i], receivedMsgs[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update project status with project jobs url", func(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
prjCmdOutputChan := make(chan *models.ProjectCmdOutputLine)
|
||||
projectStatusUpdater := mocks.NewMockProjectStatusUpdater()
|
||||
projectJobURLGenerator := mocks.NewMockProjectJobURLGenerator()
|
||||
prjCmdOutputHandler := handlers.NewAsyncProjectCommandOutputHandler(
|
||||
prjCmdOutputChan,
|
||||
projectStatusUpdater,
|
||||
projectJobURLGenerator,
|
||||
logger,
|
||||
)
|
||||
|
||||
When(projectJobURLGenerator.GenerateProjectJobURL(matchers.EqModelsProjectCommandContext(ctx))).ThenReturn("url-to-project-jobs", nil)
|
||||
err := prjCmdOutputHandler.SetJobURLWithStatus(ctx, models.PlanCommand, models.PendingCommitStatus)
|
||||
Ok(t, err)
|
||||
|
||||
projectStatusUpdater.VerifyWasCalledOnce().UpdateProject(ctx, models.PlanCommand, models.PendingCommitStatus, "url-to-project-jobs")
|
||||
})
|
||||
|
||||
t.Run("update project status with project jobs url error", func(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
prjCmdOutputChan := make(chan *models.ProjectCmdOutputLine)
|
||||
projectStatusUpdater := mocks.NewMockProjectStatusUpdater()
|
||||
projectJobURLGenerator := mocks.NewMockProjectJobURLGenerator()
|
||||
prjCmdOutputHandler := handlers.NewAsyncProjectCommandOutputHandler(
|
||||
prjCmdOutputChan,
|
||||
projectStatusUpdater,
|
||||
projectJobURLGenerator,
|
||||
logger,
|
||||
)
|
||||
|
||||
When(projectJobURLGenerator.GenerateProjectJobURL(matchers.EqModelsProjectCommandContext(ctx))).ThenReturn("url-to-project-jobs", errors.New("some error"))
|
||||
err := prjCmdOutputHandler.SetJobURLWithStatus(ctx, models.PlanCommand, models.PendingCommitStatus)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
61
server/handlers/websocket_handler.go
Normal file
61
server/handlers/websocket_handler.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_websocket_handler.go WebsocketHandler
|
||||
|
||||
type WebsocketHandler interface {
|
||||
Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (WebsocketConnectionWrapper, error)
|
||||
SetReadHandler(w WebsocketConnectionWrapper)
|
||||
SetCloseHandler(w WebsocketConnectionWrapper, receiver chan string)
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_websocket_connection_wrapper.go WebsocketConnectionWrapper
|
||||
|
||||
type WebsocketConnectionWrapper interface {
|
||||
ReadMessage() (messageType int, p []byte, err error)
|
||||
WriteMessage(messageType int, data []byte) error
|
||||
SetCloseHandler(h func(code int, text string) error)
|
||||
}
|
||||
|
||||
type DefaultWebsocketHandler struct {
|
||||
handler websocket.Upgrader
|
||||
Logger logging.SimpleLogging
|
||||
}
|
||||
|
||||
func NewWebsocketHandler(logger logging.SimpleLogging) WebsocketHandler {
|
||||
h := websocket.Upgrader{}
|
||||
h.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
return &DefaultWebsocketHandler{
|
||||
handler: h,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (wh *DefaultWebsocketHandler) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (WebsocketConnectionWrapper, error) {
|
||||
return wh.handler.Upgrade(w, r, responseHeader)
|
||||
}
|
||||
|
||||
func (wh *DefaultWebsocketHandler) SetReadHandler(w WebsocketConnectionWrapper) {
|
||||
for {
|
||||
_, _, err := w.ReadMessage()
|
||||
if err != nil {
|
||||
wh.Logger.Warn("Failed to read WS message: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wh *DefaultWebsocketHandler) SetCloseHandler(w WebsocketConnectionWrapper, receiver chan string) {
|
||||
w.SetCloseHandler(func(code int, text string) error {
|
||||
// Close the channnel after websocket connection closed.
|
||||
// Will gracefully exit the ProjectCommandOutputHandler.Receive() call and cleanup.
|
||||
close(receiver)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -21,52 +21,18 @@ import (
|
||||
)
|
||||
|
||||
// NewRequestLogger creates a RequestLogger.
|
||||
func NewRequestLogger(s *Server) *RequestLogger {
|
||||
return &RequestLogger{
|
||||
s.Logger,
|
||||
s.WebAuthentication,
|
||||
s.WebUsername,
|
||||
s.WebPassword,
|
||||
}
|
||||
func NewRequestLogger(logger logging.SimpleLogging) *RequestLogger {
|
||||
return &RequestLogger{logger}
|
||||
}
|
||||
|
||||
// RequestLogger logs requests and their response codes
|
||||
// as well as handle the basicauth on the requests
|
||||
// RequestLogger logs requests and their response codes.
|
||||
type RequestLogger struct {
|
||||
logger logging.SimpleLogging
|
||||
WebAuthentication bool
|
||||
WebUsername string
|
||||
WebPassword string
|
||||
logger logging.SimpleLogging
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware function. It logs all requests at DEBUG level.
|
||||
func (l *RequestLogger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
l.logger.Debug("%s %s – from %s", r.Method, r.URL.RequestURI(), r.RemoteAddr)
|
||||
allowed := false
|
||||
if !l.WebAuthentication ||
|
||||
r.URL.Path == "/events" ||
|
||||
r.URL.Path == "/healthz" ||
|
||||
r.URL.Path == "/status" {
|
||||
allowed = true
|
||||
} else {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
r.SetBasicAuth(user, pass)
|
||||
l.logger.Debug("user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI())
|
||||
if user == l.WebUsername && pass == l.WebPassword {
|
||||
l.logger.Debug("[VALID] user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI())
|
||||
allowed = true
|
||||
} else {
|
||||
allowed = false
|
||||
l.logger.Info("[INVALID] user: %s / pass: %s >> url: %s", user, pass, r.URL.RequestURI())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
rw.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(rw, "Unauthorized", http.StatusUnauthorized)
|
||||
} else {
|
||||
next(rw, r)
|
||||
}
|
||||
next(rw, r)
|
||||
l.logger.Debug("%s %s – respond HTTP %d", r.Method, r.URL.RequestURI(), rw.(negroni.ResponseWriter).Status())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
// Router can be used to retrieve Atlantis URLs. It acts as an intermediary
|
||||
@@ -15,6 +18,8 @@ type Router struct {
|
||||
// LockViewRouteName is the named route for the lock view that can be Get'd
|
||||
// from the Underlying router.
|
||||
LockViewRouteName string
|
||||
// ProjectJobsViewRouteName is the named route for the projects active jobs
|
||||
ProjectJobsViewRouteName string
|
||||
// LockViewRouteIDQueryParam is the query parameter needed to construct the
|
||||
// lock view: underlying.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, "my id").
|
||||
LockViewRouteIDQueryParam string
|
||||
@@ -33,3 +38,20 @@ func (r *Router) GenerateLockURL(lockID string) string {
|
||||
// golang likes to double escape the lockURL path when using url.Parse().
|
||||
return r.AtlantisURL.String() + lockURL.String()
|
||||
}
|
||||
|
||||
func (r *Router) GenerateProjectJobURL(ctx models.ProjectCommandContext) (string, error) {
|
||||
pull := ctx.Pull
|
||||
projectIdentifier := models.GetProjectIdentifier(ctx.RepoRelDir, ctx.ProjectName)
|
||||
jobURL, err := r.Underlying.Get(r.ProjectJobsViewRouteName).URL(
|
||||
"org", pull.BaseRepo.Owner,
|
||||
"repo", pull.BaseRepo.Name,
|
||||
"pull", fmt.Sprintf("%d", pull.Num),
|
||||
"project", projectIdentifier,
|
||||
"workspace", ctx.Workspace,
|
||||
)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "creating job url for %s/%d/%s/%s", pull.BaseRepo.FullName, pull.Num, projectIdentifier, ctx.Workspace)
|
||||
}
|
||||
|
||||
return r.AtlantisURL.String() + jobURL.String(), nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/runatlantis/atlantis/server"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
@@ -60,3 +61,57 @@ func TestRouter_GenerateLockURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupJobsRouter(t *testing.T) *server.Router {
|
||||
atlantisURL, err := server.ParseAtlantisURL("http://localhost:4141")
|
||||
Ok(t, err)
|
||||
|
||||
underlyingRouter := mux.NewRouter()
|
||||
underlyingRouter.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}", func(_ http.ResponseWriter, _ *http.Request) {}).Methods("GET").Name("project-jobs-detail")
|
||||
|
||||
return &server.Router{
|
||||
AtlantisURL: atlantisURL,
|
||||
Underlying: underlyingRouter,
|
||||
ProjectJobsViewRouteName: "project-jobs-detail",
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateProjectJobURL_ShouldGenerateURLWithProjectNameWhenProjectNameSpecified(t *testing.T) {
|
||||
router := setupJobsRouter(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Pull: models.PullRequest{
|
||||
BaseRepo: models.Repo{
|
||||
Owner: "test-owner",
|
||||
Name: "test-repo",
|
||||
},
|
||||
Num: 1,
|
||||
},
|
||||
ProjectName: "test-project",
|
||||
Workspace: "default",
|
||||
}
|
||||
expectedURL := "http://localhost:4141/jobs/test-owner/test-repo/1/test-project/default"
|
||||
gotURL, err := router.GenerateProjectJobURL(ctx)
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, expectedURL, gotURL)
|
||||
}
|
||||
|
||||
func TestGenerateProjectJobURL_ShouldGenerateURLWithDirectoryAndWorkspaceWhenProjectNameNotSpecified(t *testing.T) {
|
||||
router := setupJobsRouter(t)
|
||||
ctx := models.ProjectCommandContext{
|
||||
Pull: models.PullRequest{
|
||||
BaseRepo: models.Repo{
|
||||
Owner: "test-owner",
|
||||
Name: "test-repo",
|
||||
},
|
||||
Num: 1,
|
||||
},
|
||||
RepoRelDir: "ops/terraform/test-root",
|
||||
Workspace: "default",
|
||||
}
|
||||
expectedURL := "http://localhost:4141/jobs/test-owner/test-repo/1/ops-terraform-test-root/default"
|
||||
gotURL, err := router.GenerateProjectJobURL(ctx)
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, expectedURL, gotURL)
|
||||
}
|
||||
|
||||
119
server/server.go
119
server/server.go
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/runatlantis/atlantis/server/core/db"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
"github.com/runatlantis/atlantis/server/handlers"
|
||||
|
||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -41,6 +42,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/controllers"
|
||||
events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
|
||||
"github.com/runatlantis/atlantis/server/controllers/templates"
|
||||
"github.com/runatlantis/atlantis/server/controllers/websocket"
|
||||
"github.com/runatlantis/atlantis/server/core/locking"
|
||||
"github.com/runatlantis/atlantis/server/core/runtime"
|
||||
"github.com/runatlantis/atlantis/server/core/runtime/policy"
|
||||
@@ -67,11 +69,11 @@ const (
|
||||
// route. ex:
|
||||
// mux.Router.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, "my id")
|
||||
LockViewRouteIDQueryParam = "id"
|
||||
|
||||
// ProjectJobsViewRouteName is the named route in mux.Router for the log stream view.
|
||||
ProjectJobsViewRouteName = "project-jobs-detail"
|
||||
// binDirName is the name of the directory inside our data dir where
|
||||
// we download binaries.
|
||||
BinDirName = "bin"
|
||||
|
||||
// terraformPluginCacheDir is the name of the dir inside our data dir
|
||||
// where we tell terraform to cache plugins and modules.
|
||||
TerraformPluginCacheDirName = "plugin-cache"
|
||||
@@ -92,14 +94,15 @@ type Server struct {
|
||||
GithubAppController *controllers.GithubAppController
|
||||
LocksController *controllers.LocksController
|
||||
StatusController *controllers.StatusController
|
||||
JobsController *controllers.JobsController
|
||||
IndexTemplate templates.TemplateWriter
|
||||
LockDetailTemplate templates.TemplateWriter
|
||||
ProjectJobsTemplate templates.TemplateWriter
|
||||
ProjectJobsErrorTemplate templates.TemplateWriter
|
||||
SSLCertFile string
|
||||
SSLKeyFile string
|
||||
Drainer *events.Drainer
|
||||
WebAuthentication bool
|
||||
WebUsername string
|
||||
WebPassword string
|
||||
ProjectCmdOutputHandler handlers.ProjectCommandOutputHandler
|
||||
}
|
||||
|
||||
// Config holds config for server that isn't passed in by the user.
|
||||
@@ -276,7 +279,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
vcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient, azuredevopsClient)
|
||||
commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient, TitleBuilder: vcs.StatusTitleBuilder{TitlePrefix: userConfig.VCSStatusName}}
|
||||
|
||||
binDir, err := mkSubDir(userConfig.DataDir, BinDirName)
|
||||
|
||||
if err != nil {
|
||||
@@ -289,6 +291,36 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err,
|
||||
"parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL)
|
||||
}
|
||||
|
||||
underlyingRouter := mux.NewRouter()
|
||||
router := &Router{
|
||||
AtlantisURL: parsedURL,
|
||||
LockViewRouteIDQueryParam: LockViewRouteIDQueryParam,
|
||||
LockViewRouteName: LockViewRouteName,
|
||||
ProjectJobsViewRouteName: ProjectJobsViewRouteName,
|
||||
Underlying: underlyingRouter,
|
||||
}
|
||||
|
||||
var projectCmdOutputHandler handlers.ProjectCommandOutputHandler
|
||||
// When TFE is enabled log streaming is not necessary.
|
||||
|
||||
if userConfig.TFEToken != "" {
|
||||
projectCmdOutputHandler = &handlers.NoopProjectOutputHandler{}
|
||||
} else {
|
||||
projectCmdOutput := make(chan *models.ProjectCmdOutputLine)
|
||||
projectCmdOutputHandler = handlers.NewAsyncProjectCommandOutputHandler(
|
||||
projectCmdOutput,
|
||||
commitStatusUpdater,
|
||||
router,
|
||||
logger,
|
||||
)
|
||||
}
|
||||
|
||||
terraformClient, err := terraform.NewClient(
|
||||
logger,
|
||||
binDir,
|
||||
@@ -299,7 +331,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
config.DefaultTFVersionFlag,
|
||||
userConfig.TFDownloadURL,
|
||||
&terraform.DefaultDownloader{},
|
||||
true)
|
||||
true,
|
||||
projectCmdOutputHandler)
|
||||
// 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.
|
||||
@@ -357,11 +390,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
DB: boltdb,
|
||||
}
|
||||
|
||||
parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err,
|
||||
"parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL)
|
||||
}
|
||||
validator := &yaml.ParserValidator{}
|
||||
|
||||
globalCfg := valid.NewGlobalCfgFromArgs(
|
||||
@@ -384,19 +412,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
underlyingRouter := mux.NewRouter()
|
||||
router := &Router{
|
||||
AtlantisURL: parsedURL,
|
||||
LockViewRouteIDQueryParam: LockViewRouteIDQueryParam,
|
||||
LockViewRouteName: LockViewRouteName,
|
||||
Underlying: underlyingRouter,
|
||||
}
|
||||
pullClosedExecutor := &events.PullClosedExecutor{
|
||||
VCSClient: vcsClient,
|
||||
Locker: lockingClient,
|
||||
WorkingDir: workingDir,
|
||||
Logger: logger,
|
||||
DB: boltdb,
|
||||
VCSClient: vcsClient,
|
||||
Locker: lockingClient,
|
||||
WorkingDir: workingDir,
|
||||
Logger: logger,
|
||||
DB: boltdb,
|
||||
LogStreamResourceCleaner: projectCmdOutputHandler,
|
||||
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
||||
}
|
||||
eventParser := &events.EventParser{
|
||||
GithubUser: userConfig.GithubUser,
|
||||
@@ -520,11 +543,16 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
GlobalAutomerge: userConfig.Automerge,
|
||||
}
|
||||
|
||||
projectOutputWrapper := &events.ProjectOutputWrapper{
|
||||
ProjectCmdOutputHandler: projectCmdOutputHandler,
|
||||
ProjectCommandRunner: projectCommandRunner,
|
||||
}
|
||||
|
||||
policyCheckCommandRunner := events.NewPolicyCheckCommandRunner(
|
||||
dbUpdater,
|
||||
pullUpdater,
|
||||
commitStatusUpdater,
|
||||
projectCommandRunner,
|
||||
projectOutputWrapper,
|
||||
userConfig.ParallelPoolSize,
|
||||
userConfig.SilenceVCSStatusNoProjects,
|
||||
)
|
||||
@@ -537,7 +565,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
workingDir,
|
||||
commitStatusUpdater,
|
||||
projectCommandBuilder,
|
||||
projectCommandRunner,
|
||||
projectOutputWrapper,
|
||||
dbUpdater,
|
||||
pullUpdater,
|
||||
policyCheckCommandRunner,
|
||||
@@ -554,7 +582,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
applyLockingClient,
|
||||
commitStatusUpdater,
|
||||
projectCommandBuilder,
|
||||
projectCommandRunner,
|
||||
projectOutputWrapper,
|
||||
autoMerger,
|
||||
pullUpdater,
|
||||
dbUpdater,
|
||||
@@ -568,7 +596,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(
|
||||
commitStatusUpdater,
|
||||
projectCommandBuilder,
|
||||
projectCommandRunner,
|
||||
projectOutputWrapper,
|
||||
pullUpdater,
|
||||
dbUpdater,
|
||||
userConfig.SilenceNoProjects,
|
||||
@@ -584,7 +612,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
versionCommandRunner := events.NewVersionCommandRunner(
|
||||
pullUpdater,
|
||||
projectCommandBuilder,
|
||||
projectCommandRunner,
|
||||
projectOutputWrapper,
|
||||
userConfig.ParallelPoolSize,
|
||||
userConfig.SilenceNoProjects,
|
||||
)
|
||||
@@ -638,6 +666,23 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
DB: boltdb,
|
||||
DeleteLockCommand: deleteLockCommand,
|
||||
}
|
||||
|
||||
wsMux := websocket.NewMultiplexor(
|
||||
logger,
|
||||
controllers.ProjectInfoKeyGenerator{},
|
||||
projectCmdOutputHandler,
|
||||
)
|
||||
|
||||
jobsController := &controllers.JobsController{
|
||||
AtlantisVersion: config.AtlantisVersion,
|
||||
AtlantisURL: parsedURL,
|
||||
Logger: logger,
|
||||
ProjectJobsTemplate: templates.ProjectJobsTemplate,
|
||||
ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate,
|
||||
Db: boltdb,
|
||||
WsMux: wsMux,
|
||||
}
|
||||
|
||||
eventsController := &events_controllers.VCSEventsController{
|
||||
CommandRunner: commandRunner,
|
||||
PullCleaner: pullClosedExecutor,
|
||||
@@ -679,15 +724,16 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
VCSEventsController: eventsController,
|
||||
GithubAppController: githubAppController,
|
||||
LocksController: locksController,
|
||||
JobsController: jobsController,
|
||||
StatusController: statusController,
|
||||
IndexTemplate: templates.IndexTemplate,
|
||||
LockDetailTemplate: templates.LockTemplate,
|
||||
ProjectJobsTemplate: templates.ProjectJobsTemplate,
|
||||
ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate,
|
||||
SSLKeyFile: userConfig.SSLKeyFile,
|
||||
SSLCertFile: userConfig.SSLCertFile,
|
||||
Drainer: drainer,
|
||||
WebAuthentication: userConfig.WebBasicAuth,
|
||||
WebUsername: userConfig.WebUsername,
|
||||
WebPassword: userConfig.WebPassword,
|
||||
ProjectCmdOutputHandler: projectCmdOutputHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -707,12 +753,15 @@ func (s *Server) Start() error {
|
||||
s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}")
|
||||
s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET").
|
||||
Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName)
|
||||
s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}", s.JobsController.GetProjectJobs).Methods("GET").Name(ProjectJobsViewRouteName)
|
||||
s.Router.HandleFunc("/jobs/{org}/{repo}/{pull}/{project}/{workspace}/ws", s.JobsController.GetProjectJobsWS).Methods("GET")
|
||||
|
||||
n := negroni.New(&negroni.Recovery{
|
||||
Logger: log.New(os.Stdout, "", log.LstdFlags),
|
||||
PrintStack: false,
|
||||
StackAll: false,
|
||||
StackSize: 1024 * 8,
|
||||
}, NewRequestLogger(s))
|
||||
}, NewRequestLogger(s.Logger))
|
||||
n.UseHandler(s.Router)
|
||||
|
||||
defer s.Logger.Flush()
|
||||
@@ -722,6 +771,10 @@ func (s *Server) Start() error {
|
||||
// Stop on SIGINTs and SIGTERMs.
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
s.ProjectCmdOutputHandler.Handle()
|
||||
}()
|
||||
|
||||
server := &http.Server{Addr: fmt.Sprintf(":%d", s.Port), Handler: n}
|
||||
go func() {
|
||||
s.Logger.Info("Atlantis started - listening on port %v", s.Port)
|
||||
|
||||
Reference in New Issue
Block a user