feat: streaming terraform logs in real-time (#1937)

This commit is contained in:
Aayush Gupta
2021-12-30 06:52:52 -08:00
committed by GitHub
parent b2ee4ee698
commit 004074a29f
94 changed files with 3198 additions and 1778 deletions

2
go.mod
View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -6,5 +6,6 @@ null_resource.automerge[0]: Creation complete after *s [id=*******************]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

View File

@@ -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.
```

View File

@@ -6,5 +6,6 @@ null_resource.automerge[0]: Creation complete after *s [id=*******************]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
```

View File

@@ -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.
```

View File

@@ -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:

View File

@@ -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`

View File

@@ -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"
```

View File

@@ -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"
```

View File

@@ -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`

View File

@@ -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"
```

View File

@@ -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"
```

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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"
```

View File

@@ -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"
```

View File

@@ -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`

View File

@@ -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>
---

View File

@@ -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"
```

View File

@@ -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>

View File

@@ -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`

View File

@@ -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>
---

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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"
```

View File

@@ -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"
```

View File

@@ -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`

View File

@@ -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`

View File

@@ -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"
```

View File

@@ -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"
```

View File

@@ -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`

View File

@@ -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"
```
---

View File

@@ -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"
```
---

View File

@@ -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`

View File

@@ -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`

View 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)
}

View File

@@ -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

View 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)
}

View 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
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
})
}

View File

@@ -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}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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
}

View 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
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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")
}

View File

@@ -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()))
})
}

View 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, "")
}

View 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
}

View 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
}

View 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
}

View File

@@ -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
}

View 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
}

View 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
}

View 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 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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) {
}

View 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)
})
}

View 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
})
}

View File

@@ -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())
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)