feat: state rm (#2880)

* feat: state rm

* review feedback

* fix conflict for pegomock generation code

* adopt state command into allow-commands

* fix conflicts

* fix: state rm works on workspace

* notify import/state rm discard plan file

* fix lint

* use repeat instead warning for re-plan

* perl -pi -e 's!\* 🔁 plan file was discarded. to!🚮 A plan file was discarded. Re-plan would be required before applying.\n\n\* 🔁 To!g' server/**/*

* follow main branch
This commit is contained in:
Ken Kaizu
2023-01-19 13:02:18 +09:00
committed by GitHub
parent 9e3d8879ec
commit cb0aadf571
81 changed files with 1793 additions and 217 deletions

1
go.mod
View File

@@ -43,6 +43,7 @@ require (
github.com/xanzy/go-gitlab v0.78.0 github.com/xanzy/go-gitlab v0.78.0
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
go.uber.org/zap v1.24.0 go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230113152452-c42ee1cf562e
golang.org/x/term v0.4.0 golang.org/x/term v0.4.0
golang.org/x/text v0.6.0 golang.org/x/text v0.6.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

2
go.sum
View File

@@ -508,6 +508,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230113152452-c42ee1cf562e h1:uGuXqQsI2BAE8xNqSqNxhTdDdhlvpBvWFw/KBwtCtjI=
golang.org/x/exp v0.0.0-20230113152452-c42ee1cf562e/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View File

@@ -56,6 +56,11 @@ workflows:
- init - init
- import: - import:
extra_args: ["-var-file", "production.tfvars"] extra_args: ["-var-file", "production.tfvars"]
state_rm:
steps:
- init
- state_rm:
extra_args: ["-lock=false"]
``` ```
Then in your repo-level `atlantis.yaml` file, you would reference the workflows: Then in your repo-level `atlantis.yaml` file, you would reference the workflows:
```yaml ```yaml
@@ -374,13 +379,15 @@ projects:
plan: plan:
apply: apply:
import: import:
state_rm:
``` ```
| Key | Type | Default | Required | Description | | Key | Type | Default | Required | Description |
|--------|-----------------|-------------------------|----------|----------------------------------| |----------|-----------------|---------------------------|----------|---------------------------------------|
| plan | [Stage](#stage) | `steps: [init, plan]` | no | How to plan for this project. | | plan | [Stage](#stage) | `steps: [init, plan]` | no | How to plan for this project. |
| apply | [Stage](#stage) | `steps: [apply]` | no | How to apply for this project. | | apply | [Stage](#stage) | `steps: [apply]` | no | How to apply for this project. |
| import | [Stage](#stage) | `steps: [init, import]` | no | How to import for this project. | | import | [Stage](#stage) | `steps: [init, import]` | no | How to import for this project. |
| state_rm | [Stage](#stage) | `steps: [init, state_rm]` | no | How to run state rm for this project. |
### Stage ### Stage
```yaml ```yaml
@@ -403,10 +410,11 @@ Steps can be a single string for a built-in command.
- plan - plan
- apply - apply
- import - import
- state_rm
``` ```
| Key | Type | Default | Required | Description | | Key | Type | Default | Required | Description |
|------------------------|--------|---------|----------|------------------------------------------------------------------------------------------------------------------| |---------------------------------|--------|---------|----------|------------------------------------------------------------------------------------------------------------------------------|
| init/plan/apply/import | string | none | no | Use a built-in command without additional configuration. Only `init`, `plan`, `apply` and `import` are supported | | init/plan/apply/import/state_rm | string | none | no | Use a built-in command without additional configuration. Only `init`, `plan`, `apply`, `import` and `state_rm` are supported |
#### Built-In Command With Extra Args #### Built-In Command With Extra Args
A map from string to `extra_args` for a built-in command with extra arguments. A map from string to `extra_args` for a built-in command with extra arguments.
@@ -419,10 +427,12 @@ A map from string to `extra_args` for a built-in command with extra arguments.
extra_args: [arg1, arg2] extra_args: [arg1, arg2]
- import: - import:
extra_args: [arg1, arg2] extra_args: [arg1, arg2]
- state_rm:
extra_args: [arg1, arg2]
``` ```
| Key | Type | Default | Required | Description | | Key | Type | Default | Required | Description |
|------------------------|------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| |---------------------------------|------------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| init/plan/apply/import | map[`extra_args` -> array[string]] | none | no | Use a built-in command and append `extra_args`. Only `init`, `plan`, `apply` and `import` are supported as keys and only `extra_args` is supported as a value | | init/plan/apply/import/state_rm | map[`extra_args` -> array[string]] | none | no | Use a built-in command and append `extra_args`. Only `init`, `plan`, `apply`, `import` and `state_rm` are supported as keys and only `extra_args` is supported as a value |
#### Custom `run` Command #### Custom `run` Command
Or a custom command Or a custom command

View File

@@ -57,7 +57,7 @@ Values are chosen in this order:
Notes: Notes:
* Accepts a comma separated list, ex. `command1,command2`. * Accepts a comma separated list, ex. `command1,command2`.
* `version`, `plan`, `apply`, `unlock`, `approve_policies`, `import` and `all` are available. * `version`, `plan`, `apply`, `unlock`, `approve_policies`, `import`, `state` and `all` are available.
* `all` is a special keyword that allows all commands. If pass `all` then all other commands will be ignored. * `all` is a special keyword that allows all commands. If pass `all` then all other commands will be ignored.
### `--allow-draft-prs` ### `--allow-draft-prs`

View File

@@ -169,6 +169,51 @@ atlantis import -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var
``` ```
If a flag is needed to be always appended, see [Custom Workflow Use Cases](custom-workflows.html#adding-extra-arguments-to-terraform-commands). If a flag is needed to be always appended, see [Custom Workflow Use Cases](custom-workflows.html#adding-extra-arguments-to-terraform-commands).
---
## atlantis state rm
```bash
atlantis state [options] rm ADDRESS... -- [terraform state rm flags]
```
### Explanation
Runs `terraform state rm` that matches the directory/project/workspace.
This command discards the terraform plan result. After run state rm and before an apply, another `atlantis plan` must be run again.
To allow the `state` command requires [--allow-commands](/docs/server-configuration.html#allow-commands) configuration.
### Examples
```bash
# Runs state rm
atlantis state rm ADDRESS1 ADDRESS2
# Runs state rm in the root directory of the repo with workspace `default`
atlantis state -d . rm ADDRESS
# Runs state rm in the `project1` directory of the repo with workspace `default`
atlantis state -d project1 rm ADDRESS
# Runs state rm in the root directory of the repo with workspace `staging`
atlantis state -w staging rm ADDRESS
```
::: tip
* If run state rm to for_each resources, it requires a single quoted address.
* ex. `atlantis state rm 'aws_instance.example["foo"]'`
:::
### Options
* `-d directory` Run state rm a resource for this directory, relative to root of repo. Use `.` for root.
* `-p project` Run state rm a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml`](repo-level-atlantis-yaml.html) repo configuration file. This cannot be used at the same time as `-d` or `-w`.
* `-w workspace` Run state rm a resource for a specific [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused.
### Additional Terraform flags
If `terraform state rm` requires additional arguments, like `-lock=false'`
append them to the end of the comment after `--`, e.g.
```
atlantis state -d dir rm 'aws_instance.example["foo"]' -- -lock=false
```
If a flag is needed to be always appended, see [Custom Workflow Use Cases](custom-workflows.html#adding-extra-arguments-to-terraform-commands).
--- ---
## atlantis unlock ## atlantis unlock
```bash ```bash

View File

@@ -497,6 +497,71 @@ func TestGitHubWorkflow(t *testing.T) {
{"exp-output-merge.txt"}, {"exp-output-merge.txt"},
}, },
}, },
{
Description: "state rm single project",
RepoDir: "state-rm-single-project",
ModifiedFiles: []string{"main.tf"},
ExpAutoplan: true,
Comments: []string{
"atlantis import random_id.simple AA",
"atlantis import 'random_id.for_each[\"overridden\"]' BB -- -var var=overridden",
"atlantis import random_id.count[0] BB",
"atlantis plan -- -var var=overridden",
"atlantis state rm 'random_id.for_each[\"overridden\"]' -- -lock=false",
"atlantis state rm random_id.count[0] random_id.simple",
"atlantis plan -- -var var=overridden",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-import-simple.txt"},
{"exp-output-import-foreach.txt"},
{"exp-output-import-count.txt"},
{"exp-output-plan.txt"},
{"exp-output-state-rm-foreach.txt"},
{"exp-output-state-rm-multiple.txt"},
{"exp-output-plan-again.txt"},
{"exp-output-merged.txt"},
},
},
{
Description: "state rm workspace",
RepoDir: "state-rm-workspace",
Comments: []string{
"atlantis import -p dir1-ops 'random_id.dummy1[0]' AA",
"atlantis plan -p dir1-ops",
"atlantis state rm -p dir1-ops 'random_id.dummy1[0]'",
"atlantis plan -p dir1-ops",
},
ExpReplies: [][]string{
{"exp-output-import-dummy1.txt"},
{"exp-output-plan.txt"},
{"exp-output-state-rm-dummy1.txt"},
{"exp-output-plan-again.txt"},
{"exp-output-merge.txt"},
},
},
{
Description: "state rm multiple project",
RepoDir: "state-rm-multiple-project",
ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"},
ExpAutoplan: true,
Comments: []string{
"atlantis import -d dir1 random_id.dummy AA",
"atlantis import -d dir2 random_id.dummy BB",
"atlantis plan",
"atlantis state rm random_id.dummy",
"atlantis plan",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-import-dummy1.txt"},
{"exp-output-import-dummy2.txt"},
{"exp-output-plan.txt"},
{"exp-output-state-rm-multiple-projects.txt"},
{"exp-output-plan-again.txt"},
{"exp-output-merged.txt"},
},
},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
@@ -1152,7 +1217,8 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
ApplyStepRunner: &runtime.ApplyStepRunner{ ApplyStepRunner: &runtime.ApplyStepRunner{
TerraformExecutor: terraformClient, TerraformExecutor: terraformClient,
}, },
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion), ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFVersion),
RunStepRunner: &runtime.RunStepRunner{ RunStepRunner: &runtime.RunStepRunner{
TerraformExecutor: terraformClient, TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion, DefaultTFVersion: defaultTFVersion,
@@ -1262,6 +1328,12 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
projectCommandRunner, projectCommandRunner,
) )
stateCommandRunner := events.NewStateCommandRunner(
pullUpdater,
projectCommandBuilder,
projectCommandRunner,
)
commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{ commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{
command.Plan: planCommandRunner, command.Plan: planCommandRunner,
command.Apply: applyCommandRunner, command.Apply: applyCommandRunner,
@@ -1269,6 +1341,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
command.Unlock: unlockCommandRunner, command.Unlock: unlockCommandRunner,
command.Version: versionCommandRunner, command.Version: versionCommandRunner,
command.Import: importCommandRunner, command.Import: importCommandRunner,
command.State: stateCommandRunner,
} }
commandRunner := &events.DefaultCommandRunner{ commandRunner := &events.DefaultCommandRunner{

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -d dir1` * `atlantis plan -d dir1`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -d .` * `atlantis plan -d .`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -d .` * `atlantis plan -d .`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -d .` * `atlantis plan -d .`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -d .` * `atlantis plan -d .`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops` * `atlantis plan -p dir1-ops`

View File

@@ -12,5 +12,7 @@ The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform. your Terraform state and will henceforth be managed by Terraform.
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops` * `atlantis plan -p dir1-ops`

View File

@@ -0,0 +1,4 @@
version: 3
projects:
- dir: dir1
- dir: dir2

View File

@@ -0,0 +1,4 @@
resource "random_id" "dummy" {
keepers = {}
byte_length = 1
}

View File

@@ -0,0 +1,4 @@
resource "random_id" "dummy" {
keepers = {}
byte_length = 1
}

View File

@@ -0,0 +1,75 @@
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:
# random_id.dummy will be created
+ resource "random_id" "dummy" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
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:
# random_id.dummy will be created
+ resource "random_id" "dummy" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
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

@@ -0,0 +1,18 @@
Ran Import for dir: `dir1` workspace: `default`
```diff
random_id.dummy: Importing from ID "AA"...
random_id.dummy: Import prepared!
Prepared random_id for import
random_id.dummy: Refreshing state... [id=AA]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d dir1`

View File

@@ -0,0 +1,18 @@
Ran Import for dir: `dir2` workspace: `default`
```diff
random_id.dummy: Importing from ID "BB"...
random_id.dummy: Import prepared!
Prepared random_id for import
random_id.dummy: Refreshing state... [id=BB]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d dir2`

View File

@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- dir: `dir1` workspace: `default`
- dir: `dir2` workspace: `default`

View File

@@ -0,0 +1,75 @@
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:
# random_id.dummy will be created
+ resource "random_id" "dummy" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
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:
# random_id.dummy will be created
+ resource "random_id" "dummy" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
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

@@ -0,0 +1,43 @@
Ran Plan for 2 projects:
1. dir: `dir1` workspace: `default`
1. dir: `dir2` workspace: `default`
### 1. dir: `dir1` workspace: `default`
```diff
random_id.dummy: Refreshing state... [id=AA]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
```
* :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`
---
### 2. dir: `dir2` workspace: `default`
```diff
random_id.dummy: Refreshing state... [id=BB]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
```
* :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`
---
* :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,29 @@
Ran State for 2 projects:
1. dir: `dir1` workspace: `default`
1. dir: `dir2` workspace: `default`
### 1. dir: `dir1` workspace: `default`
```diff
Removed random_id.dummy
Successfully removed 1 resource instance(s).
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d dir1`
---
### 2. dir: `dir2` workspace: `default`
```diff
Removed random_id.dummy
Successfully removed 1 resource instance(s).
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d dir2`
---

View File

@@ -0,0 +1,60 @@
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:
# random_id.count[0] will be created
+ resource "random_id" "count" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
# random_id.for_each["default"] will be created
+ resource "random_id" "for_each" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
# random_id.simple will be created
+ resource "random_id" "simple" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
Plan: 3 to add, 0 to change, 0 to destroy.
```
* :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

@@ -0,0 +1,18 @@
Ran Import for dir: `.` workspace: `default`
```diff
random_id.count[0]: Importing from ID "BB"...
random_id.count[0]: Import prepared!
Prepared random_id for import
random_id.count[0]: Refreshing state... [id=BB]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d .`

View File

@@ -0,0 +1,18 @@
Ran Import for dir: `.` workspace: `default`
```diff
random_id.for_each["overridden"]: Importing from ID "BB"...
random_id.for_each["overridden"]: Import prepared!
Prepared random_id for import
random_id.for_each["overridden"]: Refreshing state... [id=BB]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d .`

View File

@@ -0,0 +1,18 @@
Ran Import for dir: `.` workspace: `default`
```diff
random_id.simple: Importing from ID "AA"...
random_id.simple: Import prepared!
Prepared random_id for import
random_id.simple: Refreshing state... [id=AA]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d .`

View File

@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- dir: `.` workspace: `default`

View File

@@ -0,0 +1,60 @@
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:
# random_id.count[0] will be created
+ resource "random_id" "count" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
# random_id.for_each["overridden"] will be created
+ resource "random_id" "for_each" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
# random_id.simple will be created
+ resource "random_id" "simple" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
Plan: 3 to add, 0 to change, 0 to destroy.
```
* :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

@@ -0,0 +1,20 @@
Ran Plan for dir: `.` workspace: `default`
```diff
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
```
* :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`
---
* :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,11 @@
Ran State `rm` for dir: `.` workspace: `default`
```diff
Removed random_id.for_each["overridden"]
Successfully removed 1 resource instance(s).
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d .`

View File

@@ -0,0 +1,12 @@
Ran State `rm` for dir: `.` workspace: `default`
```diff
Removed random_id.count[0]
Removed random_id.simple
Successfully removed 2 resource instance(s).
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d .`

View File

@@ -0,0 +1,20 @@
resource "random_id" "simple" {
keepers = {}
byte_length = 1
}
resource "random_id" "for_each" {
for_each = toset([var.var])
keepers = {}
byte_length = 1
}
resource "random_id" "count" {
count = 1
keepers = {}
byte_length = 1
}
variable "var" {
default = "default"
}

View File

@@ -0,0 +1,5 @@
version: 3
projects:
- name: dir1-ops
dir: dir1
workspace: ops

View File

@@ -0,0 +1,10 @@
resource "random_id" "dummy1" {
count = terraform.workspace == "ops" ? 1 : 0
keepers = {}
byte_length = 1
}
output "workspace" {
value = terraform.workspace
}

View File

@@ -0,0 +1,18 @@
Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`
```diff
random_id.dummy1[0]: Importing from ID "AA"...
random_id.dummy1[0]: Import prepared!
Prepared random_id for import
random_id.dummy1[0]: Refreshing state... [id=AA]
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`

View File

@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- dir: `dir1` workspace: `ops`

View File

@@ -0,0 +1,38 @@
Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`
<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:
# random_id.dummy1[0] will be created
+ resource "random_id" "dummy1" {
+ b64_std = (known after apply)
+ b64_url = (known after apply)
+ byte_length = 1
+ dec = (known after apply)
+ hex = (known after apply)
+ id = (known after apply)
+ keepers = {}
}
Plan: 1 to add, 0 to change, 0 to destroy.
```
* :arrow_forward: To **apply** this plan, comment:
* `atlantis apply -p dir1-ops`
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`
</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,22 @@
Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`
```diff
random_id.dummy1[0]: Refreshing state... [id=AA]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
```
* :arrow_forward: To **apply** this plan, comment:
* `atlantis apply -p dir1-ops`
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`
---
* :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,11 @@
Ran State `rm` for project: `dir1-ops` dir: `dir1` workspace: `ops`
```diff
Removed random_id.dummy1[0]
Successfully removed 1 resource instance(s).
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`

View File

@@ -175,7 +175,8 @@ workflows:
}, },
}, },
}, },
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
}, },
}, },
@@ -755,6 +756,9 @@ workflows:
import: import:
steps: steps:
- import - import
state_rm:
steps:
- state_rm
`, `,
exp: valid.RepoCfg{ exp: valid.RepoCfg{
Version: 3, Version: 3,
@@ -808,6 +812,13 @@ workflows:
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "state_rm",
},
},
},
}, },
}, },
}, },
@@ -843,6 +854,10 @@ workflows:
steps: steps:
- import: - import:
extra_args: ["a", "b"] extra_args: ["a", "b"]
state_rm:
steps:
- state_rm:
extra_args: ["a", "b"]
`, `,
exp: valid.RepoCfg{ exp: valid.RepoCfg{
Version: 3, Version: 3,
@@ -899,6 +914,14 @@ workflows:
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "state_rm",
ExtraArgs: []string{"a", "b"},
},
},
},
}, },
}, },
}, },
@@ -923,6 +946,9 @@ workflows:
import: import:
steps: steps:
- run: echo apply "arg 3" - run: echo apply "arg 3"
state_rm:
steps:
- run: echo apply "arg 4"
`, `,
exp: valid.RepoCfg{ exp: valid.RepoCfg{
Version: 3, Version: 3,
@@ -971,6 +997,14 @@ workflows:
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "run",
RunCommand: "echo apply \"arg 4\"",
},
},
},
}, },
}, },
}, },
@@ -1003,6 +1037,11 @@ workflows:
- env: - env:
name: env_name name: env_name
value: env_value value: env_value
state_rm:
steps:
- env:
name: env_name
value: env_value
`, `,
exp: valid.RepoCfg{ exp: valid.RepoCfg{
Version: 3, Version: 3,
@@ -1055,6 +1094,15 @@ workflows:
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "env",
EnvVarName: "env_name",
EnvVarValue: "env_value",
},
},
},
}, },
}, },
}, },
@@ -1194,6 +1242,17 @@ func TestParseGlobalCfg(t *testing.T) {
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "run",
RunCommand: "custom command",
},
{
StepName: "state_rm",
},
},
},
} }
conftestVersion, _ := version.NewVersion("v1.0.0") conftestVersion, _ := version.NewVersion("v1.0.0")
@@ -1291,6 +1350,7 @@ workflows:
plan: plan:
policy_check: policy_check:
import: import:
state_rm:
`, `,
exp: valid.GlobalCfg{ exp: valid.GlobalCfg{
Repos: defaultCfg.Repos, Repos: defaultCfg.Repos,
@@ -1312,6 +1372,8 @@ workflows:
steps: steps:
import: import:
steps: steps:
state_rm:
steps:
`, `,
exp: valid.GlobalCfg{ exp: valid.GlobalCfg{
Repos: defaultCfg.Repos, Repos: defaultCfg.Repos,
@@ -1362,6 +1424,10 @@ workflows:
steps: steps:
- run: custom command - run: custom command
- import - import
state_rm:
steps:
- run: custom command
- state_rm
policies: policies:
conftest_version: v1.0.0 conftest_version: v1.0.0
policy_sets: policy_sets:
@@ -1454,6 +1520,8 @@ workflows:
steps: [] steps: []
import: import:
steps: [] steps: []
state_rm:
steps: []
`, `,
exp: valid.GlobalCfg{ exp: valid.GlobalCfg{
Repos: []valid.Repo{ Repos: []valid.Repo{
@@ -1481,6 +1549,9 @@ workflows:
Import: valid.Stage{ Import: valid.Stage{
Steps: nil, Steps: nil,
}, },
StateRm: valid.Stage{
Steps: nil,
},
}, },
AllowedWorkflows: []string{}, AllowedWorkflows: []string{},
AllowedOverrides: []string{}, AllowedOverrides: []string{},
@@ -1599,6 +1670,14 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "run",
RunCommand: "custom state_rm",
},
},
},
} }
conftestVersion, _ := version.NewVersion("v1.0.0") conftestVersion, _ := version.NewVersion("v1.0.0")
@@ -1661,6 +1740,11 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
"steps": [ "steps": [
{"run": "custom import"} {"run": "custom import"}
] ]
},
"state_rm": {
"steps": [
{"run": "custom state_rm"}
]
} }
} }
}, },
@@ -1833,5 +1917,6 @@ func defaultWorkflow(name string) valid.Workflow {
Plan: valid.DefaultPlanStage, Plan: valid.DefaultPlanStage,
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
} }
} }

View File

@@ -308,6 +308,7 @@ func TestConfig_ToValid(t *testing.T) {
Apply: nil, Apply: nil,
PolicyCheck: nil, PolicyCheck: nil,
Import: nil, Import: nil,
StateRm: nil,
}, },
}, },
}, },
@@ -322,6 +323,7 @@ func TestConfig_ToValid(t *testing.T) {
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Apply: valid.DefaultApplyStage, Apply: valid.DefaultApplyStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
}, },
}, },
@@ -362,6 +364,13 @@ func TestConfig_ToValid(t *testing.T) {
}, },
}, },
}, },
StateRm: &raw.Stage{
Steps: []raw.Step{
{
Key: String("state_rm"),
},
},
},
}, },
}, },
Projects: []raw.Project{ Projects: []raw.Project{
@@ -405,6 +414,13 @@ func TestConfig_ToValid(t *testing.T) {
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "state_rm",
},
},
},
}, },
}, },
Projects: []valid.Project{ Projects: []valid.Project{

View File

@@ -25,6 +25,7 @@ const (
EnvStepName = "env" EnvStepName = "env"
MultiEnvStepName = "multienv" MultiEnvStepName = "multienv"
ImportStepName = "import" ImportStepName = "import"
StateRmStepName = "state_rm"
) )
// Step represents a single action/command to perform. In YAML, it can be set as // Step represents a single action/command to perform. In YAML, it can be set as
@@ -90,7 +91,8 @@ func (s Step) validStepName(stepName string) bool {
stepName == MultiEnvStepName || stepName == MultiEnvStepName ||
stepName == ShowStepName || stepName == ShowStepName ||
stepName == PolicyCheckStepName || stepName == PolicyCheckStepName ||
stepName == ImportStepName stepName == ImportStepName ||
stepName == StateRmStepName
} }
func (s Step) Validate() error { func (s Step) Validate() error {

View File

@@ -10,6 +10,7 @@ type Workflow struct {
Plan *Stage `yaml:"plan,omitempty" json:"plan,omitempty"` Plan *Stage `yaml:"plan,omitempty" json:"plan,omitempty"`
PolicyCheck *Stage `yaml:"policy_check,omitempty" json:"policy_check,omitempty"` PolicyCheck *Stage `yaml:"policy_check,omitempty" json:"policy_check,omitempty"`
Import *Stage `yaml:"import,omitempty" json:"import,omitempty"` Import *Stage `yaml:"import,omitempty" json:"import,omitempty"`
StateRm *Stage `yaml:"state_rm,omitempty" json:"state_rm,omitempty"`
} }
func (w Workflow) Validate() error { func (w Workflow) Validate() error {
@@ -18,6 +19,7 @@ func (w Workflow) Validate() error {
validation.Field(&w.Plan), validation.Field(&w.Plan),
validation.Field(&w.PolicyCheck), validation.Field(&w.PolicyCheck),
validation.Field(&w.Import), validation.Field(&w.Import),
validation.Field(&w.StateRm),
) )
} }
@@ -38,6 +40,7 @@ func (w Workflow) ToValid(name string) valid.Workflow {
v.Plan = w.toValidStage(w.Plan, valid.DefaultPlanStage) v.Plan = w.toValidStage(w.Plan, valid.DefaultPlanStage)
v.PolicyCheck = w.toValidStage(w.PolicyCheck, valid.DefaultPolicyCheckStage) v.PolicyCheck = w.toValidStage(w.PolicyCheck, valid.DefaultPolicyCheckStage)
v.Import = w.toValidStage(w.Import, valid.DefaultImportStage) v.Import = w.toValidStage(w.Import, valid.DefaultImportStage)
v.StateRm = w.toValidStage(w.StateRm, valid.DefaultStateRmStage)
return v return v
} }

View File

@@ -149,6 +149,7 @@ func TestWorkflow_ToValid(t *testing.T) {
Plan: valid.DefaultPlanStage, Plan: valid.DefaultPlanStage,
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
}, },
{ {
@@ -182,6 +183,13 @@ func TestWorkflow_ToValid(t *testing.T) {
}, },
}, },
}, },
StateRm: &raw.Stage{
Steps: []raw.Step{
{
Key: String("state_rm"),
},
},
},
}, },
exp: valid.Workflow{ exp: valid.Workflow{
Apply: valid.Stage{ Apply: valid.Stage{
@@ -212,6 +220,13 @@ func TestWorkflow_ToValid(t *testing.T) {
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "state_rm",
},
},
},
}, },
}, },
} }

View File

@@ -149,6 +149,18 @@ var DefaultImportStage = Stage{
}, },
} }
// DefaultStateRmStage is the Atlantis default state_rm stage.
var DefaultStateRmStage = Stage{
Steps: []Step{
{
StepName: "init",
},
{
StepName: "state_rm",
},
},
}
// Deprecated: use NewGlobalCfgFromArgs // Deprecated: use NewGlobalCfgFromArgs
func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, unDivergedReq bool, preWorkflowHooks []*WorkflowHook, postWorkflowHooks []*WorkflowHook) GlobalCfg { func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, unDivergedReq bool, preWorkflowHooks []*WorkflowHook, postWorkflowHooks []*WorkflowHook) GlobalCfg {
return NewGlobalCfgFromArgs(GlobalCfgArgs{ return NewGlobalCfgFromArgs(GlobalCfgArgs{
@@ -194,6 +206,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
Plan: DefaultPlanStage, Plan: DefaultPlanStage,
PolicyCheck: DefaultPolicyCheckStage, PolicyCheck: DefaultPolicyCheckStage,
Import: DefaultImportStage, Import: DefaultImportStage,
StateRm: DefaultStateRmStage,
} }
// Must construct slices here instead of using a `var` declaration because // Must construct slices here instead of using a `var` declaration because
// we treat nil slices differently. // we treat nil slices differently.

View File

@@ -55,6 +55,16 @@ func TestNewGlobalCfg(t *testing.T) {
}, },
}, },
}, },
StateRm: valid.Stage{
Steps: []valid.Step{
{
StepName: "init",
},
{
StepName: "state_rm",
},
},
},
} }
baseCfg := valid.GlobalCfg{ baseCfg := valid.GlobalCfg{
Repos: []valid.Repo{ Repos: []valid.Repo{
@@ -631,6 +641,7 @@ policies:
Plan: valid.DefaultPlanStage, Plan: valid.DefaultPlanStage,
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
PolicySets: valid.PolicySets{ PolicySets: valid.PolicySets{
Version: nil, Version: nil,
@@ -675,6 +686,7 @@ policies:
Plan: valid.DefaultPlanStage, Plan: valid.DefaultPlanStage,
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
PolicySets: valid.PolicySets{ PolicySets: valid.PolicySets{
Version: version, Version: version,
@@ -736,6 +748,7 @@ func TestGlobalCfg_MergeProjectCfg(t *testing.T) {
PolicyCheck: valid.DefaultPolicyCheckStage, PolicyCheck: valid.DefaultPolicyCheckStage,
Plan: valid.DefaultPlanStage, Plan: valid.DefaultPlanStage,
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
} }
cases := map[string]struct { cases := map[string]struct {
gCfg string gCfg string
@@ -774,7 +787,8 @@ workflows:
}, },
}, },
}, },
Import: valid.DefaultImportStage, Import: valid.DefaultImportStage,
StateRm: valid.DefaultStateRmStage,
}, },
RepoRelDir: ".", RepoRelDir: ".",
Workspace: "default", Workspace: "default",

View File

@@ -170,4 +170,5 @@ type Workflow struct {
Plan Stage Plan Stage
PolicyCheck Stage PolicyCheck Stage
Import Stage Import Stage
StateRm Stage
} }

View File

@@ -8,9 +8,9 @@ import (
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock" . "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/core/runtime/mocks/matchers" runtimematchers "github.com/runatlantis/atlantis/server/core/runtime/mocks/matchers"
"github.com/runatlantis/atlantis/server/core/terraform/mocks" "github.com/runatlantis/atlantis/server/core/terraform/mocks"
matchers2 "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers" tfmatchers "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing" . "github.com/runatlantis/atlantis/testing"
@@ -35,7 +35,7 @@ func TestImportStepRunner_Run_Success(t *testing.T) {
tfVersion, _ := version.NewVersion("0.15.0") tfVersion, _ := version.NewVersion("0.15.0")
s := NewImportStepRunner(terraform, tfVersion) s := NewImportStepRunner(terraform, tfVersion)
When(terraform.RunCommandWithVersion(matchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). When(terraform.RunCommandWithVersion(runtimematchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), tfmatchers.AnyMapOfStringToString(), tfmatchers.AnyPtrToGoVersionVersion(), AnyString())).
ThenReturn("output", nil) ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err) Ok(t, err)
@@ -65,7 +65,7 @@ func TestImportStepRunner_Run_Workspace(t *testing.T) {
tfVersion, _ := version.NewVersion("0.15.0") tfVersion, _ := version.NewVersion("0.15.0")
s := NewImportStepRunner(terraform, tfVersion) s := NewImportStepRunner(terraform, tfVersion)
When(terraform.RunCommandWithVersion(matchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). When(terraform.RunCommandWithVersion(runtimematchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), runtimematchers.AnyMapOfStringToString(), runtimematchers.AnyPtrToGoVersionVersion(), AnyString())).
ThenReturn("output", nil) ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err) Ok(t, err)

View File

@@ -0,0 +1,46 @@
package runtime
import (
"os"
"path/filepath"
version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/events/command"
)
type stateRmStepRunner struct {
terraformExecutor TerraformExec
defaultTFVersion *version.Version
}
func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner {
runner := &stateRmStepRunner{
terraformExecutor: terraformExecutor,
defaultTFVersion: defaultTfVersion,
}
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner)
}
func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfVersion := p.defaultTFVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
stateRmCmd := []string{"state", "rm"}
stateRmCmd = append(stateRmCmd, extraArgs...)
stateRmCmd = append(stateRmCmd, ctx.EscapedCommentArgs...)
out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfVersion, ctx.Workspace)
// If the state rm was successful and a plan file exists, delete the plan.
planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))
if err == nil {
if _, planPathErr := os.Stat(planPath); !os.IsNotExist(planPathErr) {
ctx.Log.Info("state rm successful, deleting planfile")
if removeErr := os.Remove(planPath); removeErr != nil {
ctx.Log.Warn("failed to delete planfile after successful state rm: %s", removeErr)
}
}
}
return out, err
}

View File

@@ -0,0 +1,84 @@
package runtime
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock"
runtimematchers "github.com/runatlantis/atlantis/server/core/runtime/mocks/matchers"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfmatchers "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
func TestStateRmStepRunner_Run_Success(t *testing.T) {
logger := logging.NewNoopLogger(t)
workspace := "default"
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace))
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"-lock=false", "addr1", "addr2", "addr3"},
Workspace: workspace,
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewStateRmStepRunner(terraform, tfVersion)
When(terraform.RunCommandWithVersion(runtimematchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), tfmatchers.AnyMapOfStringToString(), tfmatchers.AnyPtrToGoVersionVersion(), AnyString())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, "default")
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
func TestStateRmStepRunner_Run_Workspace(t *testing.T) {
logger := logging.NewNoopLogger(t)
workspace := "something"
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace))
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"-lock=false", "addr1", "addr2", "addr3"},
Workspace: workspace,
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewStateRmStepRunner(terraform, tfVersion)
When(terraform.RunCommandWithVersion(runtimematchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), runtimematchers.AnyMapOfStringToString(), runtimematchers.AnyPtrToGoVersionVersion(), AnyString())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
// switch workspace
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace)
// exec state rm
commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace)
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}

View File

@@ -28,9 +28,16 @@ const (
Version Version
// Import is a command to run terraform import // Import is a command to run terraform import
Import Import
// State is a command to run terraform state rm
State
// Adding more? Don't forget to update String() below // Adding more? Don't forget to update String() below
) )
type ArgCount struct {
Min int
Max int
}
// AllCommentCommands are list of commands that can be run from a comment. // AllCommentCommands are list of commands that can be run from a comment.
var AllCommentCommands = []Name{ var AllCommentCommands = []Name{
Version, Version,
@@ -39,6 +46,7 @@ var AllCommentCommands = []Name{
Unlock, Unlock,
ApprovePolicies, ApprovePolicies,
Import, Import,
State,
} }
// TitleString returns the string representation in title form. // TitleString returns the string representation in title form.
@@ -64,6 +72,8 @@ func (c Name) String() string {
return "version" return "version"
case Import: case Import:
return "import" return "import"
case State:
return "state"
} }
return "" return ""
} }
@@ -73,11 +83,53 @@ func (c Name) DefaultUsage() string {
switch c { switch c {
case Import: case Import:
return "import ADDRESS ID" return "import ADDRESS ID"
case State:
return "state [rm ADDRESS...]"
default: default:
return c.String() return c.String()
} }
} }
// SubCommands returns the list of sub commands for the command
func (c Name) SubCommands() []string {
switch c {
case State:
return []string{"rm"}
default:
return nil
}
}
// CommandArgCount returns the number of required arguments for the command
func (c Name) CommandArgCount(subCommand string) (*ArgCount, error) {
switch c {
case Import:
return &ArgCount{2, 2}, nil // "atlantis import ADDRESS ID"
case State:
if subCommand == "rm" {
return &ArgCount{1, -1}, nil // "atlantis state rm ADDRESS..."
}
return nil, fmt.Errorf("command arg count unknown sub command: %s", subCommand)
default:
return &ArgCount{0, 0}, nil // other command doesn't require any args
}
}
// IsMatchCount returns true if the number of arguments matches the requirement
func (a ArgCount) IsMatchCount(count int) bool {
if a.Min != -1 {
if count < a.Min {
return false
}
}
if a.Max != -1 {
if count > a.Max {
return false
}
}
return true
}
// ParseCommandName parses raw name into a command name. // ParseCommandName parses raw name into a command name.
func ParseCommandName(name string) (Name, error) { func ParseCommandName(name string) (Name, error) {
switch name { switch name {
@@ -95,6 +147,8 @@ func ParseCommandName(name string) (Name, error) {
return Version, nil return Version, nil
case "import": case "import":
return Import, nil return Import, nil
case "state":
return State, nil
} }
return -1, fmt.Errorf("unknown command name: %s", name) return -1, fmt.Errorf("unknown command name: %s", name)
} }

View File

@@ -1,6 +1,9 @@
package command_test package command_test
import ( import (
"fmt"
"math"
"reflect"
"testing" "testing"
"github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/command"
@@ -36,6 +39,7 @@ func TestName_String(t *testing.T) {
{command.ApprovePolicies, "approve_policies"}, {command.ApprovePolicies, "approve_policies"},
{command.Version, "version"}, {command.Version, "version"},
{command.Import, "import"}, {command.Import, "import"},
{command.State, "state"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) { t.Run(tt.want, func(t *testing.T) {
@@ -58,6 +62,7 @@ func TestName_DefaultUsage(t *testing.T) {
{command.ApprovePolicies, "approve_policies"}, {command.ApprovePolicies, "approve_policies"},
{command.Version, "version"}, {command.Version, "version"},
{command.Import, "import ADDRESS ID"}, {command.Import, "import ADDRESS ID"},
{command.State, "state [rm ADDRESS...]"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.c.String(), func(t *testing.T) { t.Run(tt.c.String(), func(t *testing.T) {
@@ -68,6 +73,98 @@ func TestName_DefaultUsage(t *testing.T) {
} }
} }
func TestName_SubCommands(t *testing.T) {
tests := []struct {
c command.Name
want []string
}{
{c: command.Apply},
{c: command.Plan},
{c: command.Unlock},
{c: command.PolicyCheck},
{c: command.ApprovePolicies},
{c: command.Version},
{c: command.Import},
{c: command.State, want: []string{"rm"}},
}
for _, tt := range tests {
t.Run(tt.c.String(), func(t *testing.T) {
if got := tt.c.SubCommands(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SubCommands() = %v, want %v", got, tt.want)
}
})
}
}
func TestName_CommandArgCount(t *testing.T) {
tests := []struct {
c command.Name
subCommand string
want *command.ArgCount
wantErr bool
}{
{c: command.Apply, want: &command.ArgCount{}},
{c: command.Plan, want: &command.ArgCount{}},
{c: command.Unlock, want: &command.ArgCount{}},
{c: command.PolicyCheck, want: &command.ArgCount{}},
{c: command.ApprovePolicies, want: &command.ArgCount{}},
{c: command.Version, want: &command.ArgCount{}},
{c: command.Import, want: &command.ArgCount{Min: 2, Max: 2}},
{c: command.State, subCommand: "rm", want: &command.ArgCount{Min: 1, Max: -1}},
{c: command.State, subCommand: "unknown", wantErr: true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s %s", tt.c, tt.subCommand), func(t *testing.T) {
got, err := tt.c.CommandArgCount(tt.subCommand)
if (err != nil) != tt.wantErr {
t.Errorf("CommandArgCount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CommandArgCount() got = %v, want %v", got, tt.want)
}
})
}
}
func TestArgCount_IsMatchCount(t *testing.T) {
type fields struct {
Min int
Max int
}
tests := []struct {
name string
fields fields
count int
want bool
}{
{name: "[0,0] success", fields: fields{Min: 0, Max: 0}, count: 0, want: true},
{name: "[0,0] failure", fields: fields{Min: 0, Max: 0}, count: 1, want: false},
{name: "[1,1] success", fields: fields{Min: 1, Max: 1}, count: 1, want: true},
{name: "[1,1] failure1", fields: fields{Min: 1, Max: 1}, count: 0, want: false},
{name: "[1,1] failure2", fields: fields{Min: 1, Max: 1}, count: 2, want: false},
{name: "[-inf,1] success1", fields: fields{Min: -1, Max: 1}, count: 0, want: true},
{name: "[-inf,1] success2", fields: fields{Min: -1, Max: 1}, count: 1, want: true},
{name: "[-inf,1] failure", fields: fields{Min: -1, Max: 1}, count: 2, want: false},
{name: "[1,inf] success1", fields: fields{Min: 1, Max: -1}, count: 1, want: true},
{name: "[1,inf] success2", fields: fields{Min: 1, Max: -1}, count: math.MaxInt, want: true},
{name: "[1,inf] failure", fields: fields{Min: 1, Max: -1}, count: 0, want: false},
{name: "[-inf,inf] success", fields: fields{Min: -1, Max: -1}, count: 0, want: true},
{name: "[-inf,inf] success", fields: fields{Min: -1, Max: -1}, count: math.MaxInt, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := command.ArgCount{
Min: tt.fields.Min,
Max: tt.fields.Max,
}
if got := a.IsMatchCount(tt.count); got != tt.want {
t.Errorf("IsMatchCount() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseCommandName(t *testing.T) { func TestParseCommandName(t *testing.T) {
tests := []struct { tests := []struct {
exp command.Name exp command.Name
@@ -80,6 +177,7 @@ func TestParseCommandName(t *testing.T) {
{command.ApprovePolicies, "approve_policies"}, {command.ApprovePolicies, "approve_policies"},
{command.Version, "version"}, {command.Version, "version"},
{command.Import, "import"}, {command.Import, "import"},
{command.State, "state"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@@ -7,6 +7,7 @@ import (
// ProjectResult is the result of executing a plan/policy_check/apply for a specific project. // ProjectResult is the result of executing a plan/policy_check/apply for a specific project.
type ProjectResult struct { type ProjectResult struct {
Command Name Command Name
SubCommand string
RepoRelDir string RepoRelDir string
Workspace string Workspace string
Error error Error error
@@ -16,6 +17,7 @@ type ProjectResult struct {
ApplySuccess string ApplySuccess string
VersionSuccess string VersionSuccess string
ImportSuccess *models.ImportSuccess ImportSuccess *models.ImportSuccess
StateRmSuccess *models.StateRmSuccess
ProjectName string ProjectName string
} }

View File

@@ -27,6 +27,7 @@ import (
"github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/models"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/exp/slices"
) )
const ( const (
@@ -206,7 +207,6 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
var verbose, autoMergeDisabled bool var verbose, autoMergeDisabled bool
var flagSet *pflag.FlagSet var flagSet *pflag.FlagSet
var name command.Name var name command.Name
var requiredCommandArgCount int
// Set up the flag parsing depending on the command. // Set up the flag parsing depending on the command.
switch cmd { switch cmd {
@@ -251,47 +251,22 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run import in relative to root of repo, ex. 'child/dir'.") flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run import in relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", "Which project to run import for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.") flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", "Which project to run import for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.")
flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.")
requiredCommandArgCount = 2 // import requires `atlantis import ADDRESS ID` args case command.State.String():
name = command.State
flagSet = pflag.NewFlagSet(command.State.String(), pflag.ContinueOnError)
flagSet.SetOutput(io.Discard)
flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before processing tfstate.")
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run state command in relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", "Which project to run state command for. Refers to the name of the project configured in a repo config file. Cannot be used at same time as workspace or dir flags.")
flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.")
default: default:
return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q this is a bug", cmd)} return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q this is a bug", cmd)}
} }
// Now parse the flags. subName, extraArgs, errResult := e.parseArgs(name, args, flagSet)
// It's safe to use [2:] because we know there's at least 2 elements in args. if errResult != "" {
err = flagSet.Parse(args[2:]) return CommentParseResult{CommentResponse: errResult}
if err == pflag.ErrHelp {
return CommentParseResult{CommentResponse: fmt.Sprintf("```\nUsage of %s:\n%s\n```", name.DefaultUsage(), flagSet.FlagUsagesWrapped(usagesCols))}
} }
if err != nil {
if cmd == command.Unlock.String() {
return CommentParseResult{CommentResponse: fmt.Sprintf(UnlockUsage, e.ExecutableName)}
}
return CommentParseResult{CommentResponse: e.errMarkdown(err.Error(), cmd, flagSet)}
}
var commandArgs []string // commandArgs are the arguments that are passed before `--` without any parameter flags.
if flagSet.ArgsLenAtDash() == -1 {
commandArgs = flagSet.Args()
} else {
commandArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()]
}
if len(commandArgs) != requiredCommandArgCount {
if requiredCommandArgCount > 0 {
return CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf("invalid argument count %s", strings.Join(commandArgs, " ")), name.DefaultUsage(), flagSet)}
}
return CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf("unknown argument(s) %s", strings.Join(commandArgs, " ")), name.DefaultUsage(), flagSet)}
}
var extraArgs []string // command extra_args
if flagSet.ArgsLenAtDash() != -1 {
extraArgs = append(extraArgs, flagSet.Args()[flagSet.ArgsLenAtDash():]...)
}
// pass commandArgs into extraArgs after extra args.
// - after comment_parser, we will use extra_args only.
// - terraform command args accept after options like followings
// - from: `atlantis import ADDRESS ID -- -var foo=bar
// - to: `terraform import -var foo=bar ADDRESS ID`
extraArgs = append(extraArgs, commandArgs...)
dir, err = e.validateDir(dir) dir, err = e.validateDir(dir)
if err != nil { if err != nil {
@@ -316,10 +291,73 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com
} }
return CommentParseResult{ return CommentParseResult{
Command: NewCommentCommand(dir, extraArgs, name, verbose, autoMergeDisabled, workspace, project), Command: NewCommentCommand(dir, extraArgs, name, subName, verbose, autoMergeDisabled, workspace, project),
} }
} }
func (e *CommentParser) parseArgs(name command.Name, args []string, flagSet *pflag.FlagSet) (string, []string, string) {
// Now parse the flags.
// It's safe to use [2:] because we know there's at least 2 elements in args.
err := flagSet.Parse(args[2:])
if err == pflag.ErrHelp {
return "", nil, fmt.Sprintf("```\nUsage of %s:\n%s\n```", name.DefaultUsage(), flagSet.FlagUsagesWrapped(usagesCols))
}
if err != nil {
if name == command.Unlock {
return "", nil, fmt.Sprintf(UnlockUsage, e.ExecutableName)
}
return "", nil, e.errMarkdown(err.Error(), name.String(), flagSet)
}
var commandArgs []string // commandArgs are the arguments that are passed before `--` without any parameter flags.
if flagSet.ArgsLenAtDash() == -1 {
commandArgs = flagSet.Args()
} else {
commandArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()]
}
// If command require subcommand, get it from command args
var subCommand string
availableSubCommands := name.SubCommands()
if len(availableSubCommands) > 0 { // command requires a subcommand
if len(commandArgs) < 1 {
return "", nil, e.errMarkdown("subcommand required", name.String(), flagSet)
}
subCommand, commandArgs = commandArgs[0], commandArgs[1:]
isAvailableSubCommand := slices.Contains(availableSubCommands, subCommand)
if !isAvailableSubCommand {
errMsg := fmt.Sprintf("invalid subcommand %s (not %s)", subCommand, strings.Join(availableSubCommands, ", "))
return "", nil, e.errMarkdown(errMsg, name.String(), flagSet)
}
}
// check command args count requirements
commandArgCount, err := name.CommandArgCount(subCommand)
if err != nil {
return "", nil, e.errMarkdown(err.Error(), name.String(), flagSet)
}
if !commandArgCount.IsMatchCount(len(commandArgs)) {
return "", nil, e.errMarkdown(fmt.Sprintf("unknown argument(s) %s", strings.Join(commandArgs, " ")), name.DefaultUsage(), flagSet)
}
var extraArgs []string // command extra_args
if flagSet.ArgsLenAtDash() != -1 {
extraArgs = append(extraArgs, flagSet.Args()[flagSet.ArgsLenAtDash():]...)
}
// pass commandArgs into extraArgs after extra args.
// - after comment_parser, we will use extra_args only.
// - terraform command args accept after options like followings
// - e.g.
// - from: `atlantis import ADDRESS ID -- -var foo=bar
// - to: `terraform import -var foo=bar ADDRESS ID`
// - e.g.
// - from: `atlantis state rm ADDRESS1 ADDRESS2 -- -var foo=bar
// - to: `terraform state rm -var foo=bar ADDRESS1 ADDRESS2` (subcommand=rm)
extraArgs = append(extraArgs, commandArgs...)
return subCommand, extraArgs, ""
}
// BuildPlanComment builds a plan comment for the specified args. // BuildPlanComment builds a plan comment for the specified args.
func (e *CommentParser) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string { func (e *CommentParser) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string {
flags := e.buildFlags(repoRelDir, workspace, project, false) flags := e.buildFlags(repoRelDir, workspace, project, false)
@@ -424,6 +462,7 @@ func (e *CommentParser) HelpComment() string {
AllowUnlock bool AllowUnlock bool
AllowApprovePolicies bool AllowApprovePolicies bool
AllowImport bool AllowImport bool
AllowState bool
}{ }{
ExecutableName: e.ExecutableName, ExecutableName: e.ExecutableName,
AllowVersion: e.isAllowedCommand(command.Version.String()), AllowVersion: e.isAllowedCommand(command.Version.String()),
@@ -432,6 +471,7 @@ func (e *CommentParser) HelpComment() string {
AllowUnlock: e.isAllowedCommand(command.Unlock.String()), AllowUnlock: e.isAllowedCommand(command.Unlock.String()),
AllowApprovePolicies: e.isAllowedCommand(command.ApprovePolicies.String()), AllowApprovePolicies: e.isAllowedCommand(command.ApprovePolicies.String()),
AllowImport: e.isAllowedCommand(command.Import.String()), AllowImport: e.isAllowedCommand(command.Import.String()),
AllowState: e.isAllowedCommand(command.State.String()),
}); err != nil { }); err != nil {
return fmt.Sprintf("Failed to render template, this is a bug: %v", err) return fmt.Sprintf("Failed to render template, this is a bug: %v", err)
} }
@@ -483,8 +523,14 @@ Commands:
version Print the output of 'terraform version' version Print the output of 'terraform version'
{{- end }} {{- end }}
{{- if .AllowImport }} {{- if .AllowImport }}
import Runs 'terraform import' for the changes in this pull request. import ADDRESS ID
To plan a specific project, use the -d, -w and -p flags. Runs 'terraform import' for the passed address resource.
To import a specific project, use the -d, -w and -p flags.
{{- end }}
{{- if .AllowState }}
state rm ADDRESS...
Runs 'terraform state rm' for the passed address resource.
To remove a specific project resource, use the -d, -w and -p flags.
{{- end }} {{- end }}
help View help. help View help.

View File

@@ -29,13 +29,7 @@ var commentParser = events.CommentParser{
GithubUser: "github-user", GithubUser: "github-user",
GitlabUser: "gitlab-user", GitlabUser: "gitlab-user",
ExecutableName: "atlantis", ExecutableName: "atlantis",
AllowCommands: []command.Name{ AllowCommands: command.AllCommentCommands,
command.Plan,
command.Apply,
command.Unlock,
command.ApprovePolicies,
command.Import,
},
} }
func TestNewCommentParser(t *testing.T) { func TestNewCommentParser(t *testing.T) {
@@ -151,82 +145,69 @@ func TestParse_HelpResponse(t *testing.T) {
func TestParse_UnusedArguments(t *testing.T) { func TestParse_UnusedArguments(t *testing.T) {
t.Log("if there are unused flags we return an error") t.Log("if there are unused flags we return an error")
cases := []struct { cases := []struct {
Command command.Name Command command.Name
Args string Args string
Unused string Unused string
ExpMessage string
}{ }{
{ {
command.Plan, command.Plan,
"-d . arg", "-d . arg",
"arg", "arg",
"unknown argument(s)",
}, },
{ {
command.Plan, command.Plan,
"arg -d .", "arg -d .",
"arg", "arg",
"unknown argument(s)",
}, },
{ {
command.Plan, command.Plan,
"arg", "arg",
"arg", "arg",
"unknown argument(s)",
}, },
{ {
command.Plan, command.Plan,
"arg arg2", "arg arg2",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.Plan, command.Plan,
"-d . arg -w kjj arg2", "-d . arg -w kjj arg2",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.Apply, command.Apply,
"-d . arg", "-d . arg",
"arg", "arg",
"unknown argument(s)",
}, },
{ {
command.Apply, command.Apply,
"arg arg2", "arg arg2",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.Apply, command.Apply,
"arg arg2 -- useful", "arg arg2 -- useful",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.Apply, command.Apply,
"arg arg2 --", "arg arg2 --",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.ApprovePolicies, command.ApprovePolicies,
"arg arg2 --", "arg arg2 --",
"arg arg2", "arg arg2",
"unknown argument(s)",
}, },
{ {
command.Import, command.Import,
"arg --", "arg --",
"arg", "arg",
"invalid argument count",
}, },
{ {
command.Import, command.Import,
"arg1 arg2 arg3 --", "arg1 arg2 arg3 --",
"arg1 arg2 arg3", "arg1 arg2 arg3",
"invalid argument count",
}, },
} }
for _, c := range cases { for _, c := range cases {
@@ -244,7 +225,7 @@ func TestParse_UnusedArguments(t *testing.T) {
case command.Import: case command.Import:
usage = ImportUsage usage = ImportUsage
} }
Equals(t, fmt.Sprintf("```\nError: %s %s.\n%s```", c.ExpMessage, c.Unused, usage), r.CommentResponse) Equals(t, fmt.Sprintf("```\nError: unknown argument(s) %s.\n%s```", c.Unused, usage), r.CommentResponse)
}) })
} }
} }
@@ -319,6 +300,8 @@ func TestParse_SubcommandUsage(t *testing.T) {
{"atlantis approve_policies --help", "approve_policies"}, {"atlantis approve_policies --help", "approve_policies"},
{"atlantis import -h", "import ADDRESS ID"}, {"atlantis import -h", "import ADDRESS ID"},
{"atlantis import --help", "import ADDRESS ID"}, {"atlantis import --help", "import ADDRESS ID"},
{"atlantis state -h", "state [rm ADDRESS...]"},
{"atlantis state --help", "state [rm ADDRESS...]"},
} }
for _, c := range tests { for _, c := range tests {
r := commentParser.Parse(c.input, models.Github) r := commentParser.Parse(c.input, models.Github)
@@ -357,6 +340,10 @@ func TestParse_InvalidFlags(t *testing.T) {
"atlantis import --abc", "atlantis import --abc",
"Error: unknown flag: --abc", "Error: unknown flag: --abc",
}, },
{
"atlantis state rm --abc",
"Error: unknown flag: --abc",
},
} }
for _, c := range cases { for _, c := range cases {
r := commentParser.Parse(c.comment, models.Github) r := commentParser.Parse(c.comment, models.Github)
@@ -373,16 +360,20 @@ func TestParse_RelativeDirPath(t *testing.T) {
"atlantis plan -d ..", "atlantis plan -d ..",
"atlantis apply -d ..", "atlantis apply -d ..",
"atlantis import -d .. address id", "atlantis import -d .. address id",
"atlantis state -d .. rm address",
// These won't return an error because we prepend with . when parsing. // These won't return an error because we prepend with . when parsing.
//"atlantis plan -d /..", //"atlantis plan -d /..",
//"atlantis apply -d /..", //"atlantis apply -d /..",
//"atlantis import -d /.. address id", //"atlantis import -d /.. address id",
//"atlantis state rm -d /.. address",
"atlantis plan -d ./..", "atlantis plan -d ./..",
"atlantis apply -d ./..", "atlantis apply -d ./..",
"atlantis import -d ./.. address id", "atlantis import -d ./.. address id",
"atlantis state -d ./.. rm address",
"atlantis plan -d a/b/../../..", "atlantis plan -d a/b/../../..",
"atlantis apply -d a/../..", "atlantis apply -d a/../..",
"atlantis import -d a/../.. address id", "atlantis import -d a/../.. address id",
"atlantis state -d a/../.. rm address id",
} }
for _, c := range comments { for _, c := range comments {
r := commentParser.Parse(c, models.Github) r := commentParser.Parse(c, models.Github)
@@ -427,15 +418,19 @@ func TestParse_InvalidWorkspace(t *testing.T) {
"atlantis plan -w ..", "atlantis plan -w ..",
"atlantis apply -w ..", "atlantis apply -w ..",
"atlantis import -w .. address id", "atlantis import -w .. address id",
"atlantis import -w .. rm address",
"atlantis plan -w /", "atlantis plan -w /",
"atlantis apply -w /", "atlantis apply -w /",
"atlantis import -w / address id", "atlantis import -w / address id",
"atlantis state -w / rm address",
"atlantis plan -w ..abc", "atlantis plan -w ..abc",
"atlantis apply -w abc..", "atlantis apply -w abc..",
"atlantis import -w abc.. address id", "atlantis import -w abc.. address id",
"atlantis state -w abc.. rm address",
"atlantis plan -w abc..abc", "atlantis plan -w abc..abc",
"atlantis apply -w ../../../etc/passwd", "atlantis apply -w ../../../etc/passwd",
"atlantis import -w ../../../etc/passwd address id", "atlantis import -w ../../../etc/passwd address id",
"atlantis state -w ../../../etc/passwd rm address",
} }
for _, c := range comments { for _, c := range comments {
r := commentParser.Parse(c, models.Github) r := commentParser.Parse(c, models.Github)
@@ -681,7 +676,7 @@ func TestParse_Parsing(t *testing.T) {
} }
for _, test := range cases { for _, test := range cases {
for _, cmdName := range []string{"plan", "apply", "import 'some[\"addr\"]' id"} { for _, cmdName := range []string{"plan", "apply", "import 'some[\"addr\"]' id", "state rm 'some[\"addr\"]'"} {
comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags) comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags)
t.Run(comment, func(t *testing.T) { t.Run(comment, func(t *testing.T) {
r := commentParser.Parse(comment, models.Github) r := commentParser.Parse(comment, models.Github)
@@ -710,6 +705,15 @@ func TestParse_Parsing(t *testing.T) {
Assert(t, r.Command.Name == command.Import, "did not parse comment %q as import command", comment) Assert(t, r.Command.Name == command.Import, "did not parse comment %q as import command", comment)
Assert(t, expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", expExtraArgs, actExtraArgs, comment) Assert(t, expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", expExtraArgs, actExtraArgs, comment)
} }
if strings.HasPrefix(cmdName, "state rm") {
expExtraArgs := "some[\"addr\"]" // state rm use default args with `some["addr"]`
if test.expExtraArgs != "" {
expExtraArgs = fmt.Sprintf("%s %s", test.expExtraArgs, expExtraArgs)
}
Assert(t, r.Command.Name == command.State, "did not parse comment %q as state command", comment)
Assert(t, r.Command.SubName == "rm", "did not parse comment %q as state rm subcommand", comment)
Assert(t, expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", expExtraArgs, actExtraArgs, comment)
}
}) })
} }
} }
@@ -873,8 +877,12 @@ Commands:
approve_policies approve_policies
Approves all current policy checking failures for the PR. Approves all current policy checking failures for the PR.
version Print the output of 'terraform version' version Print the output of 'terraform version'
import Runs 'terraform import' for the changes in this pull request. import ADDRESS ID
To plan a specific project, use the -d, -w and -p flags. Runs 'terraform import' for the passed address resource.
To import a specific project, use the -d, -w and -p flags.
state rm ADDRESS...
Runs 'terraform state rm' for the passed address resource.
To remove a specific project resource, use the -d, -w and -p flags.
help View help. help View help.
Flags: Flags:

View File

@@ -38,6 +38,8 @@ const usagesCols = 90
type PullCommand interface { type PullCommand interface {
// CommandName is the name of the command we're running. // CommandName is the name of the command we're running.
CommandName() command.Name CommandName() command.Name
// SubCommandName is the subcommand name of the command we're running.
SubCommandName() string
// IsVerbose is true if the output of this command should be verbose. // IsVerbose is true if the output of this command should be verbose.
IsVerbose() bool IsVerbose() bool
// IsAutoplan is true if this is an autoplan command vs. a comment command. // IsAutoplan is true if this is an autoplan command vs. a comment command.
@@ -53,6 +55,11 @@ func (c PolicyCheckCommand) CommandName() command.Name {
return command.PolicyCheck return command.PolicyCheck
} }
// SubCommandName is a subcommand for policy_check.
func (c PolicyCheckCommand) SubCommandName() string {
return ""
}
// IsVerbose is false for policy_check commands. // IsVerbose is false for policy_check commands.
func (c PolicyCheckCommand) IsVerbose() bool { func (c PolicyCheckCommand) IsVerbose() bool {
return false return false
@@ -72,6 +79,11 @@ func (c AutoplanCommand) CommandName() command.Name {
return command.Plan return command.Plan
} }
// SubCommandName is a subcommand for auto plan.
func (c AutoplanCommand) SubCommandName() string {
return ""
}
// IsVerbose is false for autoplan commands. // IsVerbose is false for autoplan commands.
func (c AutoplanCommand) IsVerbose() bool { func (c AutoplanCommand) IsVerbose() bool {
return false return false
@@ -92,6 +104,8 @@ type CommentCommand struct {
Flags []string Flags []string
// Name is the name of the command the comment specified. // Name is the name of the command the comment specified.
Name command.Name Name command.Name
// SubName is the name of the sub command the comment specified.
SubName string
// AutoMergeDisabled is true if the command should not automerge after apply. // AutoMergeDisabled is true if the command should not automerge after apply.
AutoMergeDisabled bool AutoMergeDisabled bool
// Verbose is true if the command should output verbosely. // Verbose is true if the command should output verbosely.
@@ -117,6 +131,11 @@ func (c CommentCommand) CommandName() command.Name {
return c.Name return c.Name
} }
// SubCommandName returns the name of this subcommand.
func (c CommentCommand) SubCommandName() string {
return c.SubName
}
// IsVerbose is true if the command should give verbose output. // IsVerbose is true if the command should give verbose output.
func (c CommentCommand) IsVerbose() bool { func (c CommentCommand) IsVerbose() bool {
return c.Verbose return c.Verbose
@@ -133,7 +152,7 @@ func (c CommentCommand) String() string {
} }
// NewCommentCommand constructs a CommentCommand, setting all missing fields to defaults. // NewCommentCommand constructs a CommentCommand, setting all missing fields to defaults.
func NewCommentCommand(repoRelDir string, flags []string, name command.Name, verbose, autoMergeDisabled bool, workspace string, project string) *CommentCommand { func NewCommentCommand(repoRelDir string, flags []string, name command.Name, subName string, verbose, autoMergeDisabled bool, workspace string, project string) *CommentCommand {
// If repoRelDir was empty we want to keep it that way to indicate that it // If repoRelDir was empty we want to keep it that way to indicate that it
// wasn't specified in the comment. // wasn't specified in the comment.
if repoRelDir != "" { if repoRelDir != "" {
@@ -146,6 +165,7 @@ func NewCommentCommand(repoRelDir string, flags []string, name command.Name, ver
RepoRelDir: repoRelDir, RepoRelDir: repoRelDir,
Flags: flags, Flags: flags,
Name: name, Name: name,
SubName: subName,
Verbose: verbose, Verbose: verbose,
Workspace: workspace, Workspace: workspace,
AutoMergeDisabled: autoMergeDisabled, AutoMergeDisabled: autoMergeDisabled,

View File

@@ -729,14 +729,14 @@ func TestNewCommand_CleansDir(t *testing.T) {
for _, c := range cases { for _, c := range cases {
t.Run(c.RepoRelDir, func(t *testing.T) { t.Run(c.RepoRelDir, func(t *testing.T) {
cmd := events.NewCommentCommand(c.RepoRelDir, nil, command.Plan, false, false, "workspace", "") cmd := events.NewCommentCommand(c.RepoRelDir, nil, command.Plan, "", false, false, "workspace", "")
Equals(t, c.ExpDir, cmd.RepoRelDir) Equals(t, c.ExpDir, cmd.RepoRelDir)
}) })
} }
} }
func TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) { func TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) {
cmd := events.NewCommentCommand("", nil, command.Plan, false, false, "", "") cmd := events.NewCommentCommand("", nil, command.Plan, "", false, false, "", "")
Equals(t, events.CommentCommand{ Equals(t, events.CommentCommand{
RepoRelDir: "", RepoRelDir: "",
Flags: nil, Flags: nil,
@@ -748,7 +748,7 @@ func TestNewCommand_EmptyDirWorkspaceProject(t *testing.T) {
} }
func TestNewCommand_AllFieldsSet(t *testing.T) { func TestNewCommand_AllFieldsSet(t *testing.T) {
cmd := events.NewCommentCommand("dir", []string{"a", "b"}, command.Plan, true, false, "workspace", "project") cmd := events.NewCommentCommand("dir", []string{"a", "b"}, command.Plan, "", true, false, "workspace", "project")
Equals(t, events.CommentCommand{ Equals(t, events.CommentCommand{
Workspace: "workspace", Workspace: "workspace",
RepoRelDir: "dir", RepoRelDir: "dir",

View File

@@ -40,7 +40,7 @@ func (b *InstrumentedProjectCommandBuilder) BuildPlanCommands(ctx *command.Conte
) )
} }
func (b *InstrumentedProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) { func (b *InstrumentedProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {
return b.buildAndEmitStats( return b.buildAndEmitStats(
"import", "import",
func() ([]command.ProjectContext, error) { func() ([]command.ProjectContext, error) {
@@ -49,6 +49,15 @@ func (b *InstrumentedProjectCommandBuilder) BuildVersionCommands(ctx *command.Co
) )
} }
func (b *InstrumentedProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) {
return b.buildAndEmitStats(
"state rm",
func() ([]command.ProjectContext, error) {
return b.ProjectCommandBuilder.BuildStateRmCommands(ctx, comment)
},
)
}
func (b *InstrumentedProjectCommandBuilder) buildAndEmitStats( func (b *InstrumentedProjectCommandBuilder) buildAndEmitStats(
command string, command string,
execute func() ([]command.ProjectContext, error), execute func() ([]command.ProjectContext, error),

View File

@@ -12,6 +12,7 @@ type IntrumentedCommandRunner interface {
Apply(ctx command.ProjectContext) command.ProjectResult Apply(ctx command.ProjectContext) command.ProjectResult
ApprovePolicies(ctx command.ProjectContext) command.ProjectResult ApprovePolicies(ctx command.ProjectContext) command.ProjectResult
Import(ctx command.ProjectContext) command.ProjectResult Import(ctx command.ProjectContext) command.ProjectResult
StateRm(ctx command.ProjectContext) command.ProjectResult
} }
type InstrumentedProjectCommandRunner struct { type InstrumentedProjectCommandRunner struct {
@@ -53,6 +54,10 @@ func (p *InstrumentedProjectCommandRunner) Import(ctx command.ProjectContext) co
return RunAndEmitStats("import", ctx, p.projectCommandRunner.Import, p.scope) return RunAndEmitStats("import", ctx, p.projectCommandRunner.Import, p.scope)
} }
func (p *InstrumentedProjectCommandRunner) StateRm(ctx command.ProjectContext) command.ProjectResult {
return RunAndEmitStats("state rm", ctx, p.projectCommandRunner.StateRm, p.scope)
}
func RunAndEmitStats(commandName string, ctx command.ProjectContext, execute func(ctx command.ProjectContext) command.ProjectResult, scope tally.Scope) command.ProjectResult { func RunAndEmitStats(commandName string, ctx command.ProjectContext, execute func(ctx command.ProjectContext) command.ProjectResult, scope tally.Scope) command.ProjectResult {
// ensures we are differentiating between project level command and overall command // ensures we are differentiating between project level command and overall command
scope = ctx.SetProjectScopeTags(scope) scope = ctx.SetProjectScopeTags(scope)

View File

@@ -34,6 +34,7 @@ var (
approvePoliciesCommandTitle = command.ApprovePolicies.TitleString() approvePoliciesCommandTitle = command.ApprovePolicies.TitleString()
versionCommandTitle = command.Version.TitleString() versionCommandTitle = command.Version.TitleString()
importCommandTitle = command.Import.TitleString() importCommandTitle = command.Import.TitleString()
stateCommandTitle = command.State.TitleString()
// maxUnwrappedLines is the maximum number of lines the Terraform output // maxUnwrappedLines is the maximum number of lines the Terraform output
// can be before we wrap it in an expandable template. // can be before we wrap it in an expandable template.
maxUnwrappedLines = 12 maxUnwrappedLines = 12
@@ -60,6 +61,7 @@ type MarkdownRenderer struct {
// commonData is data that all responses have. // commonData is data that all responses have.
type commonData struct { type commonData struct {
Command string Command string
SubCommand string
Verbose bool Verbose bool
Log string Log string
PlansDeleted bool PlansDeleted bool
@@ -140,10 +142,11 @@ func NewMarkdownRenderer(
// Render formats the data into a markdown string. // Render formats the data into a markdown string.
// nolint: interfacer // nolint: interfacer
func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, log string, verbose bool, vcsHost models.VCSHostType) string { func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, subCmd, log string, verbose bool, vcsHost models.VCSHostType) string {
commandStr := cases.Title(language.English).String(strings.Replace(cmdName.String(), "_", " ", -1)) commandStr := cases.Title(language.English).String(strings.Replace(cmdName.String(), "_", " ", -1))
common := commonData{ common := commonData{
Command: commandStr, Command: commandStr,
SubCommand: subCmd,
Verbose: verbose, Verbose: verbose,
Log: log, Log: log,
PlansDeleted: res.PlansDeleted, PlansDeleted: res.PlansDeleted,
@@ -225,6 +228,13 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
} else { } else {
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("importSuccessUnwrapped"), result.ImportSuccess) resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("importSuccessUnwrapped"), result.ImportSuccess)
} }
} else if result.StateRmSuccess != nil {
result.StateRmSuccess.Output = strings.TrimSpace(result.StateRmSuccess.Output)
if m.shouldUseWrappedTmpl(vcsHost, result.StateRmSuccess.Output) {
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("stateRmSuccessWrapped"), result.StateRmSuccess)
} else {
resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("stateRmSuccessUnwrapped"), result.StateRmSuccess)
}
} else { } else {
resultData.Rendered = "Found no template. This is a bug!" resultData.Rendered = "Found no template. This is a bug!"
} }
@@ -249,6 +259,13 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
tmpl = templates.Lookup("singleProjectApply") tmpl = templates.Lookup("singleProjectApply")
case len(resultsTmplData) == 1 && common.Command == importCommandTitle: case len(resultsTmplData) == 1 && common.Command == importCommandTitle:
tmpl = templates.Lookup("singleProjectImport") tmpl = templates.Lookup("singleProjectImport")
case len(resultsTmplData) == 1 && common.Command == stateCommandTitle:
switch common.SubCommand {
case "rm":
tmpl = templates.Lookup("singleProjectStateRm")
default:
return fmt.Sprintf("no template matchedthis is a bug: command=%s, subcommand=%s", common.Command, common.SubCommand)
}
case common.Command == planCommandTitle, case common.Command == planCommandTitle,
common.Command == policyCheckCommandTitle: common.Command == policyCheckCommandTitle:
tmpl = templates.Lookup("multiProjectPlan") tmpl = templates.Lookup("multiProjectPlan")
@@ -260,8 +277,15 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
tmpl = templates.Lookup("multiProjectVersion") tmpl = templates.Lookup("multiProjectVersion")
case common.Command == importCommandTitle: case common.Command == importCommandTitle:
tmpl = templates.Lookup("multiProjectImport") tmpl = templates.Lookup("multiProjectImport")
case common.Command == stateCommandTitle:
switch common.SubCommand {
case "rm":
tmpl = templates.Lookup("multiProjectStateRm")
default:
return fmt.Sprintf("no template matchedthis is a bug: command=%s, subcommand=%s", common.Command, common.SubCommand)
}
default: default:
return "no template matchedthis is a bug" return fmt.Sprintf("no template matchedthis is a bug: command=%s", common.Command)
} }
return m.renderTemplateTrimSpace(tmpl, resultData{resultsTmplData, common}) return m.renderTemplateTrimSpace(tmpl, resultData{resultsTmplData, common})
} }

View File

@@ -64,7 +64,7 @@ func TestRenderErr(t *testing.T) {
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) { t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, models.Github) s := r.Render(res, c.Command, "", "log", verbose, models.Github)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s))
} else { } else {
@@ -109,7 +109,7 @@ func TestRenderFailure(t *testing.T) {
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) { t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, models.Github) s := r.Render(res, c.Command, "", "log", verbose, models.Github)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s))
} else { } else {
@@ -126,7 +126,7 @@ func TestRenderErrAndFailure(t *testing.T) {
Error: errors.New("error"), Error: errors.New("error"),
Failure: "failure", Failure: "failure",
} }
s := r.Render(res, command.Plan, "", false, models.Github) s := r.Render(res, command.Plan, "", "", false, models.Github)
Equals(t, "**Plan Error**\n```\nerror\n```", s) Equals(t, "**Plan Error**\n```\nerror\n```", s)
} }
@@ -134,6 +134,7 @@ func TestRenderProjectResults(t *testing.T) {
cases := []struct { cases := []struct {
Description string Description string
Command command.Name Command command.Name
SubCommand string
ProjectResults []command.ProjectResult ProjectResults []command.ProjectResult
VCSHost models.VCSHostType VCSHost models.VCSHostType
Expected string Expected string
@@ -141,6 +142,7 @@ func TestRenderProjectResults(t *testing.T) {
{ {
"no projects", "no projects",
command.Plan, command.Plan,
"",
[]command.ProjectResult{}, []command.ProjectResult{},
models.Github, models.Github,
"Ran Plan for 0 projects:\n\n\n", "Ran Plan for 0 projects:\n\n\n",
@@ -148,6 +150,7 @@ func TestRenderProjectResults(t *testing.T) {
{ {
"single successful plan", "single successful plan",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
PlanSuccess: &models.PlanSuccess{ PlanSuccess: &models.PlanSuccess{
@@ -183,6 +186,7 @@ $$$
{ {
"single successful plan with main ahead", "single successful plan with main ahead",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
PlanSuccess: &models.PlanSuccess{ PlanSuccess: &models.PlanSuccess{
@@ -221,6 +225,7 @@ $$$
{ {
"single successful plan with project name", "single successful plan with project name",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
PlanSuccess: &models.PlanSuccess{ PlanSuccess: &models.PlanSuccess{
@@ -257,6 +262,7 @@ $$$
{ {
"single successful policy check with project name", "single successful policy check with project name",
command.PolicyCheck, command.PolicyCheck,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
PolicyCheckSuccess: &models.PolicyCheckSuccess{ PolicyCheckSuccess: &models.PolicyCheckSuccess{
@@ -306,6 +312,7 @@ $$$
{ {
"single successful import", "single successful import",
command.Import, command.Import,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
ImportSuccess: &models.ImportSuccess{ ImportSuccess: &models.ImportSuccess{
@@ -324,12 +331,43 @@ $$$diff
import-output import-output
$$$ $$$
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* $atlantis plan -d path -w workspace$`, * $atlantis plan -d path -w workspace$`,
},
{
"single successful state rm",
command.State,
"rm",
[]command.ProjectResult{
{
StateRmSuccess: &models.StateRmSuccess{
Output: "state-rm-output",
RePlanCmd: "atlantis plan -d path -w workspace",
},
Workspace: "workspace",
RepoRelDir: "path",
ProjectName: "projectname",
},
},
models.Github,
`Ran State $rm$ for project: $projectname$ dir: $path$ workspace: $workspace$
$$$diff
state-rm-output
$$$
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* $atlantis plan -d path -w workspace$
`,
}, },
{ {
"single successful apply", "single successful apply",
command.Apply, command.Apply,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
ApplySuccess: "success", ApplySuccess: "success",
@@ -347,6 +385,7 @@ $$$`,
{ {
"single successful apply with project name", "single successful apply with project name",
command.Apply, command.Apply,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
ApplySuccess: "success", ApplySuccess: "success",
@@ -365,6 +404,7 @@ $$$`,
{ {
"multiple successful plans", "multiple successful plans",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -427,6 +467,7 @@ $$$
{ {
"multiple successful policy checks", "multiple successful policy checks",
command.PolicyCheck, command.PolicyCheck,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -489,6 +530,7 @@ $$$
{ {
"multiple successful applies", "multiple successful applies",
command.Apply, command.Apply,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
RepoRelDir: "path", RepoRelDir: "path",
@@ -525,6 +567,7 @@ $$$
{ {
"single errored plan", "single errored plan",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Error: errors.New("error"), Error: errors.New("error"),
@@ -543,6 +586,7 @@ $$$`,
{ {
"single failed plan", "single failed plan",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
RepoRelDir: "path", RepoRelDir: "path",
@@ -558,6 +602,7 @@ $$$`,
{ {
"successful, failed, and errored plan", "successful, failed, and errored plan",
command.Plan, command.Plan,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -620,6 +665,7 @@ $$$
{ {
"successful, failed, and errored policy check", "successful, failed, and errored policy check",
command.PolicyCheck, command.PolicyCheck,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -685,6 +731,7 @@ $$$
{ {
"successful, failed, and errored apply", "successful, failed, and errored apply",
command.Apply, command.Apply,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -731,6 +778,7 @@ $$$
{ {
"successful, failed, and errored apply", "successful, failed, and errored apply",
command.Apply, command.Apply,
"",
[]command.ProjectResult{ []command.ProjectResult{
{ {
Workspace: "workspace", Workspace: "workspace",
@@ -784,7 +832,7 @@ $$$
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, c.VCSHost) s := r.Render(res, c.Command, c.SubCommand, "log", verbose, c.VCSHost)
expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
@@ -939,7 +987,7 @@ $$$
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, c.VCSHost) s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
@@ -1087,7 +1135,7 @@ $$$
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, c.VCSHost) s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
@@ -1132,7 +1180,7 @@ func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) {
}, },
}, },
}, },
}, command.PolicyCheck, "log", false, models.Github) }, command.PolicyCheck, "", "log", false, models.Github)
exp := "Ran Policy Check for dir: `path` workspace: `workspace`\n\nsomecustometext" exp := "Ran Policy Check for dir: `path` workspace: `workspace`\n\nsomecustometext"
Equals(t, exp, rendered) Equals(t, exp, rendered)
} }
@@ -1158,7 +1206,7 @@ func TestRenderProjectResults_DisableFolding(t *testing.T) {
Error: errors.New(strings.Repeat("line\n", 13)), Error: errors.New(strings.Repeat("line\n", 13)),
}, },
}, },
}, command.Plan, "log", false, models.Github) }, command.Plan, "", "log", false, models.Github)
Equals(t, false, strings.Contains(rendered, "\n<details>")) Equals(t, false, strings.Contains(rendered, "\n<details>"))
} }
@@ -1249,7 +1297,7 @@ func TestRenderProjectResults_WrappedErr(t *testing.T) {
Error: errors.New(c.Output), Error: errors.New(c.Output),
}, },
}, },
}, command.Plan, "log", false, c.VCSHost) }, command.Plan, "", "log", false, c.VCSHost)
var exp string var exp string
if c.ShouldWrap { if c.ShouldWrap {
exp = `Ran Plan for dir: $.$ workspace: $default$ exp = `Ran Plan for dir: $.$ workspace: $default$
@@ -1377,7 +1425,7 @@ func TestRenderProjectResults_WrapSingleProject(t *testing.T) {
} }
rendered := mr.Render(command.Result{ rendered := mr.Render(command.Result{
ProjectResults: []command.ProjectResult{pr}, ProjectResults: []command.ProjectResult{pr},
}, cmd, "log", false, c.VCSHost) }, cmd, "", "log", false, c.VCSHost)
// Check result. // Check result.
var exp string var exp string
@@ -1476,7 +1524,7 @@ func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) {
ApplySuccess: tfOut, ApplySuccess: tfOut,
}, },
}, },
}, command.Apply, "log", false, models.Github) }, command.Apply, "", "log", false, models.Github)
exp := `Ran Apply for 2 projects: exp := `Ran Apply for 2 projects:
1. dir: $.$ workspace: $staging$ 1. dir: $.$ workspace: $staging$
@@ -1541,7 +1589,7 @@ func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) {
}, },
}, },
}, },
}, command.Plan, "log", false, models.Github) }, command.Plan, "", "log", false, models.Github)
exp := `Ran Plan for 2 projects: exp := `Ran Plan for 2 projects:
1. dir: $.$ workspace: $staging$ 1. dir: $.$ workspace: $staging$
@@ -1692,7 +1740,7 @@ This plan was not saved because one or more projects failed and automerge requir
"", // MarkdownTemplateOverridesDir "", // MarkdownTemplateOverridesDir
"atlantis", // executableName "atlantis", // executableName
) )
rendered := mr.Render(c.cr, command.Plan, "log", false, models.Github) rendered := mr.Render(c.cr, command.Plan, "", "log", false, models.Github)
expWithBackticks := strings.Replace(c.exp, "$", "`", -1) expWithBackticks := strings.Replace(c.exp, "$", "`", -1)
Equals(t, expWithBackticks, rendered) Equals(t, expWithBackticks, rendered)
}) })
@@ -2157,7 +2205,7 @@ $$$
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, c.VCSHost) s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
@@ -2594,7 +2642,7 @@ func TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) {
} }
for _, verbose := range []bool{true, false} { for _, verbose := range []bool{true, false} {
t.Run(c.Description, func(t *testing.T) { t.Run(c.Description, func(t *testing.T) {
s := r.Render(res, c.Command, "log", verbose, c.VCSHost) s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
if !verbose { if !verbose {
Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
@@ -2632,7 +2680,7 @@ func BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) {
b.Run(fmt.Sprintf("verbose %t", verbose), func(b *testing.B) { b.Run(fmt.Sprintf("verbose %t", verbose), func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
render = r.Render(res, c.Command, "log", verbose, c.VCSHost) render = r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
} }
Render = render Render = render
}) })

View File

@@ -121,6 +121,25 @@ func (mock *MockProjectCommandBuilder) BuildPlanCommands(_param0 *command.Contex
return ret0, ret1 return ret0, ret1
} }
func (mock *MockProjectCommandBuilder) BuildStateRmCommands(_param0 *command.Context, _param1 *events.CommentCommand) ([]command.ProjectContext, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockProjectCommandBuilder().")
}
params := []pegomock.Param{_param0, _param1}
result := pegomock.GetGenericMockFrom(mock).Invoke("BuildStateRmCommands", params, []reflect.Type{reflect.TypeOf((*[]command.ProjectContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 []command.ProjectContext
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].([]command.ProjectContext)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockProjectCommandBuilder) BuildVersionCommands(_param0 *command.Context, _param1 *events.CommentCommand) ([]command.ProjectContext, error) { func (mock *MockProjectCommandBuilder) BuildVersionCommands(_param0 *command.Context, _param1 *events.CommentCommand) ([]command.ProjectContext, error) {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockProjectCommandBuilder().") panic("mock must not be nil. Use myMock := NewMockProjectCommandBuilder().")
@@ -328,6 +347,37 @@ func (c *MockProjectCommandBuilder_BuildPlanCommands_OngoingVerification) GetAll
return return
} }
func (verifier *VerifierMockProjectCommandBuilder) BuildStateRmCommands(_param0 *command.Context, _param1 *events.CommentCommand) *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification {
params := []pegomock.Param{_param0, _param1}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildStateRmCommands", params, verifier.timeout)
return &MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification struct {
mock *MockProjectCommandBuilder
methodInvocations []pegomock.MethodInvocation
}
func (c *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) {
_param0, _param1 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1], _param1[len(_param1)-1]
}
func (c *MockProjectCommandBuilder_BuildStateRmCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*command.Context, _param1 []*events.CommentCommand) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]*command.Context, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(*command.Context)
}
_param1 = make([]*events.CommentCommand, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(*events.CommentCommand)
}
}
return
}
func (verifier *VerifierMockProjectCommandBuilder) BuildVersionCommands(_param0 *command.Context, _param1 *events.CommentCommand) *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification { func (verifier *VerifierMockProjectCommandBuilder) BuildVersionCommands(_param0 *command.Context, _param1 *events.CommentCommand) *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification {
params := []pegomock.Param{_param0, _param1} params := []pegomock.Param{_param0, _param1}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildVersionCommands", params, verifier.timeout) methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildVersionCommands", params, verifier.timeout)

View File

@@ -100,6 +100,21 @@ func (mock *MockProjectCommandRunner) PolicyCheck(_param0 command.ProjectContext
return ret0 return ret0
} }
func (mock *MockProjectCommandRunner) StateRm(_param0 command.ProjectContext) command.ProjectResult {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockProjectCommandRunner().")
}
params := []pegomock.Param{_param0}
result := pegomock.GetGenericMockFrom(mock).Invoke("StateRm", params, []reflect.Type{reflect.TypeOf((*command.ProjectResult)(nil)).Elem()})
var ret0 command.ProjectResult
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(command.ProjectResult)
}
}
return ret0
}
func (mock *MockProjectCommandRunner) Version(_param0 command.ProjectContext) command.ProjectResult { func (mock *MockProjectCommandRunner) Version(_param0 command.ProjectContext) command.ProjectResult {
if mock == nil { if mock == nil {
panic("mock must not be nil. Use myMock := NewMockProjectCommandRunner().") panic("mock must not be nil. Use myMock := NewMockProjectCommandRunner().")
@@ -287,6 +302,33 @@ func (c *MockProjectCommandRunner_PolicyCheck_OngoingVerification) GetAllCapture
return return
} }
func (verifier *VerifierMockProjectCommandRunner) StateRm(_param0 command.ProjectContext) *MockProjectCommandRunner_StateRm_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "StateRm", params, verifier.timeout)
return &MockProjectCommandRunner_StateRm_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockProjectCommandRunner_StateRm_OngoingVerification struct {
mock *MockProjectCommandRunner
methodInvocations []pegomock.MethodInvocation
}
func (c *MockProjectCommandRunner_StateRm_OngoingVerification) GetCapturedArguments() command.ProjectContext {
_param0 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1]
}
func (c *MockProjectCommandRunner_StateRm_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]command.ProjectContext, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(command.ProjectContext)
}
}
return
}
func (verifier *VerifierMockProjectCommandRunner) Version(_param0 command.ProjectContext) *MockProjectCommandRunner_Version_OngoingVerification { func (verifier *VerifierMockProjectCommandRunner) Version(_param0 command.ProjectContext) *MockProjectCommandRunner_Version_OngoingVerification {
params := []pegomock.Param{_param0} params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Version", params, verifier.timeout) methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Version", params, verifier.timeout)

View File

@@ -430,6 +430,14 @@ type ImportSuccess struct {
RePlanCmd string RePlanCmd string
} }
// StateRmSuccess is the result of a successful state rm run.
type StateRmSuccess struct {
// Output is the output from terraform state rm
Output string
// RePlanCmd is the command that users should run to re-plan this project.
RePlanCmd string
}
// Summary extracts one line summary of policy check. // Summary extracts one line summary of policy check.
func (p *PolicyCheckSuccess) Summary() string { func (p *PolicyCheckSuccess) Summary() string {
note := "" note := ""

View File

@@ -165,6 +165,13 @@ type ProjectImportCommandBuilder interface {
BuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) BuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)
} }
type ProjectStateCommandBuilder interface {
// BuildStateRmCommands builds project state rm commands for this ctx and comment. If
// comment doesn't specify one project then there may be multiple commands
// to be run.
BuildStateRmCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error)
}
//go:generate pegomock generate -m --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder //go:generate pegomock generate -m --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder
// ProjectCommandBuilder builds commands that run on individual projects. // ProjectCommandBuilder builds commands that run on individual projects.
@@ -174,6 +181,7 @@ type ProjectCommandBuilder interface {
ProjectApprovePoliciesCommandBuilder ProjectApprovePoliciesCommandBuilder
ProjectVersionCommandBuilder ProjectVersionCommandBuilder
ProjectImportCommandBuilder ProjectImportCommandBuilder
ProjectStateCommandBuilder
} }
// DefaultProjectCommandBuilder implements ProjectCommandBuilder. // DefaultProjectCommandBuilder implements ProjectCommandBuilder.
@@ -199,7 +207,7 @@ type DefaultProjectCommandBuilder struct {
// See ProjectCommandBuilder.BuildAutoplanCommands. // See ProjectCommandBuilder.BuildAutoplanCommands.
func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) { func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) {
projCtxs, err := p.buildAllCommandsByCfg(ctx, command.Plan, nil, false) projCtxs, err := p.buildAllCommandsByCfg(ctx, command.Plan, "", nil, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -217,7 +225,7 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Contex
// See ProjectCommandBuilder.BuildPlanCommands. // See ProjectCommandBuilder.BuildPlanCommands.
func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
if !cmd.IsForSpecificProject() { if !cmd.IsForSpecificProject() {
return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.Flags, cmd.Verbose) return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)
} }
pcc, err := p.buildProjectPlanCommand(ctx, cmd) pcc, err := p.buildProjectPlanCommand(ctx, cmd)
return pcc, err return pcc, err
@@ -228,7 +236,7 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *command.Context,
if !cmd.IsForSpecificProject() { if !cmd.IsForSpecificProject() {
return p.buildAllProjectCommandsByPlan(ctx, cmd) return p.buildAllProjectCommandsByPlan(ctx, cmd)
} }
pac, err := p.buildProjectApplyCommand(ctx, cmd) pac, err := p.buildProjectCommand(ctx, cmd)
return pac, err return pac, err
} }
@@ -240,21 +248,29 @@ func (p *DefaultProjectCommandBuilder) BuildVersionCommands(ctx *command.Context
if !cmd.IsForSpecificProject() { if !cmd.IsForSpecificProject() {
return p.buildAllProjectCommandsByPlan(ctx, cmd) return p.buildAllProjectCommandsByPlan(ctx, cmd)
} }
pac, err := p.buildProjectVersionCommand(ctx, cmd) pac, err := p.buildProjectCommand(ctx, cmd)
return pac, err return pac, err
} }
func (p *DefaultProjectCommandBuilder) BuildImportCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { func (p *DefaultProjectCommandBuilder) BuildImportCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
if !cmd.IsForSpecificProject() { if !cmd.IsForSpecificProject() {
// import discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan. // import discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan.
return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.Flags, cmd.Verbose) return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)
} }
return p.buildProjectImportCommand(ctx, cmd) return p.buildProjectCommand(ctx, cmd)
}
func (p *DefaultProjectCommandBuilder) BuildStateRmCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
if !cmd.IsForSpecificProject() {
// state rm discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan.
return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose)
}
return p.buildProjectCommand(ctx, cmd)
} }
// buildAllCommandsByCfg builds init contexts for all projects we determine were // buildAllCommandsByCfg builds init contexts for all projects we determine were
// modified in this ctx. // modified in this ctx.
func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, subCmdName string, commentFlags []string, verbose bool) ([]command.ProjectContext, error) {
// We'll need the list of modified files. // We'll need the list of modified files.
modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Pull.BaseRepo, ctx.Pull) modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Pull.BaseRepo, ctx.Pull)
if err != nil { if err != nil {
@@ -345,6 +361,7 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
p.ProjectCommandContextBuilder.BuildProjectContext( p.ProjectCommandContextBuilder.BuildProjectContext(
ctx, ctx,
cmdName, cmdName,
subCmdName,
mergedCfg, mergedCfg,
commentFlags, commentFlags,
repoDir, repoDir,
@@ -391,6 +408,7 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
p.ProjectCommandContextBuilder.BuildProjectContext( p.ProjectCommandContextBuilder.BuildProjectContext(
ctx, ctx,
cmdName, cmdName,
subCmdName,
pCfg, pCfg,
commentFlags, commentFlags,
repoDir, repoDir,
@@ -498,6 +516,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *command.Cont
return p.buildProjectCommandCtx( return p.buildProjectCommandCtx(
ctx, ctx,
command.Plan, command.Plan,
"",
cmd.ProjectName, cmd.ProjectName,
cmd.Flags, cmd.Flags,
defaultRepoDir, defaultRepoDir,
@@ -590,7 +609,7 @@ func (p *DefaultProjectCommandBuilder) buildAllProjectCommandsByPlan(ctx *comman
var cmds []command.ProjectContext var cmds []command.ProjectContext
for _, plan := range plans { for _, plan := range plans {
commentCmds, err := p.buildProjectCommandCtx(ctx, commentCmd.CommandName(), plan.ProjectName, commentCmd.Flags, defaultRepoDir, plan.RepoRelDir, plan.Workspace, commentCmd.Verbose) commentCmds, err := p.buildProjectCommandCtx(ctx, commentCmd.CommandName(), commentCmd.SubName, plan.ProjectName, commentCmd.Flags, defaultRepoDir, plan.RepoRelDir, plan.Workspace, commentCmd.Verbose)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "building command for dir %q", plan.RepoRelDir) return nil, errors.Wrapf(err, "building command for dir %q", plan.RepoRelDir)
} }
@@ -604,9 +623,9 @@ func (p *DefaultProjectCommandBuilder) buildAllProjectCommandsByPlan(ctx *comman
return cmds, nil return cmds, nil
} }
// buildProjectApplyCommand builds an apply command for the single project // buildProjectCommand builds an command for the single project
// identified by cmd. // identified by cmd except plan.
func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { func (p *DefaultProjectCommandBuilder) buildProjectCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
workspace := DefaultWorkspace workspace := DefaultWorkspace
if cmd.Workspace != "" { if cmd.Workspace != "" {
workspace = cmd.Workspace workspace = cmd.Workspace
@@ -635,89 +654,8 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *command.Con
return p.buildProjectCommandCtx( return p.buildProjectCommandCtx(
ctx, ctx,
command.Apply, cmd.Name,
cmd.ProjectName, cmd.SubName,
cmd.Flags,
repoDir,
repoRelDir,
workspace,
cmd.Verbose,
)
}
// buildProjectVersionCommand builds a version command for the single project
// identified by cmd.
func (p *DefaultProjectCommandBuilder) buildProjectVersionCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
workspace := DefaultWorkspace
if cmd.Workspace != "" {
workspace = cmd.Workspace
}
var projCtx []command.ProjectContext
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir)
if err != nil {
return projCtx, err
}
defer unlockFn()
// use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml,
// other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically
repoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace)
if os.IsNotExist(errors.Cause(err)) {
return projCtx, errors.New("no working directory founddid you run plan?")
} else if err != nil {
return projCtx, err
}
repoRelDir := DefaultRepoRelDir
if cmd.RepoRelDir != "" {
repoRelDir = cmd.RepoRelDir
}
return p.buildProjectCommandCtx(
ctx,
command.Version,
cmd.ProjectName,
cmd.Flags,
repoDir,
repoRelDir,
workspace,
cmd.Verbose,
)
}
// buildProjectImportCommand builds a import command for the single project
// identified by cmd.
func (p *DefaultProjectCommandBuilder) buildProjectImportCommand(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) {
workspace := DefaultWorkspace
if cmd.Workspace != "" {
workspace = cmd.Workspace
}
var projCtx []command.ProjectContext
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, workspace, DefaultRepoRelDir)
if err != nil {
return projCtx, err
}
defer unlockFn()
// use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml,
// other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically
repoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace)
if os.IsNotExist(errors.Cause(err)) {
return projCtx, errors.New("no working directory founddid you run plan?")
} else if err != nil {
return projCtx, err
}
repoRelDir := DefaultRepoRelDir
if cmd.RepoRelDir != "" {
repoRelDir = cmd.RepoRelDir
}
return p.buildProjectCommandCtx(
ctx,
command.Import,
cmd.ProjectName, cmd.ProjectName,
cmd.Flags, cmd.Flags,
repoDir, repoDir,
@@ -731,6 +669,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectImportCommand(ctx *command.Co
// by the parameters. // by the parameters.
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Context, func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Context,
cmd command.Name, cmd command.Name,
subCmd string,
projectName string, projectName string,
commentFlags []string, commentFlags []string,
repoDir string, repoDir string,
@@ -767,6 +706,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
p.ProjectCommandContextBuilder.BuildProjectContext( p.ProjectCommandContextBuilder.BuildProjectContext(
ctx, ctx,
cmd, cmd,
subCmd,
projCfg, projCfg,
commentFlags, commentFlags,
repoDir, repoDir,
@@ -783,6 +723,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
p.ProjectCommandContextBuilder.BuildProjectContext( p.ProjectCommandContextBuilder.BuildProjectContext(
ctx, ctx,
cmd, cmd,
subCmd,
projCfg, projCfg,
commentFlags, commentFlags,
repoDir, repoDir,

View File

@@ -668,7 +668,7 @@ projects:
PullRequestStatus: models.PullReqStatus{ PullRequestStatus: models.PullReqStatus{
Mergeable: true, Mergeable: true,
}, },
}, cmd, "", []string{"flag"}, tmp, "project1", "myworkspace", true) }, cmd, "", "", []string{"flag"}, tmp, "project1", "myworkspace", true)
if c.expErr != "" { if c.expErr != "" {
ErrEquals(t, c.expErr, err) ErrEquals(t, c.expErr, err)
@@ -876,7 +876,7 @@ projects:
PullRequestStatus: models.PullReqStatus{ PullRequestStatus: models.PullReqStatus{
Mergeable: true, Mergeable: true,
}, },
}, cmd, "myproject_[1-2]", []string{"flag"}, tmp, "project1", "myworkspace", true) }, cmd, "", "myproject_[1-2]", []string{"flag"}, tmp, "project1", "myworkspace", true)
if c.expErr != "" { if c.expErr != "" {
ErrEquals(t, c.expErr, err) ErrEquals(t, c.expErr, err)
@@ -1113,7 +1113,7 @@ workflows:
PullRequestStatus: models.PullReqStatus{ PullRequestStatus: models.PullReqStatus{
Mergeable: true, Mergeable: true,
}, },
}, command.Plan, "", []string{"flag"}, tmp, "project1", "myworkspace", true) }, command.Plan, "", "", []string{"flag"}, tmp, "project1", "myworkspace", true)
if c.expErr != "" { if c.expErr != "" {
ErrEquals(t, c.expErr, err) ErrEquals(t, c.expErr, err)

View File

@@ -34,6 +34,7 @@ type ProjectCommandContextBuilder interface {
BuildProjectContext( BuildProjectContext(
ctx *command.Context, ctx *command.Context,
cmdName command.Name, cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg, prjCfg valid.MergedProjectCfg,
commentFlags []string, commentFlags []string,
repoDir string, repoDir string,
@@ -53,6 +54,7 @@ type CommandScopedStatsProjectCommandContextBuilder struct {
func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext( func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context, ctx *command.Context,
cmdName command.Name, cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg, prjCfg valid.MergedProjectCfg,
commentFlags []string, commentFlags []string,
repoDir string, repoDir string,
@@ -61,7 +63,7 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
cb.ProjectCounter.Inc(1) cb.ProjectCounter.Inc(1)
cmds := cb.ProjectCommandContextBuilder.BuildProjectContext( cmds := cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx, cmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, terraformClient, ctx, cmdName, subCmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, terraformClient,
) )
projectCmds = []command.ProjectContext{} projectCmds = []command.ProjectContext{}
@@ -85,6 +87,7 @@ type DefaultProjectCommandContextBuilder struct {
func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context, ctx *command.Context,
cmdName command.Name, cmdName command.Name,
subName string,
prjCfg valid.MergedProjectCfg, prjCfg valid.MergedProjectCfg,
commentFlags []string, commentFlags []string,
repoDir string, repoDir string,
@@ -105,6 +108,15 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
}} }}
case command.Import: case command.Import:
steps = prjCfg.Workflow.Import.Steps steps = prjCfg.Workflow.Import.Steps
case command.State:
switch subName {
case "rm":
steps = prjCfg.Workflow.StateRm.Steps
default:
// comment_parser prevent invalid subcommand, so not need to handle this.
// if comes here, state_command_runner will respond on PR, so it's enough to do log only.
ctx.Log.Err("unknown state subcommand: %s", subName)
}
} }
// If TerraformVersion not defined in config file look for a // If TerraformVersion not defined in config file look for a
@@ -143,6 +155,7 @@ type PolicyCheckProjectCommandContextBuilder struct {
func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context, ctx *command.Context,
cmdName command.Name, cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg, prjCfg valid.MergedProjectCfg,
commentFlags []string, commentFlags []string,
repoDir string, repoDir string,
@@ -159,6 +172,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
projectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext( projectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx, ctx,
cmdName, cmdName,
subCmdName,
prjCfg, prjCfg,
commentFlags, commentFlags,
repoDir, repoDir,

View File

@@ -62,7 +62,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
}, },
} }
result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false, terraformClient) result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, terraformClient)
assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
}) })
@@ -81,7 +81,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
}, },
} }
result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false, terraformClient) result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, terraformClient)
assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
}) })
@@ -101,7 +101,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
}, },
} }
result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, true, false, false, terraformClient) result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, true, false, false, terraformClient)
assert.True(t, result[0].ParallelApplyEnabled) assert.True(t, result[0].ParallelApplyEnabled)
assert.False(t, result[0].ParallelPlanEnabled) assert.False(t, result[0].ParallelPlanEnabled)

View File

@@ -118,6 +118,11 @@ type ProjectImportCommandRunner interface {
Import(ctx command.ProjectContext) command.ProjectResult Import(ctx command.ProjectContext) command.ProjectResult
} }
type ProjectStateCommandRunner interface {
// StateRm runs terraform state rm for the project described by ctx.
StateRm(ctx command.ProjectContext) command.ProjectResult
}
// ProjectCommandRunner runs project commands. A project command is a command // ProjectCommandRunner runs project commands. A project command is a command
// for a specific TF project. // for a specific TF project.
type ProjectCommandRunner interface { type ProjectCommandRunner interface {
@@ -127,6 +132,7 @@ type ProjectCommandRunner interface {
ProjectApprovePoliciesCommandRunner ProjectApprovePoliciesCommandRunner
ProjectVersionCommandRunner ProjectVersionCommandRunner
ProjectImportCommandRunner ProjectImportCommandRunner
ProjectStateCommandRunner
} }
//go:generate pegomock generate -m --package mocks -o mocks/mock_job_url_setter.go JobURLSetter //go:generate pegomock generate -m --package mocks -o mocks/mock_job_url_setter.go JobURLSetter
@@ -200,6 +206,7 @@ type DefaultProjectCommandRunner struct {
PolicyCheckStepRunner StepRunner PolicyCheckStepRunner StepRunner
VersionStepRunner StepRunner VersionStepRunner StepRunner
ImportStepRunner StepRunner ImportStepRunner StepRunner
StateRmStepRunner StepRunner
RunStepRunner CustomStepRunner RunStepRunner CustomStepRunner
EnvStepRunner EnvStepRunner EnvStepRunner EnvStepRunner
MultiEnvStepRunner MultiEnvStepRunner MultiEnvStepRunner MultiEnvStepRunner
@@ -292,6 +299,21 @@ func (p *DefaultProjectCommandRunner) Import(ctx command.ProjectContext) command
} }
} }
// StateRm runs terraform state rm for the project described by ctx.
func (p *DefaultProjectCommandRunner) StateRm(ctx command.ProjectContext) command.ProjectResult {
stateRmSuccess, failure, err := p.doStateRm(ctx)
return command.ProjectResult{
Command: command.State,
SubCommand: "rm",
StateRmSuccess: stateRmSuccess,
Error: err,
Failure: failure,
RepoRelDir: ctx.RepoRelDir,
Workspace: ctx.Workspace,
ProjectName: ctx.ProjectName,
}
}
func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectContext) (*models.PolicyCheckSuccess, string, error) { func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectContext) (*models.PolicyCheckSuccess, string, error) {
// TODO: Make this a bit smarter // TODO: Make this a bit smarter
@@ -539,6 +561,47 @@ func (p *DefaultProjectCommandRunner) doImport(ctx command.ProjectContext) (out
}, "", nil }, "", nil
} }
func (p *DefaultProjectCommandRunner) doStateRm(ctx command.ProjectContext) (out *models.StateRmSuccess, failure string, err error) {
// Clone is idempotent so okay to run even if the repo was already cloned.
repoDir, _, cloneErr := p.WorkingDir.Clone(ctx.Log, ctx.HeadRepo, ctx.Pull, ctx.Workspace)
if cloneErr != nil {
return nil, "", cloneErr
}
projAbsPath := filepath.Join(repoDir, ctx.RepoRelDir)
if _, err = os.Stat(projAbsPath); os.IsNotExist(err) {
return nil, "", DirNotExistErr{RepoRelDir: ctx.RepoRelDir}
}
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir), ctx.RepoLocking)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
}
if !lockAttempt.LockAcquired {
return nil, lockAttempt.LockFailureReason, nil
}
ctx.Log.Debug("acquired lock for project")
// Acquire internal lock for the directory we're going to operate in.
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir)
if err != nil {
return nil, "", err
}
defer unlockFn()
outputs, err := p.runSteps(ctx.Steps, ctx, projAbsPath)
if err != nil {
return nil, "", fmt.Errorf("%s\n%s", err, strings.Join(outputs, "\n"))
}
// after state rm, re-plan command is required without state rm args
rePlanCmd := strings.TrimSpace(strings.Split(ctx.RePlanCmd, "--")[0])
return &models.StateRmSuccess{
Output: strings.Join(outputs, "\n"),
RePlanCmd: rePlanCmd,
}, "", nil
}
func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.ProjectContext, absPath string) ([]string, error) { func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.ProjectContext, absPath string) ([]string, error) {
var outputs []string var outputs []string
@@ -561,6 +624,8 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.P
out, err = p.VersionStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) out, err = p.VersionStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)
case "import": case "import":
out, err = p.ImportStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) out, err = p.ImportStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)
case "state_rm":
out, err = p.StateRmStepRunner.Run(ctx, step.ExtraArgs, absPath, envs)
case "run": case "run":
out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs, true) out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs, true)
case "env": case "env":

View File

@@ -684,6 +684,7 @@ func TestDefaultProjectCommandRunner_Import(t *testing.T) {
RegisterMockTestingT(t) RegisterMockTestingT(t)
mockInit := mocks.NewMockStepRunner() mockInit := mocks.NewMockStepRunner()
mockImport := mocks.NewMockStepRunner() mockImport := mocks.NewMockStepRunner()
mockStateRm := mocks.NewMockStepRunner()
mockWorkingDir := mocks.NewMockWorkingDir() mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker() mockLocker := mocks.NewMockProjectLocker()
mockSender := mocks.NewMockWebhooksSender() mockSender := mocks.NewMockWebhooksSender()
@@ -696,6 +697,7 @@ func TestDefaultProjectCommandRunner_Import(t *testing.T) {
LockURLGenerator: mockURLGenerator{}, LockURLGenerator: mockURLGenerator{},
InitStepRunner: mockInit, InitStepRunner: mockInit,
ImportStepRunner: mockImport, ImportStepRunner: mockImport,
StateRmStepRunner: mockStateRm,
WorkingDir: mockWorkingDir, WorkingDir: mockWorkingDir,
Webhooks: mockSender, Webhooks: mockSender,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(), WorkingDirLocker: events.NewDefaultWorkingDirLocker(),

View File

@@ -28,7 +28,7 @@ func (c *PullUpdater) updatePull(ctx *command.Context, cmd PullCommand, res comm
} }
} }
comment := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Log.GetHistory(), cmd.IsVerbose(), ctx.Pull.BaseRepo.VCSHost.Type) comment := c.MarkdownRenderer.Render(res, cmd.CommandName(), cmd.SubCommandName(), ctx.Log.GetHistory(), cmd.IsVerbose(), ctx.Pull.BaseRepo.VCSHost.Type)
if err := c.VCSClient.CreateComment(ctx.Pull.BaseRepo, ctx.Pull.Num, comment, cmd.CommandName().String()); err != nil { if err := c.VCSClient.CreateComment(ctx.Pull.BaseRepo, ctx.Pull.Num, comment, cmd.CommandName().String()); err != nil {
ctx.Log.Err("unable to comment: %s", err) ctx.Log.Err("unable to comment: %s", err)
} }

View File

@@ -0,0 +1,46 @@
package events
import (
"fmt"
"github.com/runatlantis/atlantis/server/events/command"
)
func NewStateCommandRunner(
pullUpdater *PullUpdater,
prjCmdBuilder ProjectStateCommandBuilder,
prjCmdRunner ProjectStateCommandRunner,
) *StateCommandRunner {
return &StateCommandRunner{
pullUpdater: pullUpdater,
prjCmdBuilder: prjCmdBuilder,
prjCmdRunner: prjCmdRunner,
}
}
type StateCommandRunner struct {
pullUpdater *PullUpdater
prjCmdBuilder ProjectStateCommandBuilder
prjCmdRunner ProjectStateCommandRunner
}
func (v *StateCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
var result command.Result
switch cmd.SubName {
case "rm":
result = v.runRm(ctx, cmd)
default:
result = command.Result{
Failure: fmt.Sprintf("unknown state subcommand %s", cmd.SubName),
}
}
v.pullUpdater.updatePull(ctx, cmd, result)
}
func (v *StateCommandRunner) runRm(ctx *command.Context, cmd *CommentCommand) command.Result {
projectCmds, err := v.prjCmdBuilder.BuildStateRmCommands(ctx, cmd)
if err != nil {
ctx.Log.Warn("Error %s", err)
}
return runProjectCmds(projectCmds, v.prjCmdRunner.StateRm)
}

View File

@@ -3,6 +3,8 @@
{{ .Output }} {{ .Output }}
``` ```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `{{ .RePlanCmd }}` * `{{.RePlanCmd}}`
{{ end -}} {{ end -}}

View File

@@ -4,6 +4,8 @@
{{ .Output }} {{ .Output }}
``` ```
</details> </details>
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment: * :repeat: To **plan** this project again, comment:
* `{{ .RePlanCmd }}` * `{{ .RePlanCmd }}`
{{ end -}} {{ end -}}

View File

@@ -0,0 +1,10 @@
{{ define "multiProjectStateRm" -}}
{{ template "multiProjectHeader" . }}
{{ range $i, $result := .Results -}}
### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`
{{ $result.Rendered}}
---
{{ end -}}
{{- template "log" . -}}
{{ end -}}

View File

@@ -0,0 +1,6 @@
{{ define "singleProjectStateRm" -}}
{{$result := index .Results 0}}Ran {{.Command}} `{{.SubCommand}}` for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`
{{$result.Rendered}}
{{ template "log" . }}
{{ end }}

View File

@@ -0,0 +1,10 @@
{{ define "stateRmSuccessUnwrapped" -}}
```diff
{{ .Output }}
```
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `{{.RePlanCmd}}`
{{ end }}

View File

@@ -0,0 +1,11 @@
{{ define "stateRmSuccessWrapped" -}}
<details><summary>Show Output</summary>
```diff
{{ .Output }}
```
</details>
:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
* :repeat: To **plan** this project again, comment:
* `{{.RePlanCmd}}`
{{ end }}

View File

@@ -619,6 +619,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
DefaultTFVersion: defaultTfVersion, DefaultTFVersion: defaultTfVersion,
}, },
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion), ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfVersion),
WorkingDir: workingDir, WorkingDir: workingDir,
Webhooks: webhooksManager, Webhooks: webhooksManager,
WorkingDirLocker: workingDirLocker, WorkingDirLocker: workingDirLocker,
@@ -730,6 +731,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
instrumentedProjectCmdRunner, instrumentedProjectCmdRunner,
) )
stateCommandRunner := events.NewStateCommandRunner(
pullUpdater,
projectCommandBuilder,
instrumentedProjectCmdRunner,
)
commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{ commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{
command.Plan: planCommandRunner, command.Plan: planCommandRunner,
command.Apply: applyCommandRunner, command.Apply: applyCommandRunner,
@@ -737,6 +744,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
command.Unlock: unlockCommandRunner, command.Unlock: unlockCommandRunner,
command.Version: versionCommandRunner, command.Version: versionCommandRunner,
command.Import: importCommandRunner, command.Import: importCommandRunner,
command.State: stateCommandRunner,
} }
githubTeamAllowlistChecker, err := events.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist) githubTeamAllowlistChecker, err := events.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist)

View File

@@ -19,23 +19,23 @@ func TestUserConfig_ToAllowCommandNames(t *testing.T) {
}{ }{
{ {
name: "full commands can be parsed by comma", name: "full commands can be parsed by comma",
allowCommands: "apply,plan,unlock,policy_check,approve_policies,version,import", allowCommands: "apply,plan,unlock,policy_check,approve_policies,version,import,state",
want: []command.Name{ want: []command.Name{
command.Apply, command.Plan, command.Unlock, command.PolicyCheck, command.ApprovePolicies, command.Version, command.Import, command.Apply, command.Plan, command.Unlock, command.PolicyCheck, command.ApprovePolicies, command.Version, command.Import, command.State,
}, },
}, },
{ {
name: "all", name: "all",
allowCommands: "all", allowCommands: "all",
want: []command.Name{ want: []command.Name{
command.Version, command.Plan, command.Apply, command.Unlock, command.ApprovePolicies, command.Import, command.Version, command.Plan, command.Apply, command.Unlock, command.ApprovePolicies, command.Import, command.State,
}, },
}, },
{ {
name: "all with others returns same with all result", name: "all with others returns same with all result",
allowCommands: "all,plan", allowCommands: "all,plan",
want: []command.Name{ want: []command.Name{
command.Version, command.Plan, command.Apply, command.Unlock, command.ApprovePolicies, command.Import, command.Version, command.Plan, command.Apply, command.Unlock, command.ApprovePolicies, command.Import, command.State,
}, },
}, },
{ {