From 7c3fe9d57bd1693a95f658cf5836af609a4e1a21 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 13:31:50 +0900 Subject: [PATCH 01/35] feat: atlantis import --- cmd/server.go | 19 +- runatlantis.io/.vuepress/config.js | 2 +- ...equirements.md => command-requirements.md} | 32 +- .../docs/repo-level-atlantis-yaml.md | 26 +- runatlantis.io/docs/server-configuration.md | 8 +- .../docs/server-side-repo-config.md | 36 ++- runatlantis.io/docs/using-atlantis.md | 30 ++ server/controllers/api_controller_test.go | 4 +- .../events/events_controller_e2e_test.go | 51 +++- .../import-multiple-project/atlantis.yaml | 4 + .../import-multiple-project/dir1/main.tf | 4 + .../import-multiple-project/dir2/main.tf | 4 + .../exp-output-autoplan.txt | 81 ++++++ .../exp-output-import-dummy1.txt | 20 ++ .../exp-output-import-multiple-projects.txt | 1 + .../exp-output-merge.txt | 4 + .../exp-output-plan-again.txt | 63 ++++ .../exp-output-apply-no-projects.txt | 4 + .../exp-output-autoplan.txt | 52 ++++ .../exp-output-import-dummy1.txt | 20 ++ .../exp-output-import-dummy2.txt | 20 ++ .../exp-output-merge.txt | 3 + .../exp-output-plan-again.txt | 22 ++ .../test-repos/import-single-project/main.tf | 9 + server/core/config/parser_validator_test.go | 205 ++++++++----- server/core/config/raw/global_cfg.go | 30 +- server/core/config/raw/project.go | 27 +- server/core/config/raw/project_test.go | 36 ++- server/core/config/raw/repo_cfg_test.go | 39 +-- server/core/config/raw/step.go | 4 +- server/core/config/raw/step_test.go | 23 ++ server/core/config/raw/workflow.go | 3 + server/core/config/raw/workflow_test.go | 15 + server/core/config/valid/global_cfg.go | 71 +++-- server/core/config/valid/global_cfg_test.go | 219 +++++++++----- server/core/config/valid/repo_cfg.go | 2 + server/core/runtime/import_step_runner.go | 38 +++ .../core/runtime/import_step_runner_test.go | 61 ++++ .../mocks/matchers/command_projectcontext.go | 33 +++ server/events/apply_requirement_handler.go | 43 --- server/events/command/name.go | 16 +- server/events/command/name_test.go | 74 +++-- server/events/command/project_context.go | 3 + server/events/command/project_result.go | 1 + server/events/command_requirement_handler.go | 65 +++++ .../command_requirement_handler_test.go | 194 +++++++++++++ server/events/command_runner_test.go | 8 + server/events/comment_parser.go | 22 +- server/events/comment_parser_test.go | 69 ++++- server/events/import_command_runner.go | 44 +++ server/events/markdown_renderer.go | 11 + server/events/markdown_renderer_test.go | 27 ++ server/events/mock_workingdir_test.go | 49 +++- .../mocks/matchers/command_projectresult.go | 33 +++ .../slice_of_command_projectcontext.go | 33 +++ server/events/mocks/mock_apply_handler.go | 114 -------- .../mocks/mock_command_requirement_handler.go | 163 +++++++++++ .../mocks/mock_project_command_builder.go | 50 ++++ .../mocks/mock_project_command_runner.go | 42 +++ server/events/mocks/mock_working_dir.go | 77 ++--- server/events/models/models.go | 8 + server/events/project_command_builder.go | 79 ++++- .../project_command_builder_internal_test.go | 274 +++++++++--------- .../events/project_command_context_builder.go | 6 +- server/events/project_command_runner.go | 103 +++++-- server/events/project_command_runner_test.go | 191 +++++++++--- .../templates/import_success_unwrapped.tmpl | 8 + .../templates/import_success_wrapped.tmpl | 9 + .../templates/multi_project_import.tmpl | 3 + .../single_project_import_success.tmpl | 6 + server/scheduled/executor_service.go | 5 +- server/server.go | 31 +- 72 files changed, 2455 insertions(+), 731 deletions(-) rename runatlantis.io/docs/{apply-requirements.md => command-requirements.md} (90%) create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/atlantis.yaml create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/dir1/main.tf create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/dir2/main.tf create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-autoplan.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-dummy1.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-multiple-projects.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-merge.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-plan-again.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-apply-no-projects.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-autoplan.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy1.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy2.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-merge.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-plan-again.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project/main.tf create mode 100644 server/core/runtime/import_step_runner.go create mode 100644 server/core/runtime/import_step_runner_test.go create mode 100644 server/core/terraform/mocks/matchers/command_projectcontext.go delete mode 100644 server/events/apply_requirement_handler.go create mode 100644 server/events/command_requirement_handler.go create mode 100644 server/events/command_requirement_handler_test.go create mode 100644 server/events/import_command_runner.go create mode 100644 server/events/mocks/matchers/command_projectresult.go create mode 100644 server/events/mocks/matchers/slice_of_command_projectcontext.go delete mode 100644 server/events/mocks/mock_apply_handler.go create mode 100644 server/events/mocks/mock_command_requirement_handler.go create mode 100644 server/events/templates/import_success_unwrapped.tmpl create mode 100644 server/events/templates/import_success_wrapped.tmpl create mode 100644 server/events/templates/multi_project_import.tmpl create mode 100644 server/events/templates/single_project_import_success.tmpl diff --git a/cmd/server.go b/cmd/server.go index 506b537608..da11131731 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -999,30 +999,31 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) { // being used. Right now this only applies to flags that have been made obsolete // due to server-side config. func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error { - var applyReqs []string + var commandReqs []string var deprecatedFlags []string if userConfig.RequireApproval { deprecatedFlags = append(deprecatedFlags, RequireApprovalFlag) - applyReqs = append(applyReqs, valid.ApprovedApplyReq) + commandReqs = append(commandReqs, valid.ApprovedCommandReq) } if userConfig.RequireMergeable { deprecatedFlags = append(deprecatedFlags, RequireMergeableFlag) - applyReqs = append(applyReqs, valid.MergeableApplyReq) + commandReqs = append(commandReqs, valid.MergeableCommandReq) } // Build up strings with what the recommended yaml and json config should // be instead of using the deprecated flags. yamlCfg := "---\nrepos:\n- id: /.*/" jsonCfg := `{"repos":[{"id":"/.*/"` - if len(applyReqs) > 0 { - yamlCfg += fmt.Sprintf("\n apply_requirements: [%s]", strings.Join(applyReqs, ", ")) - jsonCfg += fmt.Sprintf(`, "apply_requirements":["%s"]`, strings.Join(applyReqs, "\", \"")) - + if len(commandReqs) > 0 { + yamlCfg += fmt.Sprintf("\n apply_requirements: [%s]", strings.Join(commandReqs, ", ")) + yamlCfg += fmt.Sprintf("\n import_requirements: [%s]", strings.Join(commandReqs, ", ")) + jsonCfg += fmt.Sprintf(`, "apply_requirements":["%s"]`, strings.Join(commandReqs, "\", \"")) + jsonCfg += fmt.Sprintf(`, "import_requirements":["%s"]`, strings.Join(commandReqs, "\", \"")) } if userConfig.AllowRepoConfig { deprecatedFlags = append(deprecatedFlags, AllowRepoConfigFlag) - yamlCfg += "\n allowed_overrides: [apply_requirements, workflow]\n allow_custom_workflows: true" - jsonCfg += `, "allowed_overrides":["apply_requirements","workflow"], "allow_custom_workflows":true` + yamlCfg += "\n allowed_overrides: [apply_requirements, import_requirements, workflow]\n allow_custom_workflows: true" + jsonCfg += `, "allowed_overrides":["apply_requirements","import_requirements","workflow"], "allow_custom_workflows":true` } jsonCfg += "}]}" diff --git a/runatlantis.io/.vuepress/config.js b/runatlantis.io/.vuepress/config.js index 6f5943be3c..e5410d5e20 100644 --- a/runatlantis.io/.vuepress/config.js +++ b/runatlantis.io/.vuepress/config.js @@ -74,7 +74,7 @@ module.exports = { 'custom-workflows', 'repo-level-atlantis-yaml', 'upgrading-atlantis-yaml', - 'apply-requirements', + 'command-requirements', 'checkout-strategy', 'terraform-versions', 'terraform-cloud', diff --git a/runatlantis.io/docs/apply-requirements.md b/runatlantis.io/docs/command-requirements.md similarity index 90% rename from runatlantis.io/docs/apply-requirements.md rename to runatlantis.io/docs/command-requirements.md index 4fa7edf3d7..1a3e6309a4 100644 --- a/runatlantis.io/docs/apply-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -1,9 +1,9 @@ -# Apply Requirements +# Command Requirements [[toc]] ## Intro -Atlantis allows you to require certain conditions be satisfied **before** an `atlantis apply` -command can be run: +Atlantis allows you to require certain conditions be satisfied **before** `atlantis apply` and `atlantis import` +commands can be run: * [Approved](#approved) – requires pull requests to be approved by at least one user other than the author * [Mergeable](#mergeable) – requires pull requests to be able to be merged @@ -70,12 +70,12 @@ You can set the `mergeable` requirement by: apply_requirements: [mergeable] ``` -1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config: +1. Or by allowing an `atlantis.yaml` file to specify `apply_requirements` and `import_requirements` keys in your `repos.yaml` config: #### repos.yaml ```yaml repos: - id: /.*/ - allowed_overrides: [apply_requirements] + allowed_overrides: [apply_requirements, import_requirements] ``` #### atlantis.yaml @@ -84,6 +84,7 @@ You can set the `mergeable` requirement by: projects: - dir: . apply_requirements: [mergeable] + import_requirements: [mergeable] ``` #### Meaning @@ -152,18 +153,19 @@ Applies to `merge` checkout strategy only. #### Usage You can set the `undiverged` requirement by: -1. Creating a `repos.yaml` file with the `apply_requirements` key: +1. Creating a `repos.yaml` file with `apply_requirements` and `import_requirements` keys: ```yaml repos: - id: /.*/ apply_requirements: [undiverged] + import_requirements: [undiverged] ``` 1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config: #### repos.yaml ```yaml repos: - id: /.*/ - allowed_overrides: [apply_requirements] + allowed_overrides: [apply_requirements, apply_requirements] ``` #### atlantis.yaml @@ -172,6 +174,7 @@ You can set the `undiverged` requirement by: projects: - dir: . apply_requirements: [undiverged] + import_requirements: [undiverged] ``` #### Meaning The `merge` checkout strategy creates a temporary merge commit and runs the `plan` on the Atlantis local version of the PR @@ -180,8 +183,8 @@ if there are no changes to the source branch. `undiverged` enforces that Atlanti with remote so that the state of the source during the `apply` is identical to that if you were to merge the PR at that time. -## Setting Apply Requirements -As mentioned above, you can set apply requirements via flags, in `repos.yaml`, or in `atlantis.yaml` if `repos.yaml` +## Setting Command Requirements +As mentioned above, you can set command requirements via flags, in `repos.yaml`, or in `atlantis.yaml` if `repos.yaml` allows the override. ### Flags Override @@ -197,19 +200,22 @@ If you only want some projects/repos to have apply requirements, then you must repos: - id: /.*/ apply_requirements: [approved] + import_requirements: [approved] # Regex that defaults all repos to requiring approval - id: /github.com/runatlantis/.*/ # Regex to match any repo under the atlantis namespace, and not require approval # except for repos that might match later in the chain apply_requirements: [] + import_requirements: [] - id: github.com/runatlantis/atlantis apply_requirements: [approved] + import_requirements: [approved] # Exact string match of the github.com/runatlantis/atlantis repo # that sets apply_requirements to approved ``` 1. Specify which projects have which requirements via an `atlantis.yaml` file, and allowing - `apply_requirements` to be set in in `atlantis.yaml` by the server side `repos.yaml` + `apply_requirements` and `import_requirements` to be set in `atlantis.yaml` by the server side `repos.yaml` config. For example if I have two directories, `staging` and `production`, I might use: @@ -217,7 +223,7 @@ If you only want some projects/repos to have apply requirements, then you must ```yaml repos: - id: /.*/ - allowed_overrides: [apply_requirements] + allowed_overrides: [apply_requirements, import_requirements] # Allow any repo to specify apply_requirements in atlantis.yaml ``` @@ -226,13 +232,15 @@ If you only want some projects/repos to have apply requirements, then you must version: 3 projects: - dir: staging - # By default, apply_requirements is empty so this + # By default, apply_requirements and import_requirements are empty so this # isn't strictly necessary. apply_requirements: [] + import_requirements: [] - dir: production # This requirement will only apply to the # production directory. apply_requirements: [mergeable] + import_requirements: [mergeable] ### Multiple Requirements diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index c596855bad..03feaedbd1 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -60,7 +60,8 @@ projects: autoplan: when_modified: ["*.tf", "../modules/**/*.tf"] enabled: true - apply_requirements: [mergeable, approved] + apply_requirements: [mergeable, approved, undiverged] + import_requirements: [mergeable, approved, undiverged] workflow: myworkflow workflows: myworkflow: @@ -215,10 +216,11 @@ projects: - dir: staging - dir: production apply_requirements: [approved] + import_requirements: [approved] ``` :::warning -`apply_requirements` is a restricted key so this repo will need to be configured -to be allowed to set this key. See [Server-Side Repo Config Use Cases](server-side-repo-config.html#repos-can-set-their-own-apply-requirements). +`apply_requirements` and `import_requirements` are restricted keys so this repo will need to be configured +to be allowed to set this key. See [Server-Side Repo Config Use Cases](server-side-repo-config.html#repos-can-set-their-own-apply-or-import-requirements). ::: ### Order of planning/applying @@ -269,9 +271,11 @@ repo_locking: true autoplan: terraform_version: 0.11.0 apply_requirements: ["approved"] +import_requirements: ["approved"] workflow: myworkflow ``` +<<<<<<< HEAD | Key | Type | Default | Required | Description | |----------------------------------------|-----------------------|-------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. | @@ -285,6 +289,22 @@ workflow: myworkflow | terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | | apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. | | workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | +======= +| Key | Type | Default | Required | Description | +|-----------------------------------------|-----------------------|-------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. | +| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. | +| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. | +| workspace | string | `"default"` | no | The [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | +| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. | +| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. | +| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | +| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | +| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | +| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | +| import_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | +| workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | +>>>>>>> ec0bdf6c (feat: atlantis import) ::: tip A project represents a Terraform state. Typically, there is one state per directory and workspace however it's possible to diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 2af2e8b188..49a6c5cb6d 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -505,9 +505,9 @@ and set `--autoplan-modules` to `false`. ### `--gh-team-allowlist` ```bash - atlantis server --gh-team-allowlist="myteam:plan, secteam:apply, DevOps Team:apply" + atlantis server --gh-team-allowlist="myteam:plan, secteam:apply, DevOps Team:apply, DevOps Team:import" # or - ATLANTIS_GH_TEAM_ALLOWLIST="myteam:plan, secteam:apply, DevOps Team:apply" + ATLANTIS_GH_TEAM_ALLOWLIST="myteam:plan, secteam:apply, DevOps Team:apply, DevOps Team:import" ``` In versions v0.21.0 and later, the GitHub team name can be a name or a slug. @@ -789,7 +789,7 @@ and set `--autoplan-modules` to `false`. ATLANTIS_REQUIRE_APPROVAL=true ``` This flag is deprecated. It requires all pull requests to be approved - before `atlantis apply` is allowed. See [Apply Requirements](apply-requirements.html) for more details. + before `atlantis apply` is allowed. See [Command Requirements](command-requirements.html) for more details. Instead of using this flag, create a server-side `--repo-config` file: ```yaml @@ -808,7 +808,7 @@ and set `--autoplan-modules` to `false`. ATLANTIS_REQUIRE_MERGEABLE=true ``` This flag is deprecated. It causes all pull requests to be mergeable - before `atlantis apply` is allowed. See [Apply Requirements](apply-requirements.html) for more details. + before `atlantis apply` is allowed. See [Command Requirements](command-requirements.html) for more details. Instead of using this flag, create a server-side `--repo-config` file: ```yaml diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 025df2c9ea..c6d17d561a 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -40,7 +40,10 @@ repos: repo_config_file: path/to/atlantis.yaml # apply_requirements sets the Apply Requirements for all repos that match. - apply_requirements: [approved, mergeable] + apply_requirements: [approved, mergeable, undiverged] + + # import_requirements sets the Import Requirements for all repos that match. + import_requirements: [approved, mergeable, undiverged] # workflow sets the workflow for all repos that match. # This workflow must be defined in the workflows section. @@ -97,9 +100,9 @@ workflows: ## Use Cases Here are some of the reasons you might want to use a repo config. -### Requiring PR Is Approved Before Apply +### Requiring PR Is Approved Before Apply or Import If you want to require that all (or specific) repos must have pull requests -approved before Atlantis will allow running `apply`, use the `apply_requirements` key. +approved before Atlantis will allow running `apply` or `import`, use the `apply_requirements` or `import_requirements` keys. For all repos: ```yaml @@ -107,6 +110,7 @@ For all repos: repos: - id: /.*/ apply_requirements: [approved] + import_requirements: [approved] ``` For a specific repo: @@ -115,13 +119,14 @@ For a specific repo: repos: - id: github.com/myorg/myrepo apply_requirements: [approved] + import_requirements: [approved] ``` -See [Apply Requirements](apply-requirements.html) for more details. +See [Command Requirements](command-requirements.html) for more details. -### Requiring PR Is "Mergeable" Before Apply +### Requiring PR Is "Mergeable" Before Apply or Import If you want to require that all (or specific) repos must have pull requests -in a mergeable state before Atlantis will allow running `apply`, use the `apply_requirements` key. +in a mergeable state before Atlantis will allow running `apply` or `import`, use the `apply_requirements` or `import_requirements` keys. For all repos: ```yaml @@ -129,6 +134,7 @@ For all repos: repos: - id: /.*/ apply_requirements: [mergeable] + import_requirements: [mergeable] ``` For a specific repo: @@ -137,11 +143,12 @@ For a specific repo: repos: - id: github.com/myorg/myrepo apply_requirements: [mergeable] + import_requirements: [mergeable] ``` -See [Apply Requirements](apply-requirements.html) for more details. +See [Command Requirements](command-requirements.html) for more details. -### Repos Can Set Their Own Apply Requirements +### Repos Can Set Their Own Apply or Import Requirements If you want all (or specific) repos to be able to override the default apply requirements, use the `allowed_overrides` key. @@ -152,9 +159,10 @@ repos: - id: /.*/ # The default will be approved. apply_requirements: [approved] + import_requirements: [approved] # But all repos can set their own using atlantis.yaml - allowed_overrides: [apply_requirements] + allowed_overrides: [apply_requirements, import_requirements] ``` To allow only a specific repo to override the default: ```yaml @@ -163,20 +171,22 @@ repos: # Set a default for all repos. - id: /.*/ apply_requirements: [approved] + import_requirements: [approved] # Allow a specific repo to override. - id: github.com/myorg/myrepo - allowed_overrides: [apply_requirements] + allowed_overrides: [apply_requirements, import_requirements] ``` Then each allowed repo can have an `atlantis.yaml` file that -sets `apply_requirements` to an empty array (disabling the requirement). +sets `apply_requirements` or `import_requirements` to an empty array (disabling the requirement). ```yaml # atlantis.yaml in the repo root or set repo_config_file in repos.yaml version: 3 projects: - dir: . apply_requirements: [] + import_requirements: [] ``` ### Running Scripts Before Atlantis Workflows @@ -427,6 +437,7 @@ repos: - id: /.*/ branch: /.*/ apply_requirements: [] + import_requirements: [] workflow: default allowed_overrides: [] allow_custom_workflows: false @@ -454,7 +465,8 @@ If you set a workflow with the key `default`, it will override this. | branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched | | repo_config_file | string | none | no | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names. | | workflow | string | none | no | A custom workflow. | -| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. | +| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | +| import_requirements | []string | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | | allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge` and `repo_locking` | | allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. | | allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.html). | diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index e2a78ca3fa..ca93723b81 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -125,6 +125,7 @@ They're ignored because they can't be specified for an already generated planfil If you would like to specify these flags, do it while running `atlantis plan`. --- +<<<<<<< HEAD ## atlantis unlock ```bash atlantis unlock @@ -147,3 +148,32 @@ See also [policy checking](/docs/policy-checking.html). ### Options * `--verbose` Append Atlantis log to comment. +======= +## atlantis import + +```bash +atlantis import [options] -- [terraform import flags] addr id +``` +### Explanation +Runs `terraform import` that matches the directory/project/workspace. + +### Examples +```bash +# Runs import +atlantis import -- addr id + +# Runs import in the root directory of the repo with workspace `default`. +atlantis import -d . -- addr id + +# Runs import in the `project1` directory of the repo with workspace `default` +atlantis import -d project1 -- addr id + +# Runs import in the root directory of the repo with workspace `staging` +atlantis import -w staging -- addr id +``` + +### Options +* `-d directory` Import a resource for this directory, relative to root of repo. Use `.` for root. +* `-p project` Import a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.html). Cannot be used at same time as `-d` or `-w`. +* `-w workspace` Import a resource for this [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html). If not using Terraform workspaces you can ignore this. +>>>>>>> ec0bdf6c (feat: atlantis import) diff --git a/server/controllers/api_controller_test.go b/server/controllers/api_controller_test.go index 8cae678b63..1ba56d02f2 100644 --- a/server/controllers/api_controller_test.go +++ b/server/controllers/api_controller_test.go @@ -3,8 +3,6 @@ package controllers_test import ( "bytes" "encoding/json" - "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/models" "net/http" "net/http/httptest" "testing" @@ -13,8 +11,10 @@ import ( "github.com/runatlantis/atlantis/server/controllers" . "github.com/runatlantis/atlantis/server/core/locking/mocks" "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/command" . "github.com/runatlantis/atlantis/server/events/mocks" . "github.com/runatlantis/atlantis/server/events/mocks/matchers" + "github.com/runatlantis/atlantis/server/events/models" . "github.com/runatlantis/atlantis/server/events/vcs/mocks" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 45d68de0ce..7cedc553e0 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -404,6 +404,44 @@ func TestGitHubWorkflow(t *testing.T) { {"exp-output-merge.txt"}, }, }, + { + Description: "import single project", + RepoDir: "import-single-project", + ModifiedFiles: []string{"main.tf"}, + ExpAutoplan: true, + Comments: []string{ + "atlantis import -- random_id.dummy1 AA", + "atlantis apply", + "atlantis import -- random_id.dummy2 BB", + "atlantis plan", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-import-dummy1.txt"}, + {"exp-output-apply-no-projects.txt"}, + {"exp-output-import-dummy2.txt"}, + {"exp-output-plan-again.txt"}, + {"exp-output-merge.txt"}, + }, + }, + { + Description: "import multiple project", + RepoDir: "import-multiple-project", + ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"}, + ExpAutoplan: true, + Comments: []string{ + "atlantis import -- random_id.dummy1 AA", + "atlantis import -d dir1 -- random_id.dummy1 AA", + "atlantis plan", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-import-multiple-projects.txt"}, + {"exp-output-import-dummy1.txt"}, + {"exp-output-plan-again.txt"}, + {"exp-output-merge.txt"}, + }, + }, } for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -1047,6 +1085,10 @@ func setupE2E(t *testing.T, repoDir, repoConfigFile string) (events_controllers. ApplyStepRunner: &runtime.ApplyStepRunner{ TerraformExecutor: terraformClient, }, + ImportStepRunner: &runtime.ImportStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTFVersion, + }, RunStepRunner: &runtime.RunStepRunner{ TerraformExecutor: terraformClient, DefaultTFVersion: defaultTFVersion, @@ -1055,7 +1097,7 @@ func setupE2E(t *testing.T, repoDir, repoConfigFile string) (events_controllers. WorkingDir: workingDir, Webhooks: &mockWebhookSender{}, WorkingDirLocker: locker, - AggregateApplyRequirements: &events.AggregateApplyRequirements{ + CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: workingDir, }, } @@ -1148,12 +1190,19 @@ func setupE2E(t *testing.T, repoDir, repoConfigFile string) (events_controllers. silenceNoProjects, ) + importCommandRunner := events.NewImportCommandRunner( + pullUpdater, + projectCommandBuilder, + projectCommandRunner, + ) + commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{ command.Plan: planCommandRunner, command.Apply: applyCommandRunner, command.ApprovePolicies: approvePoliciesCommandRunner, command.Unlock: unlockCommandRunner, command.Version: versionCommandRunner, + command.Import: importCommandRunner, } commandRunner := &events.DefaultCommandRunner{ diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/atlantis.yaml b/server/controllers/events/testfixtures/test-repos/import-multiple-project/atlantis.yaml new file mode 100644 index 0000000000..006db31ba5 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/atlantis.yaml @@ -0,0 +1,4 @@ +version: 3 +projects: +- dir: dir1 +- dir: dir2 diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir1/main.tf b/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir1/main.tf new file mode 100644 index 0000000000..2aa6a6437d --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir1/main.tf @@ -0,0 +1,4 @@ +resource "random_id" "dummy1" { + keepers = {} + byte_length = 1 +} diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir2/main.tf b/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir2/main.tf new file mode 100644 index 0000000000..5292f29c85 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/dir2/main.tf @@ -0,0 +1,4 @@ +resource "random_id" "dummy2" { + keepers = {} + byte_length = 1 +} diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-autoplan.txt b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-autoplan.txt new file mode 100644 index 0000000000..7c14657512 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-autoplan.txt @@ -0,0 +1,81 @@ +Ran Plan for 2 projects: + +1. dir: `dir1` workspace: `default` +1. dir: `dir2` workspace: `default` + +### 1. dir: `dir1` workspace: `default` +
Show Output + +```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 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 -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` +
+Plan: 1 to add, 0 to change, 0 to destroy. + +--- +### 2. dir: `dir2` workspace: `default` +
Show Output + +```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.dummy2 will be created ++ resource "random_id" "dummy2" { + + 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` +
+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` diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-dummy1.txt b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-dummy1.txt new file mode 100644 index 0000000000..131d54c80e --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-dummy1.txt @@ -0,0 +1,20 @@ +Ran Import for dir: `dir1` workspace: `default` + +```diff +random_id.dummy1: Importing from ID "AA"... +random_id.dummy1: Import prepared! + Prepared random_id for import +random_id.dummy1: 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. + + +``` + +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d dir1` + + diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-multiple-projects.txt b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-multiple-projects.txt new file mode 100644 index 0000000000..023af68ffe --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-import-multiple-projects.txt @@ -0,0 +1 @@ +**Import Failed**: import cannot run on multiple projects. please specify one project. diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-merge.txt b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-merge.txt new file mode 100644 index 0000000000..9228a63148 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-merge.txt @@ -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` diff --git a/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-plan-again.txt b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-plan-again.txt new file mode 100644 index 0000000000..0d52456e5a --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-multiple-project/exp-output-plan-again.txt @@ -0,0 +1,63 @@ +Ran Plan for 2 projects: + +1. dir: `dir1` workspace: `default` +1. dir: `dir2` workspace: `default` + +### 1. dir: `dir1` workspace: `default` +```diff +random_id.dummy1: 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` +
Show Output + +```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.dummy2 will be created ++ resource "random_id" "dummy2" { + + 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` +
+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` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-apply-no-projects.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-apply-no-projects.txt new file mode 100644 index 0000000000..70da860967 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-apply-no-projects.txt @@ -0,0 +1,4 @@ +Ran Apply for 0 projects: + + + diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-autoplan.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-autoplan.txt new file mode 100644 index 0000000000..aa7b149fa7 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-autoplan.txt @@ -0,0 +1,52 @@ +Ran Plan for dir: `.` workspace: `default` + +
Show Output + +```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 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 = {} + } + + # random_id.dummy2 will be created ++ resource "random_id" "dummy2" { + + 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: 2 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 .` +
+Plan: 2 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` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy1.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy1.txt new file mode 100644 index 0000000000..4ea238b8df --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy1.txt @@ -0,0 +1,20 @@ +Ran Import for dir: `.` workspace: `default` + +```diff +random_id.dummy1: Importing from ID "AA"... +random_id.dummy1: Import prepared! + Prepared random_id for import +random_id.dummy1: 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. + + +``` + +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` + + diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy2.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy2.txt new file mode 100644 index 0000000000..c1120ec254 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-import-dummy2.txt @@ -0,0 +1,20 @@ +Ran Import for dir: `.` workspace: `default` + +```diff +random_id.dummy2: Importing from ID "BB"... +random_id.dummy2: Import prepared! + Prepared random_id for import +random_id.dummy2: 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. + + +``` + +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` + + diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-merge.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-merge.txt new file mode 100644 index 0000000000..872c5ee40c --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-merge.txt @@ -0,0 +1,3 @@ +Locks and plans deleted for the projects and workspaces modified in this pull request: + +- dir: `.` workspace: `default` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-plan-again.txt b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-plan-again.txt new file mode 100644 index 0000000000..035ebff06a --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/exp-output-plan-again.txt @@ -0,0 +1,22 @@ +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 .` + +--- +* :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` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project/main.tf b/server/controllers/events/testfixtures/test-repos/import-single-project/main.tf new file mode 100644 index 0000000000..2e60a118f5 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project/main.tf @@ -0,0 +1,9 @@ +resource "random_id" "dummy1" { + keepers = {} + byte_length = 1 +} + +resource "random_id" "dummy2" { + keepers = {} + byte_length = 1 +} diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index 33ca99e3d4..03458e0474 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -175,6 +175,7 @@ workflows: }, }, }, + Import: valid.DefaultImportStage, }, }, }, @@ -341,12 +342,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "default": { - Name: "default", - Plan: valid.DefaultPlanStage, - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "default": defaultWorkflow("default"), }, }, }, @@ -378,12 +374,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -417,12 +408,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -456,12 +442,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -495,12 +476,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -534,12 +510,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -573,12 +544,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -612,12 +578,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -651,12 +612,7 @@ workflows: }, }, Workflows: map[string]valid.Workflow{ - "myworkflow": { - Name: "myworkflow", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "myworkflow": defaultWorkflow("myworkflow"), }, }, }, @@ -796,6 +752,9 @@ workflows: steps: - plan # NOTE: we don't validate if they make sense - apply + import: + steps: + - import `, exp: valid.RepoCfg{ Version: 3, @@ -842,6 +801,13 @@ workflows: }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "import", + }, + }, + }, }, }, }, @@ -873,6 +839,10 @@ workflows: extra_args: [a, b] - apply: extra_args: ["a", "b"] + import: + steps: + - import: + extra_args: ["a", "b"] `, exp: valid.RepoCfg{ Version: 3, @@ -921,6 +891,14 @@ workflows: }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "import", + ExtraArgs: []string{"a", "b"}, + }, + }, + }, }, }, }, @@ -942,6 +920,9 @@ workflows: apply: steps: - run: echo apply "arg 2" + import: + steps: + - run: echo apply "arg 3" `, exp: valid.RepoCfg{ Version: 3, @@ -982,6 +963,14 @@ workflows: }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "run", + RunCommand: "echo apply \"arg 3\"", + }, + }, + }, }, }, }, @@ -1009,6 +998,11 @@ workflows: - env: name: env_name command: command and args + import: + steps: + - env: + name: env_name + value: env_value `, exp: valid.RepoCfg{ Version: 3, @@ -1052,6 +1046,15 @@ workflows: }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "env", + EnvVarName: "env_name", + EnvVarValue: "env_value", + }, + }, + }, }, }, }, @@ -1180,6 +1183,17 @@ func TestParseGlobalCfg(t *testing.T) { }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "run", + RunCommand: "custom command", + }, + { + StepName: "import", + }, + }, + }, } conftestVersion, _ := version.NewVersion("v1.0.0") @@ -1235,7 +1249,7 @@ func TestParseGlobalCfg(t *testing.T) { input: `repos: - id: /.*/ allowed_overrides: [invalid]`, - expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\", \"workflow\", \"delete_source_branch_on_merge\" and \"repo_locking\" are supported.).).", + expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\" and \"repo_locking\" are supported.).).", }, "invalid apply_requirement": { input: `repos: @@ -1243,6 +1257,12 @@ func TestParseGlobalCfg(t *testing.T) { apply_requirements: [invalid]`, expErr: "repos: (0: (apply_requirements: \"invalid\" is not a valid apply_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.).).", }, + "invalid import_requirement": { + input: `repos: +- id: /.*/ + import_requirements: [invalid]`, + expErr: "repos: (0: (import_requirements: \"invalid\" is not a valid import_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.).).", + }, "no workflows key": { input: `repos: []`, exp: defaultCfg, @@ -1259,12 +1279,7 @@ workflows: Repos: defaultCfg.Repos, Workflows: map[string]valid.Workflow{ "default": defaultCfg.Workflows["default"], - "name": { - Name: "name", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "name": defaultWorkflow("name"), }, }, }, @@ -1274,17 +1289,14 @@ workflows: name: apply: plan: + policy_check: + import: `, exp: valid.GlobalCfg{ Repos: defaultCfg.Repos, Workflows: map[string]valid.Workflow{ "default": defaultCfg.Workflows["default"], - "name": { - Name: "name", - Apply: valid.DefaultApplyStage, - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - }, + "name": defaultWorkflow("name"), }, }, }, @@ -1295,17 +1307,17 @@ workflows: apply: steps: plan: - steps:`, + steps: + policy_check: + steps: + import: + steps: +`, exp: valid.GlobalCfg{ Repos: defaultCfg.Repos, Workflows: map[string]valid.Workflow{ "default": defaultCfg.Workflows["default"], - "name": { - Name: "name", - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Apply: valid.DefaultApplyStage, - }, + "name": defaultWorkflow("name"), }, }, }, @@ -1320,7 +1332,7 @@ repos: workflow: custom1 post_workflow_hooks: - run: custom workflow command - allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge] + allowed_overrides: [apply_requirements, import_requirements, workflow, delete_source_branch_on_merge] allow_custom_workflows: true - id: /.*/ branch: /(master|main)/ @@ -1346,6 +1358,10 @@ workflows: steps: - run: custom command - apply + import: + steps: + - run: custom command + - import policies: conftest_version: v1.0.0 policy_sets: @@ -1363,7 +1379,7 @@ policies: PreWorkflowHooks: preWorkflowHooks, Workflow: &customWorkflow1, PostWorkflowHooks: postWorkflowHooks, - AllowedOverrides: []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"}, + AllowedOverrides: []string{"apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge"}, AllowCustomWorkflows: Bool(true), }, { @@ -1435,14 +1451,17 @@ workflows: policy_check: steps: [] apply: - steps: [] + steps: [] + import: + steps: [] `, exp: valid.GlobalCfg{ Repos: []valid.Repo{ { - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile(".*"), - ApplyRequirements: []string{}, + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + ApplyRequirements: []string{}, + ImportRequirements: []string{}, Workflow: &valid.Workflow{ Name: "default", Apply: valid.Stage{ @@ -1459,6 +1478,9 @@ workflows: }, }, }, + Import: valid.Stage{ + Steps: nil, + }, }, AllowedWorkflows: []string{}, AllowedOverrides: []string{}, @@ -1569,6 +1591,14 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "run", + RunCommand: "custom import", + }, + }, + }, } conftestVersion, _ := version.NewVersion("v1.0.0") @@ -1626,6 +1656,11 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { "steps": [ {"run": "my custom command"} ] + }, + "import": { + "steps": [ + {"run": "custom import"} + ] } } }, @@ -1790,3 +1825,13 @@ func String(v string) *string { return &v } // Bool is a helper routine that allocates a new bool value // to store v and returns a pointer to it. func Bool(v bool) *bool { return &v } + +func defaultWorkflow(name string) valid.Workflow { + return valid.Workflow{ + Name: name, + Apply: valid.DefaultApplyStage, + Plan: valid.DefaultPlanStage, + PolicyCheck: valid.DefaultPolicyCheckStage, + Import: valid.DefaultImportStage, + } +} diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go index a6c4b90f60..145928a5c2 100644 --- a/server/core/config/raw/global_cfg.go +++ b/server/core/config/raw/global_cfg.go @@ -24,6 +24,7 @@ type Repo struct { Branch string `yaml:"branch" json:"branch"` RepoConfigFile string `yaml:"repo_config_file" json:"repo_config_file"` ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"` + ImportRequirements []string `yaml:"import_requirements" json:"import_requirements"` PreWorkflowHooks []WorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"` Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"` PostWorkflowHooks []WorkflowHook `yaml:"post_workflow_hooks" json:"post_workflow_hooks"` @@ -96,9 +97,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg { // assumes: globalcfg is always initialized with one repo .* applyReqs := defaultCfg.Repos[0].ApplyRequirements - var globalApplyReqs []string - for _, req := range applyReqs { for _, nonOverrideableReq := range valid.NonOverrideableApplyReqs { if req == nonOverrideableReq { @@ -106,6 +105,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg { } } } + globalImportReqs := defaultCfg.Repos[0].ImportRequirements for k, v := range g.Workflows { validatedWorkflow := v.ToValid(k) @@ -126,7 +126,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg { var repos []valid.Repo for _, r := range g.Repos { - repos = append(repos, r.ToValid(workflows, globalApplyReqs)) + repos = append(repos, r.ToValid(workflows, globalApplyReqs, globalImportReqs)) } repos = append(defaultCfg.Repos, repos...) @@ -189,8 +189,8 @@ func (r Repo) Validate() error { overridesValid := func(value interface{}) error { overrides := value.([]string) for _, o := range overrides { - if o != valid.ApplyRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey { - return fmt.Errorf("%q is not a valid override, only %q, %q, %q and %q are supported", o, valid.ApplyRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey) + if o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey { + return fmt.Errorf("%q is not a valid override, only %q, %q, %q, %q and %q are supported", o, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey) } } return nil @@ -213,12 +213,13 @@ func (r Repo) Validate() error { validation.Field(&r.RepoConfigFile, validation.By(repoConfigFileValid)), validation.Field(&r.AllowedOverrides, validation.By(overridesValid)), validation.Field(&r.ApplyRequirements, validation.By(validApplyReq)), + validation.Field(&r.ImportRequirements, validation.By(validImportReq)), validation.Field(&r.Workflow, validation.By(workflowExists)), validation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)), ) } -func (r Repo) ToValid(workflows map[string]valid.Workflow, globalApplyReqs []string) valid.Repo { +func (r Repo) ToValid(workflows map[string]valid.Workflow, globalApplyReqs []string, globalImportReqs []string) valid.Repo { var id string var idRegex *regexp.Regexp if r.HasRegexID() { @@ -259,19 +260,29 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow, globalApplyReqs []str } var mergedApplyReqs []string - mergedApplyReqs = append(mergedApplyReqs, r.ApplyRequirements...) + var mergedImportReqs []string + mergedImportReqs = append(mergedImportReqs, r.ImportRequirements...) // only add global reqs if they don't exist already. -OUTER: +OuterGlobalApplyReqs: for _, globalReq := range globalApplyReqs { for _, currReq := range r.ApplyRequirements { if globalReq == currReq { - continue OUTER + continue OuterGlobalApplyReqs } } mergedApplyReqs = append(mergedApplyReqs, globalReq) } +OuterGlobalImportReqs: + for _, globalReq := range globalImportReqs { + for _, currReq := range r.ImportRequirements { + if globalReq == currReq { + continue OuterGlobalImportReqs + } + } + mergedImportReqs = append(mergedImportReqs, globalReq) + } return valid.Repo{ ID: id, @@ -279,6 +290,7 @@ OUTER: BranchRegex: branchRegex, RepoConfigFile: r.RepoConfigFile, ApplyRequirements: mergedApplyReqs, + ImportRequirements: mergedImportReqs, PreWorkflowHooks: preWorkflowHooks, Workflow: workflow, PostWorkflowHooks: postWorkflowHooks, diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index 081eb87903..add1db320e 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -14,10 +14,10 @@ import ( ) const ( - DefaultWorkspace = "default" - ApprovedApplyRequirement = "approved" - MergeableApplyRequirement = "mergeable" - UnDivergedApplyRequirement = "undiverged" + DefaultWorkspace = "default" + ApprovedRequirement = "approved" + MergeableRequirement = "mergeable" + UnDivergedRequirement = "undiverged" ) type Project struct { @@ -29,6 +29,7 @@ type Project struct { TerraformVersion *string `yaml:"terraform_version,omitempty"` Autoplan *Autoplan `yaml:"autoplan,omitempty"` ApplyRequirements []string `yaml:"apply_requirements,omitempty"` + ImportRequirements []string `yaml:"import_requirements,omitempty"` DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"` RepoLocking *bool `yaml:"repo_locking,omitempty"` ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"` @@ -73,6 +74,7 @@ func (p Project) Validate() error { return validation.ValidateStruct(&p, validation.Field(&p.Dir, validation.Required, validation.By(hasDotDot)), validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)), + validation.Field(&p.ImportRequirements, validation.By(validImportReq)), validation.Field(&p.TerraformVersion, validation.By(VersionValidator)), validation.Field(&p.Name, validation.By(validName)), validation.Field(&p.Branch, validation.By(branchValid)), @@ -110,8 +112,9 @@ func (p Project) ToValid() valid.Project { v.Autoplan = p.Autoplan.ToValid() } - // There are no default apply requirements. + // There are no default apply/import requirements. v.ApplyRequirements = p.ApplyRequirements + v.ImportRequirements = p.ImportRequirements v.Name = p.Name @@ -142,8 +145,18 @@ func validProjectName(name string) bool { func validApplyReq(value interface{}) error { reqs := value.([]string) for _, r := range reqs { - if r != ApprovedApplyRequirement && r != MergeableApplyRequirement && r != UnDivergedApplyRequirement { - return fmt.Errorf("%q is not a valid apply_requirement, only %q, %q and %q are supported", r, ApprovedApplyRequirement, MergeableApplyRequirement, UnDivergedApplyRequirement) + if r != ApprovedRequirement && r != MergeableRequirement && r != UnDivergedRequirement { + return fmt.Errorf("%q is not a valid apply_requirement, only %q, %q and %q are supported", r, ApprovedRequirement, MergeableRequirement, UnDivergedRequirement) + } + } + return nil +} + +func validImportReq(value interface{}) error { + reqs := value.([]string) + for _, r := range reqs { + if r != ApprovedRequirement && r != MergeableRequirement && r != UnDivergedRequirement { + return fmt.Errorf("%q is not a valid import_requirement, only %q, %q and %q are supported", r, ApprovedRequirement, MergeableRequirement, UnDivergedRequirement) } } return nil diff --git a/server/core/config/raw/project_test.go b/server/core/config/raw/project_test.go index 1698a35678..2fa360c882 100644 --- a/server/core/config/raw/project_test.go +++ b/server/core/config/raw/project_test.go @@ -21,14 +21,15 @@ func TestProject_UnmarshalYAML(t *testing.T) { description: "omit unset fields", input: "", exp: raw.Project{ - Dir: nil, - Workspace: nil, - Workflow: nil, - TerraformVersion: nil, - Autoplan: nil, - ApplyRequirements: nil, - Name: nil, - Branch: nil, + Dir: nil, + Workspace: nil, + Workflow: nil, + TerraformVersion: nil, + Autoplan: nil, + ApplyRequirements: nil, + ImportRequirements: nil, + Name: nil, + Branch: nil, }, }, { @@ -45,6 +46,8 @@ autoplan: enabled: false apply_requirements: - mergeable +import_requirements: +- mergeable execution_order_group: 10`, exp: raw.Project{ Name: String("myname"), @@ -58,6 +61,7 @@ execution_order_group: 10`, Enabled: Bool(false), }, ApplyRequirements: []string{"mergeable"}, + ImportRequirements: []string{"mergeable"}, ExecutionOrderGroup: Int(10), }, }, @@ -180,6 +184,22 @@ func TestProject_Validate(t *testing.T) { }, expErr: "", }, + { + description: "import reqs with unsupported", + input: raw.Project{ + Dir: String("."), + ImportRequirements: []string{"unsupported"}, + }, + expErr: "import_requirements: \"unsupported\" is not a valid import_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.", + }, + { + description: "import reqs with undiverged, mergeable and approved requirements", + input: raw.Project{ + Dir: String("."), + ImportRequirements: []string{"undiverged", "mergeable", "approved"}, + }, + expErr: "", + }, { description: "empty tf version string", input: raw.Project{ diff --git a/server/core/config/raw/repo_cfg_test.go b/server/core/config/raw/repo_cfg_test.go index d5493fcc25..6bde9473f0 100644 --- a/server/core/config/raw/repo_cfg_test.go +++ b/server/core/config/raw/repo_cfg_test.go @@ -307,6 +307,7 @@ func TestConfig_ToValid(t *testing.T) { Plan: &raw.Stage{}, Apply: nil, PolicyCheck: nil, + Import: nil, }, }, }, @@ -316,25 +317,11 @@ func TestConfig_ToValid(t *testing.T) { ParallelApply: false, Workflows: map[string]valid.Workflow{ "myworkflow": { - Name: "myworkflow", - Plan: valid.DefaultPlanStage, - PolicyCheck: valid.Stage{ - Steps: []valid.Step{ - { - StepName: "show", - }, - { - StepName: "policy_check", - }, - }, - }, - Apply: valid.Stage{ - Steps: []valid.Step{ - { - StepName: "apply", - }, - }, - }, + Name: "myworkflow", + Plan: valid.DefaultPlanStage, + PolicyCheck: valid.DefaultPolicyCheckStage, + Apply: valid.DefaultApplyStage, + Import: valid.DefaultImportStage, }, }, }, @@ -368,6 +355,13 @@ func TestConfig_ToValid(t *testing.T) { }, }, }, + Import: &raw.Stage{ + Steps: []raw.Step{ + { + Key: String("import"), + }, + }, + }, }, }, Projects: []raw.Project{ @@ -404,6 +398,13 @@ func TestConfig_ToValid(t *testing.T) { }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "import", + }, + }, + }, }, }, Projects: []valid.Project{ diff --git a/server/core/config/raw/step.go b/server/core/config/raw/step.go index a32a0e60c4..3d5e41f675 100644 --- a/server/core/config/raw/step.go +++ b/server/core/config/raw/step.go @@ -24,6 +24,7 @@ const ( InitStepName = "init" EnvStepName = "env" MultiEnvStepName = "multienv" + ImportStepName = "import" ) // Step represents a single action/command to perform. In YAML, it can be set as @@ -88,7 +89,8 @@ func (s Step) validStepName(stepName string) bool { stepName == EnvStepName || stepName == MultiEnvStepName || stepName == ShowStepName || - stepName == PolicyCheckStepName + stepName == PolicyCheckStepName || + stepName == ImportStepName } func (s Step) Validate() error { diff --git a/server/core/config/raw/step_test.go b/server/core/config/raw/step_test.go index b45cfd90d9..3321dce360 100644 --- a/server/core/config/raw/step_test.go +++ b/server/core/config/raw/step_test.go @@ -467,6 +467,15 @@ func TestStep_ToValid(t *testing.T) { EnvVarName: "test", }, }, + { + description: "import step", + input: raw.Step{ + Key: String("import"), + }, + exp: valid.Step{ + StepName: "import", + }, + }, { description: "init extra_args", input: raw.Step{ @@ -523,6 +532,20 @@ func TestStep_ToValid(t *testing.T) { ExtraArgs: []string{"arg1", "arg2"}, }, }, + { + description: "import extra_args", + input: raw.Step{ + Map: MapType{ + "import": { + "extra_args": []string{"arg1", "arg2"}, + }, + }, + }, + exp: valid.Step{ + StepName: "import", + ExtraArgs: []string{"arg1", "arg2"}, + }, + }, { description: "run step", input: raw.Step{ diff --git a/server/core/config/raw/workflow.go b/server/core/config/raw/workflow.go index 59050dce2b..d5164a8319 100644 --- a/server/core/config/raw/workflow.go +++ b/server/core/config/raw/workflow.go @@ -9,6 +9,7 @@ type Workflow struct { Apply *Stage `yaml:"apply,omitempty" json:"apply,omitempty"` Plan *Stage `yaml:"plan,omitempty" json:"plan,omitempty"` PolicyCheck *Stage `yaml:"policy_check,omitempty" json:"policy_check,omitempty"` + Import *Stage `yaml:"import,omitempty" json:"import,omitempty"` } func (w Workflow) Validate() error { @@ -16,6 +17,7 @@ func (w Workflow) Validate() error { validation.Field(&w.Apply), validation.Field(&w.Plan), validation.Field(&w.PolicyCheck), + validation.Field(&w.Import), ) } @@ -35,6 +37,7 @@ func (w Workflow) ToValid(name string) valid.Workflow { v.Apply = w.toValidStage(w.Apply, valid.DefaultApplyStage) v.Plan = w.toValidStage(w.Plan, valid.DefaultPlanStage) v.PolicyCheck = w.toValidStage(w.PolicyCheck, valid.DefaultPolicyCheckStage) + v.Import = w.toValidStage(w.Import, valid.DefaultImportStage) return v } diff --git a/server/core/config/raw/workflow_test.go b/server/core/config/raw/workflow_test.go index 8ff25e1a24..04c753a63b 100644 --- a/server/core/config/raw/workflow_test.go +++ b/server/core/config/raw/workflow_test.go @@ -148,6 +148,7 @@ func TestWorkflow_ToValid(t *testing.T) { Apply: valid.DefaultApplyStage, Plan: valid.DefaultPlanStage, PolicyCheck: valid.DefaultPolicyCheckStage, + Import: valid.DefaultImportStage, }, }, { @@ -174,6 +175,13 @@ func TestWorkflow_ToValid(t *testing.T) { }, }, }, + Import: &raw.Stage{ + Steps: []raw.Step{ + { + Key: String("import"), + }, + }, + }, }, exp: valid.Workflow{ Apply: valid.Stage{ @@ -197,6 +205,13 @@ func TestWorkflow_ToValid(t *testing.T) { }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "import", + }, + }, + }, }, }, } diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index d13ecd3874..df5d1f7575 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -9,11 +9,12 @@ import ( "github.com/runatlantis/atlantis/server/logging" ) -const MergeableApplyReq = "mergeable" -const ApprovedApplyReq = "approved" -const UnDivergedApplyReq = "undiverged" -const PoliciesPassedApplyReq = "policies_passed" +const MergeableCommandReq = "mergeable" +const ApprovedCommandReq = "approved" +const UnDivergedCommandReq = "undiverged" +const PoliciesPassedCommandReq = "policies_passed" const ApplyRequirementsKey = "apply_requirements" +const ImportRequirementsKey = "import_requirements" const PreWorkflowHooksKey = "pre_workflow_hooks" const WorkflowKey = "workflow" const PostWorkflowHooksKey = "post_workflow_hooks" @@ -32,7 +33,7 @@ const DefaultAtlantisFile = "atlantis.yaml" // TODO: Make this more customizable, not everyone wants this rigid workflow // maybe something along the lines of defining overridable/non-overrideable apply // requirements in the config and removing the flag to enable policy checking. -var NonOverrideableApplyReqs = []string{PoliciesPassedApplyReq} +var NonOverrideableApplyReqs = []string{PoliciesPassedCommandReq} // GlobalCfg is the final parsed version of server-side repo config. type GlobalCfg struct { @@ -67,6 +68,7 @@ type Repo struct { BranchRegex *regexp.Regexp RepoConfigFile string ApplyRequirements []string + ImportRequirements []string PreWorkflowHooks []*WorkflowHook Workflow *Workflow PostWorkflowHooks []*WorkflowHook @@ -79,6 +81,7 @@ type Repo struct { type MergedProjectCfg struct { ApplyRequirements []string + ImportRequirements []string Workflow Workflow AllowedWorkflows []string RepoRelDir string @@ -134,6 +137,18 @@ var DefaultPlanStage = Stage{ }, } +// DefaultImportStage is the Atlantis default import stage. +var DefaultImportStage = Stage{ + Steps: []Step{ + { + StepName: "init", + }, + { + StepName: "import", + }, + }, +} + // Deprecated: use NewGlobalCfgFromArgs func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, unDivergedReq bool, preWorkflowHooks []*WorkflowHook, postWorkflowHooks []*WorkflowHook) GlobalCfg { return NewGlobalCfgFromArgs(GlobalCfgArgs{ @@ -178,31 +193,31 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { Apply: DefaultApplyStage, Plan: DefaultPlanStage, PolicyCheck: DefaultPolicyCheckStage, + Import: DefaultImportStage, } // Must construct slices here instead of using a `var` declaration because // we treat nil slices differently. - applyReqs := []string{} + commandReqs := []string{} allowedOverrides := []string{} allowedWorkflows := []string{} if args.MergeableReq { - applyReqs = append(applyReqs, MergeableApplyReq) + commandReqs = append(commandReqs, MergeableCommandReq) } if args.ApprovedReq { - applyReqs = append(applyReqs, ApprovedApplyReq) + commandReqs = append(commandReqs, ApprovedCommandReq) } if args.UnDivergedReq { - applyReqs = append(applyReqs, UnDivergedApplyReq) + commandReqs = append(commandReqs, UnDivergedCommandReq) } - if args.PolicyCheckEnabled { - applyReqs = append(applyReqs, PoliciesPassedApplyReq) + commandReqs = append(commandReqs, PoliciesPassedCommandReq) } allowCustomWorkflows := false deleteSourceBranchOnMerge := false repoLockingKey := true if args.AllowRepoCfg { - allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} + allowedOverrides = []string{ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} allowCustomWorkflows = true } @@ -211,8 +226,13 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { { IDRegex: regexp.MustCompile(".*"), BranchRegex: regexp.MustCompile(".*"), +<<<<<<< HEAD RepoConfigFile: args.RepoConfigFile, ApplyRequirements: applyReqs, +======= + ApplyRequirements: commandReqs, + ImportRequirements: commandReqs, +>>>>>>> ec0bdf6c (feat: atlantis import) PreWorkflowHooks: args.PreWorkflowHooks, Workflow: &defaultWorkflow, PostWorkflowHooks: args.PostWorkflowHooks, @@ -257,7 +277,7 @@ func (r Repo) IDString() string { // final config. It assumes that all configs have been validated. func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg { log.Debug("MergeProjectCfg started") - applyReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) + applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) // If repos are allowed to override certain keys then override them. for _, key := range allowedOverrides { @@ -267,6 +287,11 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro log.Debug("overriding server-defined %s with repo settings: [%s]", ApplyRequirementsKey, strings.Join(proj.ApplyRequirements, ",")) applyReqs = proj.ApplyRequirements } + case ImportRequirementsKey: + if proj.ImportRequirements != nil { + log.Debug("overriding server-defined %s with repo settings: [%s]", ImportRequirementsKey, strings.Join(proj.ImportRequirements, ",")) + importReqs = proj.ImportRequirements + } case WorkflowKey: if proj.WorkflowName != nil { // We iterate over the global workflows first and the repo @@ -312,11 +337,12 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro log.Debug("MergeProjectCfg completed") } - log.Debug("final settings: %s: [%s], %s: %s", - ApplyRequirementsKey, strings.Join(applyReqs, ","), WorkflowKey, workflow.Name) + log.Debug("final settings: %s: [%s], %s: [%s], %s: %s", + ApplyRequirementsKey, strings.Join(applyReqs, ","), ImportRequirementsKey, strings.Join(importReqs, ","), WorkflowKey, workflow.Name) return MergedProjectCfg{ ApplyRequirements: applyReqs, + ImportRequirements: importReqs, Workflow: workflow, RepoRelDir: proj.Dir, Workspace: proj.Workspace, @@ -335,9 +361,10 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro // repo with id repoID. It is used when there is no repo config. func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg { log.Debug("building config based on server-side config") - applyReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) + applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) return MergedProjectCfg{ ApplyRequirements: applyReqs, + ImportRequirements: importReqs, Workflow: workflow, RepoRelDir: repoRelDir, Workspace: workspace, @@ -387,6 +414,9 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { if p.ApplyRequirements != nil && !sliceContainsF(allowedOverrides, ApplyRequirementsKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", ApplyRequirementsKey, AllowedOverridesKey, ApplyRequirementsKey) } + if p.ImportRequirements != nil && !sliceContainsF(allowedOverrides, ImportRequirementsKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", ImportRequirementsKey, AllowedOverridesKey, ImportRequirementsKey) + } if p.DeleteSourceBranchOnMerge != nil && !sliceContainsF(allowedOverrides, DeleteSourceBranchOnMergeKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", DeleteSourceBranchOnMergeKey, AllowedOverridesKey, DeleteSourceBranchOnMergeKey) } @@ -451,7 +481,7 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { } // getMatchingCfg returns the key settings for repoID. -func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (applyReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool) { +func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool) { toLog := make(map[string]string) traceF := func(repoIdx int, repoID string, key string, val interface{}) string { from := "default server config" @@ -473,7 +503,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} { + for _, key := range []string{ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { @@ -482,6 +512,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app toLog[ApplyRequirementsKey] = traceF(i, repo.IDString(), ApplyRequirementsKey, repo.ApplyRequirements) applyReqs = repo.ApplyRequirements } + case ImportRequirementsKey: + if repo.ImportRequirements != nil { + toLog[ImportRequirementsKey] = traceF(i, repo.IDString(), ImportRequirementsKey, repo.ImportRequirements) + importReqs = repo.ImportRequirements + } case WorkflowKey: if repo.Workflow != nil { toLog[WorkflowKey] = traceF(i, repo.IDString(), WorkflowKey, repo.Workflow.Name) diff --git a/server/core/config/valid/global_cfg_test.go b/server/core/config/valid/global_cfg_test.go index c161a03ecc..4879ee0e85 100644 --- a/server/core/config/valid/global_cfg_test.go +++ b/server/core/config/valid/global_cfg_test.go @@ -45,6 +45,16 @@ func TestNewGlobalCfg(t *testing.T) { }, }, }, + Import: valid.Stage{ + Steps: []valid.Step{ + { + StepName: "init", + }, + { + StepName: "import", + }, + }, + }, } baseCfg := valid.GlobalCfg{ Repos: []valid.Repo{ @@ -52,6 +62,7 @@ func TestNewGlobalCfg(t *testing.T) { IDRegex: regexp.MustCompile(".*"), BranchRegex: regexp.MustCompile(".*"), ApplyRequirements: []string{}, + ImportRequirements: []string{}, Workflow: &expDefaultWorkflow, AllowedWorkflows: []string{}, AllowedOverrides: []string{}, @@ -164,16 +175,19 @@ func TestNewGlobalCfg(t *testing.T) { if c.allowRepoCfg { exp.Repos[0].AllowCustomWorkflows = Bool(true) - exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking"} + exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking"} } if c.mergeableReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "mergeable") + exp.Repos[0].ImportRequirements = append(exp.Repos[0].ImportRequirements, "mergeable") } if c.approvedReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "approved") + exp.Repos[0].ImportRequirements = append(exp.Repos[0].ImportRequirements, "approved") } if c.unDivergedReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "undiverged") + exp.Repos[0].ImportRequirements = append(exp.Repos[0].ImportRequirements, "undiverged") } Equals(t, exp, act) @@ -533,6 +547,25 @@ func TestGlobalCfg_ValidateRepoCfg(t *testing.T) { repoID: "github.com/owner/repo", expErr: "repo config not allowed to set 'apply_requirements' key: server-side config needs 'allowed_overrides: [apply_requirements]'", }, + "import_reqs not allowed": { + gCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{ + AllowRepoCfg: false, + MergeableReq: false, + ApprovedReq: false, + UnDivergedReq: false, + }), + rCfg: valid.RepoCfg{ + Projects: []valid.Project{ + { + Dir: ".", + Workspace: "default", + ImportRequirements: []string{""}, + }, + }, + }, + repoID: "github.com/owner/repo", + expErr: "repo config not allowed to set 'import_requirements' key: server-side config needs 'allowed_overrides: [import_requirements]'", + }, "repo workflow doesn't exist": { gCfg: valid.NewGlobalCfgFromArgs(valid.GlobalCfgArgs{ AllowRepoCfg: true, @@ -590,12 +623,14 @@ policies: WorkflowName: String("custom"), }, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, Workflow: valid.Workflow{ Name: "default", Apply: valid.DefaultApplyStage, Plan: valid.DefaultPlanStage, PolicyCheck: valid.DefaultPolicyCheckStage, + Import: valid.DefaultImportStage, }, PolicySets: valid.PolicySets{ Version: nil, @@ -632,12 +667,14 @@ policies: WorkflowName: String("custom"), }, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, Workflow: valid.Workflow{ Name: "default", Apply: valid.DefaultApplyStage, Plan: valid.DefaultPlanStage, PolicyCheck: valid.DefaultPolicyCheckStage, + Import: valid.DefaultImportStage, }, PolicySets: valid.PolicySets{ Version: version, @@ -693,6 +730,13 @@ policies: func TestGlobalCfg_MergeProjectCfg(t *testing.T) { var emptyPolicySets valid.PolicySets + defaultWorkflow := valid.Workflow{ + Name: "default", + Apply: valid.DefaultApplyStage, + PolicyCheck: valid.DefaultPolicyCheckStage, + Plan: valid.DefaultPlanStage, + Import: valid.DefaultImportStage, + } cases := map[string]struct { gCfg string repoID string @@ -717,7 +761,8 @@ workflows: }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, Workflow: valid.Workflow{ Name: "custom", Apply: valid.DefaultApplyStage, @@ -729,6 +774,7 @@ workflows: }, }, }, + Import: valid.DefaultImportStage, }, RepoRelDir: ".", Workspace: "default", @@ -747,25 +793,49 @@ repos: `, repoID: "github.com/owner/repo", proj: valid.Project{ - Dir: ".", - Workspace: "default", - ApplyRequirements: []string{"mergeable"}, + Dir: ".", + Workspace: "default", + ApplyRequirements: []string{"mergeable"}, + ImportRequirements: []string{}, }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{"mergeable"}, - Workflow: valid.Workflow{ - Name: "default", - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Plan: valid.DefaultPlanStage, - }, - RepoRelDir: ".", - Workspace: "default", - Name: "", - AutoplanEnabled: false, - PolicySets: emptyPolicySets, - RepoLocking: true, + ApplyRequirements: []string{"mergeable"}, + ImportRequirements: []string{}, + Workflow: defaultWorkflow, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: true, + }, + }, + "repo-side import reqs win out if allowed": { + gCfg: ` +repos: +- id: /.*/ + allowed_overrides: [import_requirements] + import_requirements: [approved] +`, + repoID: "github.com/owner/repo", + proj: valid.Project{ + Dir: ".", + Workspace: "default", + ApplyRequirements: []string{}, + ImportRequirements: []string{"mergeable"}, + }, + repoWorkflows: nil, + exp: valid.MergedProjectCfg{ + ApplyRequirements: []string{}, + ImportRequirements: []string{"mergeable"}, + Workflow: defaultWorkflow, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "repo-side repo_locking win out if allowed": { @@ -776,26 +846,23 @@ repos: `, repoID: "github.com/owner/repo", proj: valid.Project{ - Dir: ".", - Workspace: "default", - ApplyRequirements: []string{}, - RepoLocking: Bool(true), + Dir: ".", + Workspace: "default", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoLocking: Bool(true), }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, - Workflow: valid.Workflow{ - Name: "default", - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Plan: valid.DefaultPlanStage, - }, - RepoRelDir: ".", - Workspace: "default", - Name: "", - AutoplanEnabled: false, - PolicySets: emptyPolicySets, - RepoLocking: false, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + Workflow: defaultWorkflow, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: false, }, }, "last server-side match wins": { @@ -803,10 +870,13 @@ repos: repos: - id: /.*/ apply_requirements: [approved] + import_requirements: [approved] - id: /github.com/.*/ apply_requirements: [mergeable] + import_requirements: [mergeable] - id: github.com/owner/repo apply_requirements: [approved, mergeable] + import_requirements: [approved, mergeable] `, repoID: "github.com/owner/repo", proj: valid.Project{ @@ -816,19 +886,15 @@ repos: }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{"approved", "mergeable"}, - Workflow: valid.Workflow{ - Name: "default", - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Plan: valid.DefaultPlanStage, - }, - RepoRelDir: "mydir", - Workspace: "myworkspace", - Name: "myname", - AutoplanEnabled: false, - PolicySets: emptyPolicySets, - RepoLocking: true, + ApplyRequirements: []string{"approved", "mergeable"}, + ImportRequirements: []string{"approved", "mergeable"}, + Workflow: defaultWorkflow, + RepoRelDir: "mydir", + Workspace: "myworkspace", + Name: "myname", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "autoplan is set properly": { @@ -845,19 +911,15 @@ repos: }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, - Workflow: valid.Workflow{ - Name: "default", - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Plan: valid.DefaultPlanStage, - }, - RepoRelDir: "mydir", - Workspace: "myworkspace", - Name: "myname", - AutoplanEnabled: true, - PolicySets: emptyPolicySets, - RepoLocking: true, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + Workflow: defaultWorkflow, + RepoRelDir: "mydir", + Workspace: "myworkspace", + Name: "myname", + AutoplanEnabled: true, + PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "execution order group is set": { @@ -875,13 +937,9 @@ repos: }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ - ApplyRequirements: []string{}, - Workflow: valid.Workflow{ - Name: "default", - Apply: valid.DefaultApplyStage, - PolicyCheck: valid.DefaultPolicyCheckStage, - Plan: valid.DefaultPlanStage, - }, + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + Workflow: defaultWorkflow, RepoRelDir: "mydir", Workspace: "myworkspace", Name: "myname", @@ -960,19 +1018,22 @@ func TestRepo_BranchMatches(t *testing.T) { func TestGlobalCfg_MatchingRepo(t *testing.T) { defaultRepo := valid.Repo{ - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile(".*"), - ApplyRequirements: []string{}, + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile(".*"), + ApplyRequirements: []string{}, + ImportRequirements: []string{}, } repo1 := valid.Repo{ - IDRegex: regexp.MustCompile(".*"), - BranchRegex: regexp.MustCompile("^main$"), - ApplyRequirements: []string{"approved"}, + IDRegex: regexp.MustCompile(".*"), + BranchRegex: regexp.MustCompile("^main$"), + ApplyRequirements: []string{"approved"}, + ImportRequirements: []string{"approved"}, } repo2 := valid.Repo{ - ID: "github.com/owner/repo", - BranchRegex: regexp.MustCompile("^main$"), - ApplyRequirements: []string{"approved", "mergeable"}, + ID: "github.com/owner/repo", + BranchRegex: regexp.MustCompile("^main$"), + ApplyRequirements: []string{"approved", "mergeable"}, + ImportRequirements: []string{"approved", "mergeable"}, } cases := map[string]struct { diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 331b4c7ba0..1222f56b4f 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -127,6 +127,7 @@ type Project struct { TerraformVersion *version.Version Autoplan Autoplan ApplyRequirements []string + ImportRequirements []string DeleteSourceBranchOnMerge *bool RepoLocking *bool ExecutionOrderGroup int @@ -168,4 +169,5 @@ type Workflow struct { Apply Stage Plan Stage PolicyCheck Stage + Import Stage } diff --git a/server/core/runtime/import_step_runner.go b/server/core/runtime/import_step_runner.go new file mode 100644 index 0000000000..3599ae9794 --- /dev/null +++ b/server/core/runtime/import_step_runner.go @@ -0,0 +1,38 @@ +package runtime + +import ( + "os" + "path/filepath" + + version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/events/command" +) + +type ImportStepRunner struct { + TerraformExecutor TerraformExec + DefaultTFVersion *version.Version +} + +func (p *ImportStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfVersion := p.DefaultTFVersion + if ctx.TerraformVersion != nil { + tfVersion = ctx.TerraformVersion + } + + importCmd := []string{"import"} + importCmd = append(importCmd, extraArgs...) + importCmd = append(importCmd, ctx.EscapedCommentArgs...) + out, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfVersion, ctx.Workspace) + + // If the import 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("import successful, deleting planfile") + if removeErr := os.Remove(planPath); removeErr != nil { + ctx.Log.Warn("failed to delete planfile after successful import: %s", removeErr) + } + } + } + return out, err +} diff --git a/server/core/runtime/import_step_runner_test.go b/server/core/runtime/import_step_runner_test.go new file mode 100644 index 0000000000..cbe97099a8 --- /dev/null +++ b/server/core/runtime/import_step_runner_test.go @@ -0,0 +1,61 @@ +package runtime + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/go-version" + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/core/runtime/mocks/matchers" + "github.com/runatlantis/atlantis/server/core/terraform/mocks" + matchers2 "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestImportStepRunner_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{"-var", "foo=bar", "addr", "id"}, + Workspace: workspace, + RepoRelDir: ".", + User: models.User{Username: "username"}, + Pull: models.PullRequest{ + Num: 2, + }, + BaseRepo: models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, + } + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.15.0") + s := &ImportStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + + When(terraform.RunCommandWithVersion(matchers.AnyModelsProjectCommandContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.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{"import", "-var", "foo=bar", "addr", "id"} + 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") +} diff --git a/server/core/terraform/mocks/matchers/command_projectcontext.go b/server/core/terraform/mocks/matchers/command_projectcontext.go new file mode 100644 index 0000000000..c25f35d932 --- /dev/null +++ b/server/core/terraform/mocks/matchers/command_projectcontext.go @@ -0,0 +1,33 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "github.com/petergtz/pegomock" + "reflect" + + command "github.com/runatlantis/atlantis/server/events/command" +) + +func AnyCommandProjectContext() command.ProjectContext { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(command.ProjectContext))(nil)).Elem())) + var nullValue command.ProjectContext + return nullValue +} + +func EqCommandProjectContext(value command.ProjectContext) command.ProjectContext { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue command.ProjectContext + return nullValue +} + +func NotEqCommandProjectContext(value command.ProjectContext) command.ProjectContext { + pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) + var nullValue command.ProjectContext + return nullValue +} + +func CommandProjectContextThat(matcher pegomock.ArgumentMatcher) command.ProjectContext { + pegomock.RegisterMatcher(matcher) + var nullValue command.ProjectContext + return nullValue +} diff --git a/server/events/apply_requirement_handler.go b/server/events/apply_requirement_handler.go deleted file mode 100644 index 107b163cab..0000000000 --- a/server/events/apply_requirement_handler.go +++ /dev/null @@ -1,43 +0,0 @@ -package events - -import ( - "github.com/runatlantis/atlantis/server/core/config/raw" - "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/models" -) - -//go:generate pegomock generate -m --package mocks -o mocks/mock_apply_handler.go ApplyRequirement -type ApplyRequirement interface { - ValidateProject(repoDir string, ctx command.ProjectContext) (string, error) -} - -type AggregateApplyRequirements struct { - WorkingDir WorkingDir -} - -func (a *AggregateApplyRequirements) ValidateProject(repoDir string, ctx command.ProjectContext) (failure string, err error) { - for _, req := range ctx.ApplyRequirements { - switch req { - case raw.ApprovedApplyRequirement: - if !ctx.PullReqStatus.ApprovalStatus.IsApproved { - return "Pull request must be approved by at least one person other than the author before running apply.", nil - } - // this should come before mergeability check since mergeability is a superset of this check. - case valid.PoliciesPassedApplyReq: - if ctx.ProjectPlanStatus == models.ErroredPolicyCheckStatus { - return "All policies must pass for project before running apply", nil - } - case raw.MergeableApplyRequirement: - if !ctx.PullReqStatus.Mergeable { - return "Pull request must be mergeable before running apply.", nil - } - case raw.UnDivergedApplyRequirement: - if a.WorkingDir.HasDiverged(ctx.Log, repoDir) { - return "Default branch must be rebased onto pull request before running apply.", nil - } - } - } - // Passed all apply requirements configured. - return "", nil -} diff --git a/server/events/command/name.go b/server/events/command/name.go index 9194102da6..1396b41b9a 100644 --- a/server/events/command/name.go +++ b/server/events/command/name.go @@ -21,10 +21,12 @@ const ( PolicyCheck // ApprovePolicies is a command to approve policies with owner check ApprovePolicies - // Autoplan is a command to run terrafor plan on PR open/update if autoplan is enabled + // Autoplan is a command to run terraform plan on PR open/update if autoplan is enabled Autoplan // Version is a command to run terraform version. Version + // Import is a command to run terraform import + Import // Adding more? Don't forget to update String() below ) @@ -49,6 +51,18 @@ func (c Name) String() string { return "approve_policies" case Version: return "version" + case Import: + return "import" } return "" } + +// DefaultUsage returns the command default usage +func (c Name) DefaultUsage() string { + switch c { + case Import: + return "import -- ADDR ID" + default: + return c.String() + } +} diff --git a/server/events/command/name_test.go b/server/events/command/name_test.go index 6f766000c0..461d228856 100644 --- a/server/events/command/name_test.go +++ b/server/events/command/name_test.go @@ -4,29 +4,65 @@ import ( "testing" "github.com/runatlantis/atlantis/server/events/command" - . "github.com/runatlantis/atlantis/testing" ) -func TestApplyCommand_String(t *testing.T) { - uc := command.Apply - - Equals(t, "apply", uc.String()) -} - -func TestPlanCommand_String(t *testing.T) { - uc := command.Plan - - Equals(t, "plan", uc.String()) +func TestName_TitleString(t *testing.T) { + tests := []struct { + c command.Name + want string + }{ + {command.Apply, "Apply"}, + {command.PolicyCheck, "Policy Check"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.c.TitleString(); got != tt.want { + t.Errorf("TitleString() = %v, want %v", got, tt.want) + } + }) + } } -func TestPolicyCheckCommand_String(t *testing.T) { - uc := command.PolicyCheck - - Equals(t, "policy_check", uc.String()) +func TestName_String(t *testing.T) { + tests := []struct { + c command.Name + want string + }{ + {command.Apply, "apply"}, + {command.Plan, "plan"}, + {command.Unlock, "unlock"}, + {command.PolicyCheck, "policy_check"}, + {command.ApprovePolicies, "approve_policies"}, + {command.Version, "version"}, + {command.Import, "import"}, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.c.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } } -func TestUnlockCommand_String(t *testing.T) { - uc := command.Unlock - - Equals(t, "unlock", uc.String()) +func TestName_DefaultUsage(t *testing.T) { + tests := []struct { + c command.Name + want string + }{ + {command.Apply, "apply"}, + {command.Plan, "plan"}, + {command.Unlock, "unlock"}, + {command.PolicyCheck, "policy_check"}, + {command.ApprovePolicies, "approve_policies"}, + {command.Version, "version"}, + {command.Import, "import -- ADDR ID"}, + } + for _, tt := range tests { + t.Run(tt.c.String(), func(t *testing.T) { + if got := tt.c.DefaultUsage(); got != tt.want { + t.Errorf("DefaultUsage() = %v, want %v", got, tt.want) + } + }) + } } diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go index 540b4abadf..e64e762578 100644 --- a/server/events/command/project_context.go +++ b/server/events/command/project_context.go @@ -26,6 +26,9 @@ type ProjectContext struct { // ApplyRequirements is the list of requirements that must be satisfied // before we will run the apply stage. ApplyRequirements []string + // ImportRequirements is the list of requirements that must be satisfied + // before we will run the import stage. + ImportRequirements []string // AutomergeEnabled is true if automerge is enabled for the repo that this // project is in. AutomergeEnabled bool diff --git a/server/events/command/project_result.go b/server/events/command/project_result.go index 3fadd00c83..9d5c83ea7c 100644 --- a/server/events/command/project_result.go +++ b/server/events/command/project_result.go @@ -15,6 +15,7 @@ type ProjectResult struct { PolicyCheckSuccess *models.PolicyCheckSuccess ApplySuccess string VersionSuccess string + ImportSuccess *models.ImportSuccess ProjectName string } diff --git a/server/events/command_requirement_handler.go b/server/events/command_requirement_handler.go new file mode 100644 index 0000000000..f67ef531be --- /dev/null +++ b/server/events/command_requirement_handler.go @@ -0,0 +1,65 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/core/config/raw" + "github.com/runatlantis/atlantis/server/core/config/valid" + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" +) + +//go:generate pegomock generate -m --package mocks -o mocks/mock_command_requirement_handler.go CommandRequirementHandler +type CommandRequirementHandler interface { + ValidateApplyProject(repoDir string, ctx command.ProjectContext) (string, error) + ValidateImportProject(repoDir string, ctx command.ProjectContext) (string, error) +} + +type DefaultCommandRequirementHandler struct { + WorkingDir WorkingDir +} + +func (a *DefaultCommandRequirementHandler) ValidateApplyProject(repoDir string, ctx command.ProjectContext) (failure string, err error) { + for _, req := range ctx.ApplyRequirements { + switch req { + case raw.ApprovedRequirement: + if !ctx.PullReqStatus.ApprovalStatus.IsApproved { + return "Pull request must be approved by at least one person other than the author before running apply.", nil + } + // this should come before mergeability check since mergeability is a superset of this check. + case valid.PoliciesPassedCommandReq: + if ctx.ProjectPlanStatus == models.ErroredPolicyCheckStatus { + return "All policies must pass for project before running apply", nil + } + case raw.MergeableRequirement: + if !ctx.PullReqStatus.Mergeable { + return "Pull request must be mergeable before running apply.", nil + } + case raw.UnDivergedRequirement: + if a.WorkingDir.HasDiverged(ctx.Log, repoDir) { + return "Default branch must be rebased onto pull request before running apply.", nil + } + } + } + // Passed all apply requirements configured. + return "", nil +} + +func (a *DefaultCommandRequirementHandler) ValidateImportProject(repoDir string, ctx command.ProjectContext) (failure string, err error) { + for _, req := range ctx.ImportRequirements { + switch req { + case raw.ApprovedRequirement: + if !ctx.PullReqStatus.ApprovalStatus.IsApproved { + return "Pull request must be approved by at least one person other than the author before running import.", nil + } + case raw.MergeableRequirement: + if !ctx.PullReqStatus.Mergeable { + return "Pull request must be mergeable before running import.", nil + } + case raw.UnDivergedRequirement: + if a.WorkingDir.HasDiverged(ctx.Log, repoDir) { + return "Default branch must be rebased onto pull request before running import.", nil + } + } + } + // Passed all import requirements configured. + return "", nil +} diff --git a/server/events/command_requirement_handler_test.go b/server/events/command_requirement_handler_test.go new file mode 100644 index 0000000000..d190cd657e --- /dev/null +++ b/server/events/command_requirement_handler_test.go @@ -0,0 +1,194 @@ +package events_test + +import ( + "fmt" + "testing" + + "github.com/runatlantis/atlantis/server/core/config/raw" + "github.com/runatlantis/atlantis/server/core/config/valid" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/logging/mocks/matchers" + + . "github.com/petergtz/pegomock" + + "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/mocks" + "github.com/stretchr/testify/assert" +) + +func TestAggregateApplyRequirements_ValidateApplyProject(t *testing.T) { + repoDir := "repoDir" + fullRequirements := []string{ + raw.ApprovedRequirement, + valid.PoliciesPassedCommandReq, + raw.MergeableRequirement, + raw.UnDivergedRequirement, + } + tests := []struct { + name string + ctx command.ProjectContext + setup func(workingDir *mocks.MockWorkingDir) + wantFailure string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "pass no requirements", + ctx: command.ProjectContext{}, + wantErr: assert.NoError, + }, + { + name: "pass full requirements", + ctx: command.ProjectContext{ + ApplyRequirements: fullRequirements, + PullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{IsApproved: true}, + Mergeable: true, + }, + ProjectPlanStatus: models.PassedPolicyCheckStatus, + }, + setup: func(workingDir *mocks.MockWorkingDir) { + When(workingDir.HasDiverged(matchers.AnyLoggingSimpleLogging(), AnyString())).ThenReturn(false) + }, + wantErr: assert.NoError, + }, + { + name: "fail by no approved", + ctx: command.ProjectContext{ + ApplyRequirements: []string{raw.ApprovedRequirement}, + PullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{IsApproved: false}, + }, + }, + wantFailure: "Pull request must be approved by at least one person other than the author before running apply.", + wantErr: assert.NoError, + }, + { + name: "fail by no policy passed", + ctx: command.ProjectContext{ + ApplyRequirements: []string{valid.PoliciesPassedCommandReq}, + ProjectPlanStatus: models.ErroredPolicyCheckStatus, + }, + wantFailure: "All policies must pass for project before running apply", + wantErr: assert.NoError, + }, + { + name: "fail by no mergeable", + ctx: command.ProjectContext{ + ApplyRequirements: []string{raw.MergeableRequirement}, + PullReqStatus: models.PullReqStatus{Mergeable: false}, + }, + wantFailure: "Pull request must be mergeable before running apply.", + wantErr: assert.NoError, + }, + { + name: "fail by diverged", + ctx: command.ProjectContext{ + ApplyRequirements: []string{raw.UnDivergedRequirement}, + }, + setup: func(workingDir *mocks.MockWorkingDir) { + When(workingDir.HasDiverged(matchers.AnyLoggingSimpleLogging(), AnyString())).ThenReturn(true) + }, + wantFailure: "Default branch must be rebased onto pull request before running apply.", + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterMockTestingT(t) + workingDir := mocks.NewMockWorkingDir() + a := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir} + if tt.setup != nil { + tt.setup(workingDir) + } + gotFailure, err := a.ValidateApplyProject(repoDir, tt.ctx) + if !tt.wantErr(t, err, fmt.Sprintf("ValidateApplyProject(%v, %v)", repoDir, tt.ctx)) { + return + } + assert.Equalf(t, tt.wantFailure, gotFailure, "ValidateApplyProject(%v, %v)", repoDir, tt.ctx) + }) + } +} + +func TestAggregateApplyRequirements_ValidateImportProject(t *testing.T) { + repoDir := "repoDir" + fullRequirements := []string{ + raw.ApprovedRequirement, + raw.MergeableRequirement, + raw.UnDivergedRequirement, + } + tests := []struct { + name string + ctx command.ProjectContext + setup func(workingDir *mocks.MockWorkingDir) + wantFailure string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "pass no requirements", + ctx: command.ProjectContext{}, + wantErr: assert.NoError, + }, + { + name: "pass full requirements", + ctx: command.ProjectContext{ + ImportRequirements: fullRequirements, + PullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{IsApproved: true}, + Mergeable: true, + }, + ProjectPlanStatus: models.PassedPolicyCheckStatus, + }, + setup: func(workingDir *mocks.MockWorkingDir) { + When(workingDir.HasDiverged(matchers.AnyLoggingSimpleLogging(), AnyString())).ThenReturn(false) + }, + wantErr: assert.NoError, + }, + { + name: "fail by no approved", + ctx: command.ProjectContext{ + ImportRequirements: []string{raw.ApprovedRequirement}, + PullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{IsApproved: false}, + }, + }, + wantFailure: "Pull request must be approved by at least one person other than the author before running import.", + wantErr: assert.NoError, + }, + { + name: "fail by no mergeable", + ctx: command.ProjectContext{ + ImportRequirements: []string{raw.MergeableRequirement}, + PullReqStatus: models.PullReqStatus{Mergeable: false}, + }, + wantFailure: "Pull request must be mergeable before running import.", + wantErr: assert.NoError, + }, + { + name: "fail by diverged", + ctx: command.ProjectContext{ + ImportRequirements: []string{raw.UnDivergedRequirement}, + }, + setup: func(workingDir *mocks.MockWorkingDir) { + When(workingDir.HasDiverged(matchers.AnyLoggingSimpleLogging(), AnyString())).ThenReturn(true) + }, + wantFailure: "Default branch must be rebased onto pull request before running import.", + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterMockTestingT(t) + workingDir := mocks.NewMockWorkingDir() + a := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir} + if tt.setup != nil { + tt.setup(workingDir) + } + gotFailure, err := a.ValidateImportProject(repoDir, tt.ctx) + if !tt.wantErr(t, err, fmt.Sprintf("ValidateImportProject(%v, %v)", repoDir, tt.ctx)) { + return + } + assert.Equalf(t, tt.wantFailure, gotFailure, "ValidateImportProject(%v, %v)", repoDir, tt.ctx) + }) + } +} diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index bc7ea38fc7..4282439f13 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -65,6 +65,7 @@ var applyLockChecker *lockingmocks.MockApplyLockChecker var lockingLocker *lockingmocks.MockLocker var applyCommandRunner *events.ApplyCommandRunner var unlockCommandRunner *events.UnlockCommandRunner +var importCommandRunner *events.ImportCommandRunner var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner var postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner @@ -181,12 +182,19 @@ func setup(t *testing.T) *vcsmocks.MockClient { SilenceNoProjects, ) + importCommandRunner = events.NewImportCommandRunner( + pullUpdater, + projectCommandBuilder, + projectCommandRunner, + ) + commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{ command.Plan: planCommandRunner, command.Apply: applyCommandRunner, command.ApprovePolicies: approvePoliciesCommandRunner, command.Unlock: unlockCommandRunner, command.Version: versionCommandRunner, + command.Import: importCommandRunner, } preWorkflowHooksCommandRunner = mocks.NewMockPreWorkflowHooksCommandRunner() diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index e4fe0e1ef7..d0a0961167 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -66,8 +66,6 @@ type CommentBuilder interface { BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string // BuildApplyComment builds an apply comment for the specified args. BuildApplyComment(repoRelDir string, workspace string, project string, autoMergeDisabled bool) string - // BuildVersionComment builds a version comment for the specified args. - BuildVersionComment(repoRelDir string, workspace string, project string) string } // CommentParser implements CommentParsing @@ -111,6 +109,7 @@ type CommentParseResult struct { // - atlantis unlock // - atlantis version // - atlantis approve_policies +// - atlantis import -- addr id func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) CommentParseResult { comment := strings.TrimSpace(rawComment) @@ -171,7 +170,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com } // Need to have a plan, apply, approve_policy or unlock at this point. - if !e.stringInSlice(cmd, []string{command.Plan.String(), command.Apply.String(), command.Unlock.String(), command.ApprovePolicies.String(), command.Version.String()}) { + if !e.stringInSlice(cmd, []string{command.Plan.String(), command.Apply.String(), command.Unlock.String(), command.ApprovePolicies.String(), command.Version.String(), command.Import.String()}) { return CommentParseResult{CommentResponse: fmt.Sprintf("```\nError: unknown command %q.\nRun 'atlantis --help' for usage.\n```", cmd)} } @@ -217,6 +216,14 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run version in relative to root of repo, ex. 'child/dir'.") flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", "Print the version for this project. Refers to the name of the project configured in a repo config file.") flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") + case command.Import.String(): + name = command.Import + flagSet = pflag.NewFlagSet(command.Import.String(), pflag.ContinueOnError) + flagSet.SetOutput(io.Discard) + flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") + flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan in relative to root of repo, ex. 'child/dir'.") + flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to run plan for. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", config.AtlantisYAMLFilename)) + flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", cmd)} } @@ -225,7 +232,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com // 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 CommentParseResult{CommentResponse: fmt.Sprintf("```\nUsage of %s:\n%s\n```", cmd, flagSet.FlagUsagesWrapped(usagesCols))} + return CommentParseResult{CommentResponse: fmt.Sprintf("```\nUsage of %s:\n%s\n```", name.DefaultUsage(), flagSet.FlagUsagesWrapped(usagesCols))} } if err != nil { if cmd == command.Unlock.String() { @@ -241,7 +248,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com unusedArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()] } if len(unusedArgs) > 0 { - return CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf("unknown argument(s) – %s", strings.Join(unusedArgs, " ")), cmd, flagSet)} + return CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf("unknown argument(s) – %s", strings.Join(unusedArgs, " ")), name.DefaultUsage(), flagSet)} } var extraArgs []string @@ -298,12 +305,15 @@ func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, p return fmt.Sprintf("%s %s%s", e.ExecutableName, command.Apply.String(), flags) } +<<<<<<< HEAD // BuildVersionComment builds a version comment for the specified args. func (e *CommentParser) BuildVersionComment(repoRelDir string, workspace string, project string) string { flags := e.buildFlags(repoRelDir, workspace, project, false) return fmt.Sprintf("%s %s%s", e.ExecutableName, command.Version.String(), flags) } +======= +>>>>>>> ec0bdf6c (feat: atlantis import) func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool) string { // Add quotes if dir has spaces. if strings.Contains(repoRelDir, " ") { @@ -411,6 +421,8 @@ Commands: approve_policies Approves all current policy checking failures for the PR. version Print the output of 'terraform version' + import Runs 'terraform import' for the changes in this pull request. + To plan a specific project, use the -d, -w and -p flags. help View help. Flags: diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 22d8ea3e38..afef43f5be 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -161,6 +161,11 @@ func TestParse_UnusedArguments(t *testing.T) { "arg arg2 --", "arg arg2", }, + { + command.Import, + "arg arg2 --", + "arg arg2", + }, } for _, c := range cases { comment := fmt.Sprintf("atlantis %s %s", c.Command.String(), c.Args) @@ -174,6 +179,8 @@ func TestParse_UnusedArguments(t *testing.T) { usage = ApplyUsage case command.ApprovePolicies: usage = ApprovePolicyUsage + case command.Import: + usage = ImportUsage } Equals(t, fmt.Sprintf("```\nError: unknown argument(s) – %s.\n%s```", c.Unused, usage), r.CommentResponse) }) @@ -225,17 +232,22 @@ func TestParse_InvalidCommand(t *testing.T) { func TestParse_SubcommandUsage(t *testing.T) { t.Log("given a comment asking for the usage of a subcommand should " + "return help") - comments := []string{ - "atlantis plan -h", - "atlantis plan --help", - "atlantis apply -h", - "atlantis apply --help", - "atlantis approve_policies -h", - "atlantis approve_policies --help", - } - for _, c := range comments { - r := commentParser.Parse(c, models.Github) - exp := "Usage of " + strings.Fields(c)[1] + tests := []struct { + input string + expUsage string + }{ + {"atlantis plan -h", "plan"}, + {"atlantis plan --help", "plan"}, + {"atlantis apply -h", "apply"}, + {"atlantis apply --help", "apply"}, + {"atlantis approve_policies -h", "approve_policies"}, + {"atlantis approve_policies --help", "approve_policies"}, + {"atlantis import -h", "import -- ADDR ID"}, + {"atlantis import --help", "import -- ADDR ID"}, + } + for _, c := range tests { + r := commentParser.Parse(c.input, models.Github) + exp := "Usage of " + c.expUsage Assert(t, strings.Contains(r.CommentResponse, exp), "For comment %q expected CommentResponse %q to contain %q", c, r.CommentResponse, exp) Assert(t, !strings.Contains(r.CommentResponse, "Error:"), @@ -266,6 +278,10 @@ func TestParse_InvalidFlags(t *testing.T) { "atlantis apply --abc", "Error: unknown flag: --abc", }, + { + "atlantis import --abc", + "Error: unknown flag: --abc", + }, } for _, c := range cases { r := commentParser.Parse(c.comment, models.Github) @@ -281,13 +297,17 @@ func TestParse_RelativeDirPath(t *testing.T) { comments := []string{ "atlantis plan -d ..", "atlantis apply -d ..", + "atlantis import -d ..", // These won't return an error because we prepend with . when parsing. //"atlantis plan -d /..", //"atlantis apply -d /..", + //"atlantis import -d /..", "atlantis plan -d ./..", "atlantis apply -d ./..", + "atlantis import -d ./..", "atlantis plan -d a/b/../../..", "atlantis apply -d a/../..", + "atlantis import -d a/../..", } for _, c := range comments { r := commentParser.Parse(c, models.Github) @@ -331,12 +351,16 @@ func TestParse_InvalidWorkspace(t *testing.T) { comments := []string{ "atlantis plan -w ..", "atlantis apply -w ..", + "atlantis import -w ..", "atlantis plan -w /", "atlantis apply -w /", + "atlantis import -w /", "atlantis plan -w ..abc", "atlantis apply -w abc..", + "atlantis import -w abc..", "atlantis plan -w abc..abc", "atlantis apply -w ../../../etc/passwd", + "atlantis import -w ../../../etc/passwd", } for _, c := range comments { r := commentParser.Parse(c, models.Github) @@ -582,7 +606,7 @@ func TestParse_Parsing(t *testing.T) { } for _, test := range cases { - for _, cmdName := range []string{"plan", "apply"} { + for _, cmdName := range []string{"plan", "apply", "import"} { comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags) t.Run(comment, func(t *testing.T) { r := commentParser.Parse(comment, models.Github) @@ -601,6 +625,9 @@ func TestParse_Parsing(t *testing.T) { if cmdName == "approve_policies" { Assert(t, r.Command.Name == command.ApprovePolicies, "did not parse comment %q as approve_policies command", comment) } + if cmdName == "import" { + Assert(t, r.Command.Name == command.Import, "did not parse comment %q as import command", comment) + } }) } } @@ -719,9 +746,6 @@ func TestBuildPlanApplyVersionComment(t *testing.T) { case command.Apply: actComment := commentParser.BuildApplyComment(c.repoRelDir, c.workspace, c.project, c.autoMergeDisabled) Equals(t, fmt.Sprintf("atlantis apply %s", c.expApplyFlags), actComment) - case command.Version: - actComment := commentParser.BuildVersionComment(c.repoRelDir, c.workspace, c.project) - Equals(t, fmt.Sprintf("atlantis version %s", c.expVersionFlags), actComment) } } }) @@ -762,6 +786,8 @@ Commands: approve_policies Approves all current policy checking failures for the PR. version Print the output of 'terraform version' + import Runs 'terraform import' for the changes in this pull request. + To plan a specific project, use the -d, -w and -p flags. help View help. Flags: @@ -791,6 +817,8 @@ Commands: approve_policies Approves all current policy checking failures for the PR. version Print the output of 'terraform version' + import Runs 'terraform import' for the changes in this pull request. + To plan a specific project, use the -d, -w and -p flags. help View help. Flags: @@ -874,6 +902,7 @@ var ApplyUsage = `Usage of apply: var ApprovePolicyUsage = `Usage of approve_policies: --verbose Append Atlantis log to comment. ` + var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + `atlantis unlock @@ -881,3 +910,13 @@ var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + Arguments or flags are not supported at the moment. If you need to unlock a specific project please use the atlantis UI.` + "\n```" + +var ImportUsage = `Usage of import -- ADDR ID: + -d, --dir string Which directory to run plan in relative to root of repo, + ex. 'child/dir'. + -p, --project string Which project to run plan for. Refers to the name of the + project configured in atlantis.yaml. Cannot be used at + same time as workspace or dir flags. + --verbose Append Atlantis log to comment. + -w, --workspace string Switch to this Terraform workspace before planning. +` diff --git a/server/events/import_command_runner.go b/server/events/import_command_runner.go new file mode 100644 index 0000000000..fc4d09fbfa --- /dev/null +++ b/server/events/import_command_runner.go @@ -0,0 +1,44 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/events/command" +) + +func NewImportCommandRunner( + pullUpdater *PullUpdater, + prjCmdBuilder ProjectImportCommandBuilder, + prjCmdRunner ProjectImportCommandRunner, +) *ImportCommandRunner { + return &ImportCommandRunner{ + pullUpdater: pullUpdater, + prjCmdBuilder: prjCmdBuilder, + prjCmdRunner: prjCmdRunner, + } +} + +type ImportCommandRunner struct { + pullUpdater *PullUpdater + prjCmdBuilder ProjectImportCommandBuilder + prjCmdRunner ProjectImportCommandRunner +} + +func (v *ImportCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) { + var err error + var projectCmds []command.ProjectContext + projectCmds, err = v.prjCmdBuilder.BuildImportCommands(ctx, cmd) + if err != nil { + ctx.Log.Warn("Error %s", err) + } + + var result command.Result + if len(projectCmds) > 1 { + // There is no usecase to kick terraform import into multiple projects. + // To avoid incorrect import, suppress to execute terraform import in multiple projects. + result = command.Result{ + Failure: "import cannot run on multiple projects. please specify one project.", + } + } else { + result = runProjectCmds(projectCmds, v.prjCmdRunner.Import) + } + v.pullUpdater.updatePull(ctx, cmd, result) +} diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index 621d88be0f..7708adf3ef 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -33,6 +33,7 @@ var ( policyCheckCommandTitle = command.PolicyCheck.TitleString() approvePoliciesCommandTitle = command.ApprovePolicies.TitleString() versionCommandTitle = command.Version.TitleString() + importCommandTitle = command.Import.TitleString() // maxUnwrappedLines is the maximum number of lines the Terraform output // can be before we wrap it in an expandable template. maxUnwrappedLines = 12 @@ -220,6 +221,12 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, resultData.Rendered = m.renderTemplate(templates.Lookup("versionUnwrappedSuccess"), struct{ Output string }{result.VersionSuccess}) } numVersionSuccesses++ + } else if result.ImportSuccess != nil { + if m.shouldUseWrappedTmpl(vcsHost, result.ImportSuccess.Output) { + resultData.Rendered = m.renderTemplate(templates.Lookup("importSuccessWrapped"), result.ImportSuccess) + } else { + resultData.Rendered = m.renderTemplate(templates.Lookup("importSuccessUnwrapped"), result.ImportSuccess) + } } else { resultData.Rendered = "Found no template. This is a bug!" } @@ -242,6 +249,8 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, tmpl = templates.Lookup("singleProjectVersionUnsuccessful") case len(resultsTmplData) == 1 && common.Command == applyCommandTitle: tmpl = templates.Lookup("singleProjectApply") + case len(resultsTmplData) == 1 && common.Command == importCommandTitle: + tmpl = templates.Lookup("singleProjectImport") case common.Command == planCommandTitle, common.Command == policyCheckCommandTitle: tmpl = templates.Lookup("multiProjectPlan") @@ -251,6 +260,8 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, tmpl = templates.Lookup("multiProjectApply") case common.Command == versionCommandTitle: tmpl = templates.Lookup("multiProjectVersion") + case common.Command == importCommandTitle: + tmpl = templates.Lookup("multiProjectImport") default: return "no template matched–this is a bug" } diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 4f390ff6e7..094ef1c56a 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -301,6 +301,33 @@ $$$ * $atlantis apply$ * :put_litter_in_its_place: To delete all plans and locks for the PR, comment: * $atlantis unlock$ +`, + }, + { + "single successful import", + command.Import, + []command.ProjectResult{ + { + ImportSuccess: &models.ImportSuccess{ + Output: "import-output", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + Workspace: "workspace", + RepoRelDir: "path", + ProjectName: "projectname", + }, + }, + models.Github, + `Ran Import for project: $projectname$ dir: $path$ workspace: $workspace$ + +$$$diff +import-output +$$$ + +* :repeat: To **plan** this project again, comment: + * $atlantis plan -d path -w workspace$ + + `, }, { diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index 8fdf24ef7c..21479f89ed 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -49,9 +49,6 @@ func (mock *MockWorkingDir) Clone(log logging.SimpleLogging, headRepo models.Rep } return ret0, ret1, ret2 } -func (mock *MockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) bool { - return false -} func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) { if mock == nil { @@ -72,6 +69,21 @@ func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, w return ret0, ret1 } +func (mock *MockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkingDir().") + } + params := []pegomock.Param{log, cloneDir} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasDiverged", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") @@ -232,6 +244,37 @@ func (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArgumen return } +func (verifier *VerifierMockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) *MockWorkingDir_HasDiverged_OngoingVerification { + params := []pegomock.Param{log, cloneDir} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasDiverged", params, verifier.timeout) + return &MockWorkingDir_HasDiverged_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkingDir_HasDiverged_OngoingVerification struct { + mock *MockWorkingDir + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) { + log, cloneDir := c.GetAllCapturedArguments() + return log[len(log)-1], cloneDir[len(cloneDir)-1] +} + +func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(string) + } + } + return +} + func (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification { params := []pegomock.Param{r, p} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params, verifier.timeout) diff --git a/server/events/mocks/matchers/command_projectresult.go b/server/events/mocks/matchers/command_projectresult.go new file mode 100644 index 0000000000..6d4c9b2e9d --- /dev/null +++ b/server/events/mocks/matchers/command_projectresult.go @@ -0,0 +1,33 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "github.com/petergtz/pegomock" + "reflect" + + command "github.com/runatlantis/atlantis/server/events/command" +) + +func AnyCommandProjectResult() command.ProjectResult { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(command.ProjectResult))(nil)).Elem())) + var nullValue command.ProjectResult + return nullValue +} + +func EqCommandProjectResult(value command.ProjectResult) command.ProjectResult { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue command.ProjectResult + return nullValue +} + +func NotEqCommandProjectResult(value command.ProjectResult) command.ProjectResult { + pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) + var nullValue command.ProjectResult + return nullValue +} + +func CommandProjectResultThat(matcher pegomock.ArgumentMatcher) command.ProjectResult { + pegomock.RegisterMatcher(matcher) + var nullValue command.ProjectResult + return nullValue +} diff --git a/server/events/mocks/matchers/slice_of_command_projectcontext.go b/server/events/mocks/matchers/slice_of_command_projectcontext.go new file mode 100644 index 0000000000..5f7e93135e --- /dev/null +++ b/server/events/mocks/matchers/slice_of_command_projectcontext.go @@ -0,0 +1,33 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "github.com/petergtz/pegomock" + "reflect" + + command "github.com/runatlantis/atlantis/server/events/command" +) + +func AnySliceOfCommandProjectContext() []command.ProjectContext { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]command.ProjectContext))(nil)).Elem())) + var nullValue []command.ProjectContext + return nullValue +} + +func EqSliceOfCommandProjectContext(value []command.ProjectContext) []command.ProjectContext { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue []command.ProjectContext + return nullValue +} + +func NotEqSliceOfCommandProjectContext(value []command.ProjectContext) []command.ProjectContext { + pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value}) + var nullValue []command.ProjectContext + return nullValue +} + +func SliceOfCommandProjectContextThat(matcher pegomock.ArgumentMatcher) []command.ProjectContext { + pegomock.RegisterMatcher(matcher) + var nullValue []command.ProjectContext + return nullValue +} diff --git a/server/events/mocks/mock_apply_handler.go b/server/events/mocks/mock_apply_handler.go deleted file mode 100644 index 3bb8b2b011..0000000000 --- a/server/events/mocks/mock_apply_handler.go +++ /dev/null @@ -1,114 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/runatlantis/atlantis/server/events (interfaces: ApplyRequirement) - -package mocks - -import ( - "reflect" - "time" - - pegomock "github.com/petergtz/pegomock" - "github.com/runatlantis/atlantis/server/events/command" -) - -type MockApplyRequirement struct { - fail func(message string, callerSkip ...int) -} - -func NewMockApplyRequirement(options ...pegomock.Option) *MockApplyRequirement { - mock := &MockApplyRequirement{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockApplyRequirement) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockApplyRequirement) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockApplyRequirement) ValidateProject(_param0 string, _param1 command.ProjectContext) (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockApplyRequirement().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidateProject", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockApplyRequirement) VerifyWasCalledOnce() *VerifierMockApplyRequirement { - return &VerifierMockApplyRequirement{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockApplyRequirement) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockApplyRequirement { - return &VerifierMockApplyRequirement{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockApplyRequirement) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockApplyRequirement { - return &VerifierMockApplyRequirement{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockApplyRequirement) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockApplyRequirement { - return &VerifierMockApplyRequirement{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockApplyRequirement struct { - mock *MockApplyRequirement - invocationCountMatcher pegomock.InvocationCountMatcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockApplyRequirement) ValidateProject(_param0 string, _param1 command.ProjectContext) *MockApplyRequirement_ValidateProject_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidateProject", params, verifier.timeout) - return &MockApplyRequirement_ValidateProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockApplyRequirement_ValidateProject_OngoingVerification struct { - mock *MockApplyRequirement - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockApplyRequirement_ValidateProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockApplyRequirement_ValidateProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]command.ProjectContext, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(command.ProjectContext) - } - } - return -} diff --git a/server/events/mocks/mock_command_requirement_handler.go b/server/events/mocks/mock_command_requirement_handler.go new file mode 100644 index 0000000000..e60b8cc5f3 --- /dev/null +++ b/server/events/mocks/mock_command_requirement_handler.go @@ -0,0 +1,163 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommandRequirementHandler) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + command "github.com/runatlantis/atlantis/server/events/command" + "reflect" + "time" +) + +type MockCommandRequirementHandler struct { + fail func(message string, callerSkip ...int) +} + +func NewMockCommandRequirementHandler(options ...pegomock.Option) *MockCommandRequirementHandler { + mock := &MockCommandRequirementHandler{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockCommandRequirementHandler) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockCommandRequirementHandler) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockCommandRequirementHandler) ValidateApplyProject(_param0 string, _param1 command.ProjectContext) (string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCommandRequirementHandler().") + } + params := []pegomock.Param{_param0, _param1} + result := pegomock.GetGenericMockFrom(mock).Invoke("ValidateApplyProject", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockCommandRequirementHandler) ValidateImportProject(_param0 string, _param1 command.ProjectContext) (string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockCommandRequirementHandler().") + } + params := []pegomock.Param{_param0, _param1} + result := pegomock.GetGenericMockFrom(mock).Invoke("ValidateImportProject", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockCommandRequirementHandler) VerifyWasCalledOnce() *VerifierMockCommandRequirementHandler { + return &VerifierMockCommandRequirementHandler{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockCommandRequirementHandler) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommandRequirementHandler { + return &VerifierMockCommandRequirementHandler{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockCommandRequirementHandler) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommandRequirementHandler { + return &VerifierMockCommandRequirementHandler{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockCommandRequirementHandler) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommandRequirementHandler { + return &VerifierMockCommandRequirementHandler{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockCommandRequirementHandler struct { + mock *MockCommandRequirementHandler + invocationCountMatcher pegomock.InvocationCountMatcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockCommandRequirementHandler) ValidateApplyProject(_param0 string, _param1 command.ProjectContext) *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification { + params := []pegomock.Param{_param0, _param1} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidateApplyProject", params, verifier.timeout) + return &MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification struct { + mock *MockCommandRequirementHandler + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) { + _param0, _param1 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1], _param1[len(_param1)-1] +} + +func (c *MockCommandRequirementHandler_ValidateApplyProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + _param1 = make([]command.ProjectContext, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(command.ProjectContext) + } + } + return +} + +func (verifier *VerifierMockCommandRequirementHandler) ValidateImportProject(_param0 string, _param1 command.ProjectContext) *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification { + params := []pegomock.Param{_param0, _param1} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidateImportProject", params, verifier.timeout) + return &MockCommandRequirementHandler_ValidateImportProject_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockCommandRequirementHandler_ValidateImportProject_OngoingVerification struct { + mock *MockCommandRequirementHandler + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification) GetCapturedArguments() (string, command.ProjectContext) { + _param0, _param1 := c.GetAllCapturedArguments() + return _param0[len(_param0)-1], _param1[len(_param1)-1] +} + +func (c *MockCommandRequirementHandler_ValidateImportProject_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []command.ProjectContext) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + _param1 = make([]command.ProjectContext, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(command.ProjectContext) + } + } + return +} diff --git a/server/events/mocks/mock_project_command_builder.go b/server/events/mocks/mock_project_command_builder.go index 1b3482359a..51ddc98295 100644 --- a/server/events/mocks/mock_project_command_builder.go +++ b/server/events/mocks/mock_project_command_builder.go @@ -122,6 +122,25 @@ func (mock *MockProjectCommandBuilder) BuildVersionCommands(ctx *command.Context return ret0, ret1 } +func (mock *MockProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *events.CommentCommand) ([]command.ProjectContext, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockProjectCommandBuilder().") + } + params := []pegomock.Param{ctx, comment} + result := pegomock.GetGenericMockFrom(mock).Invoke("BuildImportCommands", 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) VerifyWasCalledOnce() *VerifierMockProjectCommandBuilder { return &VerifierMockProjectCommandBuilder{ mock: mock, @@ -309,3 +328,34 @@ func (c *MockProjectCommandBuilder_BuildVersionCommands_OngoingVerification) Get } return } + +func (verifier *VerifierMockProjectCommandBuilder) BuildImportCommands(ctx *command.Context, comment *events.CommentCommand) *MockProjectCommandBuilder_BuildImportCommands_OngoingVerification { + params := []pegomock.Param{ctx, comment} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildImportCommands", params, verifier.timeout) + return &MockProjectCommandBuilder_BuildImportCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockProjectCommandBuilder_BuildImportCommands_OngoingVerification struct { + mock *MockProjectCommandBuilder + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockProjectCommandBuilder_BuildImportCommands_OngoingVerification) GetCapturedArguments() (*command.Context, *events.CommentCommand) { + ctx, comment := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], comment[len(comment)-1] +} + +func (c *MockProjectCommandBuilder_BuildImportCommands_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 +} diff --git a/server/events/mocks/mock_project_command_runner.go b/server/events/mocks/mock_project_command_runner.go index 9f27f89a3d..e4e8d1f637 100644 --- a/server/events/mocks/mock_project_command_runner.go +++ b/server/events/mocks/mock_project_command_runner.go @@ -101,6 +101,21 @@ func (mock *MockProjectCommandRunner) Version(ctx command.ProjectContext) comman return ret0 } +func (mock *MockProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectResult { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockProjectCommandRunner().") + } + params := []pegomock.Param{ctx} + result := pegomock.GetGenericMockFrom(mock).Invoke("Import", 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) VerifyWasCalledOnce() *VerifierMockProjectCommandRunner { return &VerifierMockProjectCommandRunner{ mock: mock, @@ -272,3 +287,30 @@ func (c *MockProjectCommandRunner_Version_OngoingVerification) GetAllCapturedArg } return } + +func (verifier *VerifierMockProjectCommandRunner) Import(ctx command.ProjectContext) *MockProjectCommandRunner_Import_OngoingVerification { + params := []pegomock.Param{ctx} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Import", params, verifier.timeout) + return &MockProjectCommandRunner_Import_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockProjectCommandRunner_Import_OngoingVerification struct { + mock *MockProjectCommandRunner + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockProjectCommandRunner_Import_OngoingVerification) GetCapturedArguments() command.ProjectContext { + ctx := c.GetAllCapturedArguments() + return ctx[len(ctx)-1] +} + +func (c *MockProjectCommandRunner_Import_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 +} diff --git a/server/events/mocks/mock_working_dir.go b/server/events/mocks/mock_working_dir.go index e5f976a011..bf4585fb2c 100644 --- a/server/events/mocks/mock_working_dir.go +++ b/server/events/mocks/mock_working_dir.go @@ -4,12 +4,11 @@ package mocks import ( - "reflect" - "time" - pegomock "github.com/petergtz/pegomock" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" + "reflect" + "time" ) type MockWorkingDir struct { @@ -49,9 +48,6 @@ func (mock *MockWorkingDir) Clone(log logging.SimpleLogging, headRepo models.Rep } return ret0, ret1, ret2 } -func (mock *MockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) bool { - return true -} func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error) { if mock == nil { @@ -72,6 +68,21 @@ func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, w return ret0, ret1 } +func (mock *MockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkingDir().") + } + params := []pegomock.Param{log, cloneDir} + result := pegomock.GetGenericMockFrom(mock).Invoke("HasDiverged", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") @@ -121,25 +132,6 @@ func (mock *MockWorkingDir) DeleteForWorkspace(r models.Repo, p models.PullReque return ret0 } -func (mock *MockWorkingDir) IsFileTracked(log logging.SimpleLogging, cloneDir string, filename string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockWorkingDir().") - } - params := []pegomock.Param{log, cloneDir, filename} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsFileTracked", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - func (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierMockWorkingDir { return &VerifierMockWorkingDir{ mock: mock, @@ -378,38 +370,3 @@ func (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedAr } return } - -func (verifier *VerifierMockWorkingDir) IsFileTracked(log logging.SimpleLogging, cloneDir string, filename string) *MockWorkingDir_IsFileTracked_OngoingVerification { - params := []pegomock.Param{log, cloneDir, filename} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsFileTracked", params, verifier.timeout) - return &MockWorkingDir_IsFileTracked_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockWorkingDir_IsFileTracked_OngoingVerification struct { - mock *MockWorkingDir - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockWorkingDir_IsFileTracked_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string, string) { - log, cloneDir, filename := c.GetAllCapturedArguments() - return log[len(log)-1], cloneDir[len(cloneDir)-1], filename[len(filename)-1] -} - -func (c *MockWorkingDir_IsFileTracked_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string, _param2 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(logging.SimpleLogging) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(string) - } - } - return -} diff --git a/server/events/models/models.go b/server/events/models/models.go index eb2840f96d..0ca9e8d941 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -411,6 +411,14 @@ type PolicyCheckSuccess struct { HasDiverged bool } +// ImportSuccess is the result of a successful import run. +type ImportSuccess struct { + // Output is the output from terraform import + 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. func (p *PolicyCheckSuccess) Summary() string { note := "" diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 6d58a79b96..006dc80ff9 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -153,6 +153,13 @@ type ProjectVersionCommandBuilder interface { BuildVersionCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) } +type ProjectImportCommandBuilder interface { + // BuildImportCommands builds project Import commands for this ctx and comment. If + // comment doesn't specify one project then there may be multiple commands + // to be run. + BuildImportCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) +} + //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder // ProjectCommandBuilder builds commands that run on individual projects. @@ -161,6 +168,7 @@ type ProjectCommandBuilder interface { ProjectApplyCommandBuilder ProjectApprovePoliciesCommandBuilder ProjectVersionCommandBuilder + ProjectImportCommandBuilder } // DefaultProjectCommandBuilder implements ProjectCommandBuilder. @@ -185,7 +193,7 @@ type DefaultProjectCommandBuilder struct { // See ProjectCommandBuilder.BuildAutoplanCommands. func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) { - projCtxs, err := p.buildPlanAllCommands(ctx, nil, false) + projCtxs, err := p.buildAllCommandsByCfg(ctx, command.Plan, nil, false) if err != nil { return nil, err } @@ -203,7 +211,7 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Contex // See ProjectCommandBuilder.BuildPlanCommands. func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { if !cmd.IsForSpecificProject() { - return p.buildPlanAllCommands(ctx, cmd.Flags, cmd.Verbose) + return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.Flags, cmd.Verbose) } pcc, err := p.buildProjectPlanCommand(ctx, cmd) return pcc, err @@ -212,27 +220,35 @@ func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, c // See ProjectCommandBuilder.BuildApplyCommands. func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { if !cmd.IsForSpecificProject() { - return p.buildAllProjectCommands(ctx, cmd) + return p.buildAllProjectCommandsByPlan(ctx, cmd) } pac, err := p.buildProjectApplyCommand(ctx, cmd) return pac, err } func (p *DefaultProjectCommandBuilder) BuildApprovePoliciesCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { - return p.buildAllProjectCommands(ctx, cmd) + return p.buildAllProjectCommandsByPlan(ctx, cmd) } func (p *DefaultProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { if !cmd.IsForSpecificProject() { - return p.buildAllProjectCommands(ctx, cmd) + return p.buildAllProjectCommandsByPlan(ctx, cmd) } pac, err := p.buildProjectVersionCommand(ctx, cmd) return pac, err } -// buildPlanAllCommands builds plan contexts for all projects we determine were +func (p *DefaultProjectCommandBuilder) BuildImportCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { + if !cmd.IsForSpecificProject() { + // import discard a plan file, so use buildAllCommandsByCfg instead buildAllProjectCommandsByPlan. + return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.Flags, cmd.Verbose) + } + return p.buildProjectImportCommand(ctx, cmd) +} + +// buildAllCommandsByCfg builds init contexts for all projects we determine were // modified in this ctx. -func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { +func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Context, cmdName command.Name, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { // We'll need the list of modified files. modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Pull.BaseRepo, ctx.Pull) if err != nil { @@ -322,7 +338,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context projCtxs = append(projCtxs, p.ProjectCommandContextBuilder.BuildProjectContext( ctx, - command.Plan, + cmdName, mergedCfg, commentFlags, repoDir, @@ -359,7 +375,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context projCtxs = append(projCtxs, p.ProjectCommandContextBuilder.BuildProjectContext( ctx, - command.Plan, + cmdName, pCfg, commentFlags, repoDir, @@ -528,9 +544,9 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *command.Context, projectName return } -// buildAllProjectCommands builds contexts for a command for every project that has +// buildAllProjectCommandsByPlan builds contexts for a command for every project that has // pending plans in this ctx. -func (p *DefaultProjectCommandBuilder) buildAllProjectCommands(ctx *command.Context, commentCmd *CommentCommand) ([]command.ProjectContext, error) { +func (p *DefaultProjectCommandBuilder) buildAllProjectCommandsByPlan(ctx *command.Context, commentCmd *CommentCommand) ([]command.ProjectContext, error) { // Lock all dirs in this pull request (instead of a single dir) because we // don't know how many dirs we'll need to run the command in. unlockFn, err := p.WorkingDirLocker.TryLockPull(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num) @@ -654,6 +670,47 @@ func (p *DefaultProjectCommandBuilder) buildProjectVersionCommand(ctx *command.C ) } +// 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 found–did 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.Flags, + repoDir, + repoRelDir, + workspace, + cmd.Verbose, + ) +} + // buildProjectCommandCtx builds a context for a single or several projects identified // by the parameters. func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Context, diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index 62239521d9..2b39242aaa 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -74,16 +74,17 @@ workflows: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -128,30 +129,32 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, }, // Set a global apply req that should be used. - "global apply_requirements": { + "global requirements": { globalCfg: ` repos: - id: /.*/ workflow: default apply_requirements: [approved, mergeable] + import_requirements: [approved, mergeable] workflows: default: plan: @@ -184,18 +187,19 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{"approved", "mergeable"}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{"approved", "mergeable"}, + ImportRequirements: []string{"approved", "mergeable"}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -210,6 +214,7 @@ repos: - id: github.com/owner/repo workflow: specific apply_requirements: [approved] + import_requirements: [approved] workflows: default: plan: @@ -248,18 +253,19 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{"approved"}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{"approved"}, + ImportRequirements: []string{"approved"}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{}, @@ -357,7 +363,8 @@ repos: - id: /.*/ workflow: default apply_requirements: [approved] - allowed_overrides: [apply_requirements, workflow] + import_requirements: [approved] + allowed_overrides: [apply_requirements, import_requirements, workflow] allow_custom_workflows: true workflows: default: @@ -377,6 +384,7 @@ projects: when_modified: [../modules/**/*.tf] terraform_version: v10.0 apply_requirements: [] + import_requirements: [] workflow: custom workflows: custom: @@ -399,18 +407,19 @@ workflows: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -459,18 +468,19 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -522,18 +532,19 @@ workflows: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{}, expApplySteps: []string{}, @@ -544,6 +555,7 @@ workflows: repos: - id: /.*/ apply_requirements: [approved] + import_requirements: [approved] - id: github.com/owner/repo workflow: custom workflows: @@ -569,17 +581,18 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{"approved"}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{"approved"}, + ImportRequirements: []string{"approved"}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -775,18 +788,19 @@ projects: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "myproject_1", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -p myproject_1 -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "myproject_1", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -p myproject_1 -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -945,16 +959,17 @@ repos: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPolicyCheckSteps: []string{"show", "policy_check"}, }, @@ -1004,18 +1019,19 @@ workflows: PullReqStatus: models.PullReqStatus{ Mergeable: true, }, - Pull: pull, - ProjectName: "", - ApplyRequirements: []string{}, - RepoConfigVersion: 3, - RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", - RepoRelDir: "project1", - TerraformVersion: mustVersion("10.0"), - User: models.User{}, - Verbose: true, - Workspace: "myworkspace", - PolicySets: emptyPolicySets, - RepoLocking: true, + Pull: pull, + ProjectName: "", + ApplyRequirements: []string{}, + ImportRequirements: []string{}, + RepoConfigVersion: 3, + RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag", + RepoRelDir: "project1", + TerraformVersion: mustVersion("10.0"), + User: models.User{}, + Verbose: true, + Workspace: "myworkspace", + PolicySets: emptyPolicySets, + RepoLocking: true, }, expPolicyCheckSteps: []string{"policy_check"}, }, diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index d18d2b294f..638b978c4c 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -110,6 +110,8 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( steps = []valid.Step{{ StepName: "version", }} + case command.Import: + steps = prjCfg.Workflow.Import.Steps } // If TerraformVersion not defined in config file look for a @@ -123,7 +125,6 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( cmdName, cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled), cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags), - cb.CommentBuilder.BuildVersionComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name), prjCfg, steps, prjCfg.PolicySets, @@ -183,7 +184,6 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( command.PolicyCheck, cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled), cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags), - cb.CommentBuilder.BuildVersionComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name), prjCfg, steps, prjCfg.PolicySets, @@ -206,7 +206,6 @@ func newProjectCommandContext(ctx *command.Context, cmd command.Name, applyCmd string, planCmd string, - versionCmd string, projCfg valid.MergedProjectCfg, steps []valid.Step, policySets valid.PolicySets, @@ -257,6 +256,7 @@ func newProjectCommandContext(ctx *command.Context, Pull: ctx.Pull, ProjectName: projCfg.Name, ApplyRequirements: projCfg.ApplyRequirements, + ImportRequirements: projCfg.ImportRequirements, RePlanCmd: planCmd, RepoRelDir: projCfg.RepoRelDir, RepoConfigVersion: projCfg.RepoCfgVersion, diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 246fdae701..24b36d8770 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -113,6 +113,11 @@ type ProjectVersionCommandRunner interface { Version(ctx command.ProjectContext) command.ProjectResult } +type ProjectImportCommandRunner interface { + // Import runs terraform import for the project described by ctx. + Import(ctx command.ProjectContext) command.ProjectResult +} + // ProjectCommandRunner runs project commands. A project command is a command // for a specific TF project. type ProjectCommandRunner interface { @@ -121,6 +126,7 @@ type ProjectCommandRunner interface { ProjectPolicyCheckCommandRunner ProjectApprovePoliciesCommandRunner ProjectVersionCommandRunner + ProjectImportCommandRunner } //go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_job_url_setter.go JobURLSetter @@ -185,22 +191,23 @@ func (p *ProjectOutputWrapper) updateProjectPRStatus(commandName command.Name, c // DefaultProjectCommandRunner implements ProjectCommandRunner. type DefaultProjectCommandRunner struct { - Locker ProjectLocker - LockURLGenerator LockURLGenerator - InitStepRunner StepRunner - PlanStepRunner StepRunner - ShowStepRunner StepRunner - ApplyStepRunner StepRunner - PolicyCheckStepRunner StepRunner - VersionStepRunner StepRunner - RunStepRunner CustomStepRunner - EnvStepRunner EnvStepRunner - MultiEnvStepRunner MultiEnvStepRunner - PullApprovedChecker runtime.PullApprovedChecker - WorkingDir WorkingDir - Webhooks WebhooksSender - WorkingDirLocker WorkingDirLocker - AggregateApplyRequirements ApplyRequirement + Locker ProjectLocker + LockURLGenerator LockURLGenerator + InitStepRunner StepRunner + PlanStepRunner StepRunner + ShowStepRunner StepRunner + ApplyStepRunner StepRunner + PolicyCheckStepRunner StepRunner + VersionStepRunner StepRunner + ImportStepRunner StepRunner + RunStepRunner CustomStepRunner + EnvStepRunner EnvStepRunner + MultiEnvStepRunner MultiEnvStepRunner + PullApprovedChecker runtime.PullApprovedChecker + WorkingDir WorkingDir + Webhooks WebhooksSender + WorkingDirLocker WorkingDirLocker + CommandRequirementHandler CommandRequirementHandler } // Plan runs terraform plan for the project described by ctx. @@ -271,6 +278,20 @@ func (p *DefaultProjectCommandRunner) Version(ctx command.ProjectContext) comman } } +// Import runs terraform import for the project described by ctx. +func (p *DefaultProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectResult { + importSuccess, failure, err := p.doImport(ctx) + return command.ProjectResult{ + Command: command.Import, + ImportSuccess: importSuccess, + Error: err, + Failure: failure, + RepoRelDir: ctx.RepoRelDir, + Workspace: ctx.Workspace, + ProjectName: ctx.ProjectName, + } +} + func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectContext) (*models.PolicyCheckSuccess, string, error) { // TODO: Make this a bit smarter @@ -414,7 +435,7 @@ func (p *DefaultProjectCommandRunner) doApply(ctx command.ProjectContext) (apply return "", "", DirNotExistErr{RepoRelDir: ctx.RepoRelDir} } - failure, err = p.AggregateApplyRequirements.ValidateProject(repoDir, ctx) + failure, err = p.CommandRequirementHandler.ValidateApplyProject(repoDir, ctx) if failure != "" || err != nil { return "", failure, err } @@ -472,6 +493,52 @@ func (p *DefaultProjectCommandRunner) doVersion(ctx command.ProjectContext) (ver return strings.Join(outputs, "\n"), "", nil } +func (p *DefaultProjectCommandRunner) doImport(ctx command.ProjectContext) (out *models.ImportSuccess, 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} + } + + failure, err = p.CommandRequirementHandler.ValidateImportProject(repoDir, ctx) + if failure != "" || err != nil { + return nil, failure, err + } + + // 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 import, re-plan command is required without import args + rePlanCmd := strings.TrimSpace(strings.Split(ctx.RePlanCmd, "--")[0]) + return &models.ImportSuccess{ + Output: strings.Join(outputs, "\n"), + RePlanCmd: rePlanCmd, + }, "", nil +} + func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.ProjectContext, absPath string) ([]string, error) { var outputs []string @@ -492,6 +559,8 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx command.P out, err = p.ApplyStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) case "version": out, err = p.VersionStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) + case "import": + out, err = p.ImportStepRunner.Run(ctx, step.ExtraArgs, absPath, envs) case "run": out, err = p.RunStepRunner.Run(ctx, step.RunCommand, absPath, envs, true) case "env": diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index 5b54005f88..260dd8f1cf 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -44,21 +44,21 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { realEnv := runtime.EnvStepRunner{} mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() - mockApplyReqHandler := mocks.NewMockApplyRequirement() + mockCommandRequirementHandler := mocks.NewMockCommandRequirementHandler() runner := events.DefaultProjectCommandRunner{ - Locker: mockLocker, - LockURLGenerator: mockURLGenerator{}, - InitStepRunner: mockInit, - PlanStepRunner: mockPlan, - ApplyStepRunner: mockApply, - RunStepRunner: mockRun, - EnvStepRunner: &realEnv, - PullApprovedChecker: nil, - WorkingDir: mockWorkingDir, - Webhooks: nil, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: mockApplyReqHandler, + Locker: mockLocker, + LockURLGenerator: mockURLGenerator{}, + InitStepRunner: mockInit, + PlanStepRunner: mockPlan, + ApplyStepRunner: mockApply, + RunStepRunner: mockRun, + EnvStepRunner: &realEnv, + PullApprovedChecker: nil, + WorkingDir: mockWorkingDir, + Webhooks: nil, + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + CommandRequirementHandler: mockCommandRequirementHandler, } repoDir := t.TempDir() @@ -261,7 +261,7 @@ func TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) { runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: &events.AggregateApplyRequirements{ + CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, } @@ -282,7 +282,7 @@ func TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) { runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: &events.AggregateApplyRequirements{ + CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, } @@ -306,15 +306,18 @@ func TestDefaultProjectCommandRunner_ApplyDiverged(t *testing.T) { runner := &events.DefaultProjectCommandRunner{ WorkingDir: mockWorkingDir, WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: &events.AggregateApplyRequirements{ + CommandRequirementHandler: &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, }, } + log := logging.NewNoopLogger(t) ctx := command.ProjectContext{ + Log: log, ApplyRequirements: []string{"undiverged"}, } tmp := t.TempDir() When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil) + When(mockWorkingDir.HasDiverged(log, tmp)).ThenReturn(true) res := runner.Apply(ctx) Equals(t, "Default branch must be rebased onto pull request before running apply.", res.Failure) @@ -410,22 +413,22 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) { mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() mockSender := mocks.NewMockWebhooksSender() - applyReqHandler := &events.AggregateApplyRequirements{ + applyReqHandler := &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, } runner := events.DefaultProjectCommandRunner{ - Locker: mockLocker, - LockURLGenerator: mockURLGenerator{}, - InitStepRunner: mockInit, - PlanStepRunner: mockPlan, - ApplyStepRunner: mockApply, - RunStepRunner: mockRun, - EnvStepRunner: mockEnv, - WorkingDir: mockWorkingDir, - Webhooks: mockSender, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: applyReqHandler, + Locker: mockLocker, + LockURLGenerator: mockURLGenerator{}, + InitStepRunner: mockInit, + PlanStepRunner: mockPlan, + ApplyStepRunner: mockApply, + RunStepRunner: mockRun, + EnvStepRunner: mockEnv, + WorkingDir: mockWorkingDir, + Webhooks: mockSender, + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + CommandRequirementHandler: applyReqHandler, } repoDir := t.TempDir() When(mockWorkingDir.GetWorkingDir( @@ -485,18 +488,18 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) { mockWorkingDir := mocks.NewMockWorkingDir() mockLocker := mocks.NewMockProjectLocker() mockSender := mocks.NewMockWebhooksSender() - applyReqHandler := &events.AggregateApplyRequirements{ + applyReqHandler := &events.DefaultCommandRequirementHandler{ WorkingDir: mockWorkingDir, } runner := events.DefaultProjectCommandRunner{ - Locker: mockLocker, - LockURLGenerator: mockURLGenerator{}, - ApplyStepRunner: mockApply, - WorkingDir: mockWorkingDir, - WorkingDirLocker: events.NewDefaultWorkingDirLocker(), - AggregateApplyRequirements: applyReqHandler, - Webhooks: mockSender, + Locker: mockLocker, + LockURLGenerator: mockURLGenerator{}, + ApplyStepRunner: mockApply, + WorkingDir: mockWorkingDir, + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + CommandRequirementHandler: applyReqHandler, + Webhooks: mockSender, } repoDir := t.TempDir() When(mockWorkingDir.GetWorkingDir( @@ -618,6 +621,122 @@ func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { Equals(t, "var=\n\nvar=value\n\ndynamic_var=dynamic_value\n\ndynamic_var=overridden\n", res.PlanSuccess.TerraformOutput) } +// Test that it runs the expected import steps. +func TestDefaultProjectCommandRunner_Import(t *testing.T) { + expEnvs := map[string]string{} + cases := []struct { + description string + steps []valid.Step + importReqs []string + pullReqStatus models.PullReqStatus + setup func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner) + + expSteps []string + expOut *models.ImportSuccess + expFailure string + }{ + { + description: "normal workflow", + steps: valid.DefaultImportStage.Steps, + importReqs: []string{"approved"}, + pullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{ + IsApproved: true, + }, + }, + setup: func(repoDir string, ctx command.ProjectContext, mockLocker *mocks.MockProjectLocker, mockInit *mocks.MockStepRunner, mockImport *mocks.MockStepRunner) { + When(mockLocker.TryLock( + matchers.AnyPtrToLoggingSimpleLogger(), + matchers.AnyModelsPullRequest(), + matchers.AnyModelsUser(), + AnyString(), + matchers.AnyModelsProject(), + AnyBool(), + )).ThenReturn(&events.TryLockResponse{ + LockAcquired: true, + LockKey: "lock-key", + }, nil) + + When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil) + When(mockImport.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("import", nil) + }, + expSteps: []string{"import"}, + expOut: &models.ImportSuccess{ + Output: "init\nimport", + RePlanCmd: "atlantis plan -d .", + }, + }, + { + description: "approval required", + steps: valid.DefaultImportStage.Steps, + importReqs: []string{"approved"}, + pullReqStatus: models.PullReqStatus{ + ApprovalStatus: models.ApprovalStatus{ + IsApproved: false, + }, + }, + expFailure: "Pull request must be approved by at least one person other than the author before running import.", + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + RegisterMockTestingT(t) + mockInit := mocks.NewMockStepRunner() + mockImport := mocks.NewMockStepRunner() + mockWorkingDir := mocks.NewMockWorkingDir() + mockLocker := mocks.NewMockProjectLocker() + mockSender := mocks.NewMockWebhooksSender() + applyReqHandler := &events.DefaultCommandRequirementHandler{ + WorkingDir: mockWorkingDir, + } + + runner := events.DefaultProjectCommandRunner{ + Locker: mockLocker, + LockURLGenerator: mockURLGenerator{}, + InitStepRunner: mockInit, + ImportStepRunner: mockImport, + WorkingDir: mockWorkingDir, + Webhooks: mockSender, + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), + CommandRequirementHandler: applyReqHandler, + } + ctx := command.ProjectContext{ + Log: logging.NewNoopLogger(t), + Steps: c.steps, + Workspace: "default", + ImportRequirements: c.importReqs, + RepoRelDir: ".", + PullReqStatus: c.pullReqStatus, + RePlanCmd: "atlantis plan -d . -- addr id", + } + repoDir := t.TempDir() + When(mockWorkingDir.Clone( + matchers.AnyPtrToLoggingSimpleLogger(), + matchers.AnyModelsRepo(), + matchers.AnyModelsPullRequest(), + AnyString(), + )).ThenReturn(repoDir, false, nil) + if c.setup != nil { + c.setup(repoDir, ctx, mockLocker, mockInit, mockImport) + } + + res := runner.Import(ctx) + Equals(t, c.expOut, res.ImportSuccess) + Equals(t, c.expFailure, res.Failure) + + for _, step := range c.expSteps { + switch step { + case "init": + mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) + case "import": + mockImport.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs) + } + } + }) + } +} + type mockURLGenerator struct{} func (m mockURLGenerator) GenerateLockURL(lockID string) string { diff --git a/server/events/templates/import_success_unwrapped.tmpl b/server/events/templates/import_success_unwrapped.tmpl new file mode 100644 index 0000000000..3c04fc7b60 --- /dev/null +++ b/server/events/templates/import_success_unwrapped.tmpl @@ -0,0 +1,8 @@ +{{ define "importSuccessUnwrapped" -}} +```diff +{{.Output}} +``` + +* :repeat: To **plan** this project again, comment: + * `{{.RePlanCmd}}` +{{ end }} diff --git a/server/events/templates/import_success_wrapped.tmpl b/server/events/templates/import_success_wrapped.tmpl new file mode 100644 index 0000000000..d4f421177e --- /dev/null +++ b/server/events/templates/import_success_wrapped.tmpl @@ -0,0 +1,9 @@ +{{ define "importSuccessWrapped" -}} +
Show Output +```diff +{{.Output}} +``` +
+* :repeat: To **plan** this project again, comment: + * `{{.RePlanCmd}}` +{{ end }} diff --git a/server/events/templates/multi_project_import.tmpl b/server/events/templates/multi_project_import.tmpl new file mode 100644 index 0000000000..81dfc6d5f1 --- /dev/null +++ b/server/events/templates/multi_project_import.tmpl @@ -0,0 +1,3 @@ +{{ define "multiProjectImport" -}} +{{ template "multiProjectApply" . }} +{{- end }} diff --git a/server/events/templates/single_project_import_success.tmpl b/server/events/templates/single_project_import_success.tmpl new file mode 100644 index 0000000000..86e4c9b030 --- /dev/null +++ b/server/events/templates/single_project_import_success.tmpl @@ -0,0 +1,6 @@ +{{ define "singleProjectImport" -}} +{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}` + +{{$result.Rendered}} +{{ template "log" . }} +{{ end }} diff --git a/server/scheduled/executor_service.go b/server/scheduled/executor_service.go index e4dab1c3e7..2d522d472e 100644 --- a/server/scheduled/executor_service.go +++ b/server/scheduled/executor_service.go @@ -2,13 +2,14 @@ package scheduled import ( "context" - "github.com/runatlantis/atlantis/server/logging" - "github.com/uber-go/tally" "os" "os/signal" "sync" "syscall" "time" + + "github.com/runatlantis/atlantis/server/logging" + "github.com/uber-go/tally" ) type ExecutorService struct { diff --git a/server/server.go b/server/server.go index 2e84a37ea6..e6c53e6f5a 100644 --- a/server/server.go +++ b/server/server.go @@ -565,16 +565,20 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { return nil, errors.Wrap(err, "initializing show step runner") } - policyCheckRunner, err := runtime.NewPolicyCheckStepRunner( + policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner( defaultTfVersion, policy.NewConfTestExecutorWorkflow(logger, binDir, &terraform.DefaultDownloader{}), ) if err != nil { - return nil, errors.Wrap(err, "initializing policy check runner") + return nil, errors.Wrap(err, "initializing policy check step runner") } - applyRequirementHandler := &events.AggregateApplyRequirements{ + if err != nil { + return nil, errors.Wrap(err, "initializing import step runner") + } + + applyRequirementHandler := &events.DefaultCommandRequirementHandler{ WorkingDir: workingDir, } @@ -592,7 +596,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { AsyncTFExec: terraformClient, }, ShowStepRunner: showStepRunner, - PolicyCheckStepRunner: policyCheckRunner, + PolicyCheckStepRunner: policyCheckStepRunner, ApplyStepRunner: &runtime.ApplyStepRunner{ TerraformExecutor: terraformClient, DefaultTFVersion: defaultTfVersion, @@ -610,10 +614,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { TerraformExecutor: terraformClient, DefaultTFVersion: defaultTfVersion, }, - WorkingDir: workingDir, - Webhooks: webhooksManager, - WorkingDirLocker: workingDirLocker, - AggregateApplyRequirements: applyRequirementHandler, + ImportStepRunner: &runtime.ImportStepRunner{ + TerraformExecutor: terraformClient, + DefaultTFVersion: defaultTfVersion, + }, + WorkingDir: workingDir, + Webhooks: webhooksManager, + WorkingDirLocker: workingDirLocker, + CommandRequirementHandler: applyRequirementHandler, } dbUpdater := &events.DBUpdater{ @@ -713,12 +721,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.SilenceNoProjects, ) + importCommandRunner := events.NewImportCommandRunner( + pullUpdater, + projectCommandBuilder, + instrumentedProjectCmdRunner, + ) + commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{ command.Plan: planCommandRunner, command.Apply: applyCommandRunner, command.ApprovePolicies: approvePoliciesCommandRunner, command.Unlock: unlockCommandRunner, command.Version: versionCommandRunner, + command.Import: importCommandRunner, } githubTeamAllowlistChecker, err := events.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist) From d0b2087dd357a4051add691903c22282ee1d9429 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Tue, 13 Dec 2022 08:41:36 +0900 Subject: [PATCH 02/35] feat: atlantis import --- .../docs/repo-level-atlantis-yaml.md | 44 ++++++---------- runatlantis.io/docs/using-atlantis.md | 50 +++++++++---------- server/core/config/valid/global_cfg.go | 4 -- server/events/comment_parser.go | 11 +--- server/events/comment_parser_test.go | 6 +-- 5 files changed, 43 insertions(+), 72 deletions(-) diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 03feaedbd1..94ceff5015 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -275,36 +275,20 @@ import_requirements: ["approved"] workflow: myworkflow ``` -<<<<<<< HEAD -| Key | Type | Default | Required | Description | -|----------------------------------------|-----------------------|-------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. | -| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. | -| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. | -| workspace | string | `"default"` | no | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | -| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. | -| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. | -| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | -| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | -| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | -| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. | -| workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | -======= -| Key | Type | Default | Required | Description | -|-----------------------------------------|-----------------------|-------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. | -| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. | -| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. | -| workspace | string | `"default"` | no | The [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | -| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. | -| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. | -| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | -| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | -| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | -| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | -| import_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | -| workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | ->>>>>>> ec0bdf6c (feat: atlantis import) +| Key | Type | Default | Required | Description | +|------------------------------------------|-----------------------|-------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. | +| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. | +| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. | +| workspace | string | `"default"` | no | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | +| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. | +| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. | +| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | +| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | +| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | +| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. | +| import_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | +| workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | ::: tip A project represents a Terraform state. Typically, there is one state per directory and workspace however it's possible to diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index ca93723b81..bc5f7fd406 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -125,30 +125,6 @@ They're ignored because they can't be specified for an already generated planfil If you would like to specify these flags, do it while running `atlantis plan`. --- -<<<<<<< HEAD -## atlantis unlock -```bash -atlantis unlock -``` - -### Explanation -Removes all atlantis locks and discards all plans for this PR. -To unlock a specific plan you can use the Atlantis UI. - ---- -## atlantis approve_policies -```bash -atlantis approve_policies -``` - -### Explanation -Approves all current policy checking failures for the PR. - -See also [policy checking](/docs/policy-checking.html). - -### Options -* `--verbose` Append Atlantis log to comment. -======= ## atlantis import ```bash @@ -156,6 +132,7 @@ atlantis import [options] -- [terraform import flags] addr id ``` ### Explanation Runs `terraform import` that matches the directory/project/workspace. +This command discards terraform plan result. Before apply, required `atlantis plan` again. ### Examples ```bash @@ -176,4 +153,27 @@ atlantis import -w staging -- addr id * `-d directory` Import a resource for this directory, relative to root of repo. Use `.` for root. * `-p project` Import a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.html). Cannot be used at same time as `-d` or `-w`. * `-w workspace` Import a resource for this [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html). If not using Terraform workspaces you can ignore this. ->>>>>>> ec0bdf6c (feat: atlantis import) + +--- +## atlantis unlock +```bash +atlantis unlock +``` + +### Explanation +Removes all atlantis locks and discards all plans for this PR. +To unlock a specific plan you can use the Atlantis UI. + +--- +## atlantis approve_policies +```bash +atlantis approve_policies +``` + +### Explanation +Approves all current policy checking failures for the PR. + +See also [policy checking](/docs/policy-checking.html). + +### Options +* `--verbose` Append Atlantis log to comment. diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index df5d1f7575..7ca62f7cb4 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -226,13 +226,9 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { { IDRegex: regexp.MustCompile(".*"), BranchRegex: regexp.MustCompile(".*"), -<<<<<<< HEAD RepoConfigFile: args.RepoConfigFile, - ApplyRequirements: applyReqs, -======= ApplyRequirements: commandReqs, ImportRequirements: commandReqs, ->>>>>>> ec0bdf6c (feat: atlantis import) PreWorkflowHooks: args.PreWorkflowHooks, Workflow: &defaultWorkflow, PostWorkflowHooks: args.PostWorkflowHooks, diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index d0a0961167..c382407979 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -222,7 +222,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com flagSet.SetOutput(io.Discard) flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan in relative to root of repo, ex. 'child/dir'.") - flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to run plan for. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", config.AtlantisYAMLFilename)) + 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.") default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", cmd)} @@ -305,15 +305,6 @@ func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, p return fmt.Sprintf("%s %s%s", e.ExecutableName, command.Apply.String(), flags) } -<<<<<<< HEAD -// BuildVersionComment builds a version comment for the specified args. -func (e *CommentParser) BuildVersionComment(repoRelDir string, workspace string, project string) string { - flags := e.buildFlags(repoRelDir, workspace, project, false) - return fmt.Sprintf("%s %s%s", e.ExecutableName, command.Version.String(), flags) -} - -======= ->>>>>>> ec0bdf6c (feat: atlantis import) func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string, autoMergeDisabled bool) string { // Add quotes if dir has spaces. if strings.Contains(repoRelDir, " ") { diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index afef43f5be..3ffb75b4c1 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -914,9 +914,9 @@ var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + var ImportUsage = `Usage of import -- ADDR ID: -d, --dir string Which directory to run plan in relative to root of repo, ex. 'child/dir'. - -p, --project string Which project to run plan for. Refers to the name of the - project configured in atlantis.yaml. Cannot be used at - same time as workspace or dir flags. + -p, --project string 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. --verbose Append Atlantis log to comment. -w, --workspace string Switch to this Terraform workspace before planning. ` From 1b63a91c844a3e7c63cc42450429499fe7b77825 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 13:53:42 +0900 Subject: [PATCH 03/35] regenerate mock comment builder --- server/events/mocks/mock_comment_building.go | 50 -------------------- 1 file changed, 50 deletions(-) diff --git a/server/events/mocks/mock_comment_building.go b/server/events/mocks/mock_comment_building.go index 59c3ecbbb8..91a04bccbf 100644 --- a/server/events/mocks/mock_comment_building.go +++ b/server/events/mocks/mock_comment_building.go @@ -55,21 +55,6 @@ func (mock *MockCommentBuilder) BuildApplyComment(repoRelDir string, workspace s return ret0 } -func (mock *MockCommentBuilder) BuildVersionComment(repoRelDir string, workspace string, project string) string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockCommentBuilder().") - } - params := []pegomock.Param{repoRelDir, workspace, project} - result := pegomock.GetGenericMockFrom(mock).Invoke("BuildVersionComment", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - func (mock *MockCommentBuilder) VerifyWasCalledOnce() *VerifierMockCommentBuilder { return &VerifierMockCommentBuilder{ mock: mock, @@ -184,38 +169,3 @@ func (c *MockCommentBuilder_BuildApplyComment_OngoingVerification) GetAllCapture } return } - -func (verifier *VerifierMockCommentBuilder) BuildVersionComment(repoRelDir string, workspace string, project string) *MockCommentBuilder_BuildVersionComment_OngoingVerification { - params := []pegomock.Param{repoRelDir, workspace, project} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildVersionComment", params, verifier.timeout) - return &MockCommentBuilder_BuildVersionComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockCommentBuilder_BuildVersionComment_OngoingVerification struct { - mock *MockCommentBuilder - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockCommentBuilder_BuildVersionComment_OngoingVerification) GetCapturedArguments() (string, string, string) { - repoRelDir, workspace, project := c.GetAllCapturedArguments() - return repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1] -} - -func (c *MockCommentBuilder_BuildVersionComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.(string) - } - } - return -} From ca1ba73be0e89ba18978b899fea209f42fe8c0a1 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 13:54:03 +0900 Subject: [PATCH 04/35] remove duplicate err check --- server/server.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/server.go b/server/server.go index e6c53e6f5a..b81781e108 100644 --- a/server/server.go +++ b/server/server.go @@ -574,10 +574,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { return nil, errors.Wrap(err, "initializing policy check step runner") } - if err != nil { - return nil, errors.Wrap(err, "initializing import step runner") - } - applyRequirementHandler := &events.DefaultCommandRequirementHandler{ WorkingDir: workingDir, } From 7272263f852bb18704105f489d2e265630b417ab Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 14:06:39 +0900 Subject: [PATCH 05/35] instrumented import command runner/builder --- .../instrumented_project_command_builder.go | 72 +++++++++---------- .../instrumented_project_command_runner.go | 5 ++ 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/server/events/instrumented_project_command_builder.go b/server/events/instrumented_project_command_builder.go index 8b9bade68c..b4cc7462c9 100644 --- a/server/events/instrumented_project_command_builder.go +++ b/server/events/instrumented_project_command_builder.go @@ -14,59 +14,59 @@ type InstrumentedProjectCommandBuilder struct { } func (b *InstrumentedProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) { - timer := b.scope.Timer(metrics.ExecutionTimeMetric).Start() - defer timer.Stop() - - executionSuccess := b.scope.Counter(metrics.ExecutionSuccessMetric) - executionError := b.scope.Counter(metrics.ExecutionErrorMetric) - - projectCmds, err := b.ProjectCommandBuilder.BuildApplyCommands(ctx, comment) - - if err != nil { - executionError.Inc(1) - b.Logger.Err("Error building apply commands: %s", err) - } else { - executionSuccess.Inc(1) - } - - return projectCmds, err - + return b.buildAndEmitStats( + "apply", + func() ([]command.ProjectContext, error) { + return b.ProjectCommandBuilder.BuildApplyCommands(ctx, comment) + }, + ) } -func (b *InstrumentedProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) { - timer := b.scope.Timer(metrics.ExecutionTimeMetric).Start() - defer timer.Stop() - executionSuccess := b.scope.Counter(metrics.ExecutionSuccessMetric) - executionError := b.scope.Counter(metrics.ExecutionErrorMetric) - - projectCmds, err := b.ProjectCommandBuilder.BuildAutoplanCommands(ctx) - - if err != nil { - executionError.Inc(1) - b.Logger.Err("Error building auto plan commands: %s", err) - } else { - executionSuccess.Inc(1) - } +func (b *InstrumentedProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) { + return b.buildAndEmitStats( + "auto plan", + func() ([]command.ProjectContext, error) { + return b.ProjectCommandBuilder.BuildAutoplanCommands(ctx) + }, + ) +} - return projectCmds, err +func (b *InstrumentedProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) { + return b.buildAndEmitStats( + "plan", + func() ([]command.ProjectContext, error) { + return b.ProjectCommandBuilder.BuildPlanCommands(ctx, comment) + }, + ) +} +func (b *InstrumentedProjectCommandBuilder) BuildVersionCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) { + return b.buildAndEmitStats( + "import", + func() ([]command.ProjectContext, error) { + return b.ProjectCommandBuilder.BuildImportCommands(ctx, comment) + }, + ) } -func (b *InstrumentedProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) { + +func (b *InstrumentedProjectCommandBuilder) buildAndEmitStats( + command string, + execute func() ([]command.ProjectContext, error), +) ([]command.ProjectContext, error) { timer := b.scope.Timer(metrics.ExecutionTimeMetric).Start() defer timer.Stop() executionSuccess := b.scope.Counter(metrics.ExecutionSuccessMetric) executionError := b.scope.Counter(metrics.ExecutionErrorMetric) - projectCmds, err := b.ProjectCommandBuilder.BuildPlanCommands(ctx, comment) + projectCmds, err := execute() if err != nil { executionError.Inc(1) - b.Logger.Err("Error building plan commands: %s", err) + b.Logger.Err("Error building %s commands: %s", command, err) } else { executionSuccess.Inc(1) } return projectCmds, err - } diff --git a/server/events/instrumented_project_command_runner.go b/server/events/instrumented_project_command_runner.go index 3fe41b4c9e..f7e2132306 100644 --- a/server/events/instrumented_project_command_runner.go +++ b/server/events/instrumented_project_command_runner.go @@ -11,6 +11,7 @@ type IntrumentedCommandRunner interface { PolicyCheck(ctx command.ProjectContext) command.ProjectResult Apply(ctx command.ProjectContext) command.ProjectResult ApprovePolicies(ctx command.ProjectContext) command.ProjectResult + Import(ctx command.ProjectContext) command.ProjectResult } type InstrumentedProjectCommandRunner struct { @@ -48,6 +49,10 @@ func (p *InstrumentedProjectCommandRunner) ApprovePolicies(ctx command.ProjectCo return RunAndEmitStats("approve policies", ctx, p.projectCommandRunner.Apply, p.scope) } +func (p *InstrumentedProjectCommandRunner) Import(ctx command.ProjectContext) command.ProjectResult { + return RunAndEmitStats("import", ctx, p.projectCommandRunner.Import, p.scope) +} + 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 scope = ctx.SetProjectScopeTags(scope) From 093308781bdadd2b613a26c05c07db417a1faee4 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 14:47:09 +0900 Subject: [PATCH 06/35] atlantis import subcommand accept args before hyphen --- runatlantis.io/docs/using-atlantis.md | 10 ++-- .../events/events_controller_e2e_test.go | 8 +-- server/events/command/name.go | 2 +- server/events/command/name_test.go | 2 +- server/events/comment_parser.go | 23 ++++--- server/events/comment_parser_test.go | 60 +++++++++++++------ 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index bc5f7fd406..e02e2ec5c6 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -128,7 +128,7 @@ If you would like to specify these flags, do it while running `atlantis plan`. ## atlantis import ```bash -atlantis import [options] -- [terraform import flags] addr id +atlantis import [options] ADDRESS ID -- [terraform import flags] ``` ### Explanation Runs `terraform import` that matches the directory/project/workspace. @@ -137,16 +137,16 @@ This command discards terraform plan result. Before apply, required `atlantis pl ### Examples ```bash # Runs import -atlantis import -- addr id +atlantis import ADDRESS ID # Runs import in the root directory of the repo with workspace `default`. -atlantis import -d . -- addr id +atlantis import -d . ADDRESS ID # Runs import in the `project1` directory of the repo with workspace `default` -atlantis import -d project1 -- addr id +atlantis import -d project1 ADDRESS ID # Runs import in the root directory of the repo with workspace `staging` -atlantis import -w staging -- addr id +atlantis import -w staging ADDRESS ID ``` ### Options diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 7cedc553e0..3fb6072516 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -410,9 +410,9 @@ func TestGitHubWorkflow(t *testing.T) { ModifiedFiles: []string{"main.tf"}, ExpAutoplan: true, Comments: []string{ - "atlantis import -- random_id.dummy1 AA", + "atlantis import random_id.dummy1 AA", "atlantis apply", - "atlantis import -- random_id.dummy2 BB", + "atlantis import random_id.dummy2 BB", "atlantis plan", }, ExpReplies: [][]string{ @@ -430,8 +430,8 @@ func TestGitHubWorkflow(t *testing.T) { ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"}, ExpAutoplan: true, Comments: []string{ - "atlantis import -- random_id.dummy1 AA", - "atlantis import -d dir1 -- random_id.dummy1 AA", + "atlantis import random_id.dummy1 AA", + "atlantis import -d dir1 random_id.dummy1 AA", "atlantis plan", }, ExpReplies: [][]string{ diff --git a/server/events/command/name.go b/server/events/command/name.go index 1396b41b9a..fcdcc5cd40 100644 --- a/server/events/command/name.go +++ b/server/events/command/name.go @@ -61,7 +61,7 @@ func (c Name) String() string { func (c Name) DefaultUsage() string { switch c { case Import: - return "import -- ADDR ID" + return "import ADDRESS ID" default: return c.String() } diff --git a/server/events/command/name_test.go b/server/events/command/name_test.go index 461d228856..220810e395 100644 --- a/server/events/command/name_test.go +++ b/server/events/command/name_test.go @@ -56,7 +56,7 @@ func TestName_DefaultUsage(t *testing.T) { {command.PolicyCheck, "policy_check"}, {command.ApprovePolicies, "approve_policies"}, {command.Version, "version"}, - {command.Import, "import -- ADDR ID"}, + {command.Import, "import ADDRESS ID"}, } for _, tt := range tests { t.Run(tt.c.String(), func(t *testing.T) { diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index c382407979..78eb6dec91 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -109,7 +109,7 @@ type CommentParseResult struct { // - atlantis unlock // - atlantis version // - atlantis approve_policies -// - atlantis import -- addr id +// - atlantis import ADDRESS ID func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) CommentParseResult { comment := strings.TrimSpace(rawComment) @@ -180,6 +180,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com var verbose, autoMergeDisabled bool var flagSet *pflag.FlagSet var name command.Name + var requiredCommandArgCount int // Set up the flag parsing depending on the command. switch cmd { @@ -224,6 +225,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan 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.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") + requiredCommandArgCount = 2 // import requires `atlantis import ` args default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", cmd)} } @@ -241,19 +243,24 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com return CommentParseResult{CommentResponse: e.errMarkdown(err.Error(), cmd, flagSet)} } - var unusedArgs []string + var commandArgs []string // commandArgs are the arguments that are passed before `--` without any parameter flags. if flagSet.ArgsLenAtDash() == -1 { - unusedArgs = flagSet.Args() + commandArgs = flagSet.Args() } else { - unusedArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()] + commandArgs = flagSet.Args()[0:flagSet.ArgsLenAtDash()] } - if len(unusedArgs) > 0 { - return CommentParseResult{CommentResponse: e.errMarkdown(fmt.Sprintf("unknown argument(s) – %s", strings.Join(unusedArgs, " ")), name.DefaultUsage(), flagSet)} + 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 + var extraArgs []string // command extra_args + // pass commandArgs into extraArgs. This is because after comment_parser, we will use extra_args only. + extraArgs = append(extraArgs, commandArgs...) if flagSet.ArgsLenAtDash() != -1 { - extraArgs = flagSet.Args()[flagSet.ArgsLenAtDash():] + extraArgs = append(extraArgs, flagSet.Args()[flagSet.ArgsLenAtDash():]...) } dir, err = e.validateDir(dir) diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 3ffb75b4c1..e850fe85da 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -107,64 +107,82 @@ func TestParse_HelpResponseWithApplyDisabled(t *testing.T) { func TestParse_UnusedArguments(t *testing.T) { t.Log("if there are unused flags we return an error") cases := []struct { - Command command.Name - Args string - Unused string + Command command.Name + Args string + Unused string + ExpMessage string }{ { command.Plan, "-d . arg", "arg", + "unknown argument(s)", }, { command.Plan, "arg -d .", "arg", + "unknown argument(s)", }, { command.Plan, "arg", "arg", + "unknown argument(s)", }, { command.Plan, "arg arg2", "arg arg2", + "unknown argument(s)", }, { command.Plan, "-d . arg -w kjj arg2", "arg arg2", + "unknown argument(s)", }, { command.Apply, "-d . arg", "arg", + "unknown argument(s)", }, { command.Apply, "arg arg2", "arg arg2", + "unknown argument(s)", }, { command.Apply, "arg arg2 -- useful", "arg arg2", + "unknown argument(s)", }, { command.Apply, "arg arg2 --", "arg arg2", + "unknown argument(s)", }, { command.ApprovePolicies, "arg arg2 --", "arg arg2", + "unknown argument(s)", }, { command.Import, - "arg arg2 --", - "arg arg2", + "arg --", + "arg", + "invalid argument count", + }, + { + command.Import, + "arg1 arg2 arg3 --", + "arg1 arg2 arg3", + "invalid argument count", }, } for _, c := range cases { @@ -182,7 +200,7 @@ func TestParse_UnusedArguments(t *testing.T) { case command.Import: usage = ImportUsage } - Equals(t, fmt.Sprintf("```\nError: unknown argument(s) – %s.\n%s```", c.Unused, usage), r.CommentResponse) + Equals(t, fmt.Sprintf("```\nError: %s – %s.\n%s```", c.ExpMessage, c.Unused, usage), r.CommentResponse) }) } } @@ -242,8 +260,8 @@ func TestParse_SubcommandUsage(t *testing.T) { {"atlantis apply --help", "apply"}, {"atlantis approve_policies -h", "approve_policies"}, {"atlantis approve_policies --help", "approve_policies"}, - {"atlantis import -h", "import -- ADDR ID"}, - {"atlantis import --help", "import -- ADDR ID"}, + {"atlantis import -h", "import ADDRESS ID"}, + {"atlantis import --help", "import ADDRESS ID"}, } for _, c := range tests { r := commentParser.Parse(c.input, models.Github) @@ -297,17 +315,17 @@ func TestParse_RelativeDirPath(t *testing.T) { comments := []string{ "atlantis plan -d ..", "atlantis apply -d ..", - "atlantis import -d ..", + "atlantis import -d .. address id", // These won't return an error because we prepend with . when parsing. //"atlantis plan -d /..", //"atlantis apply -d /..", - //"atlantis import -d /..", + //"atlantis import -d /.. address id", "atlantis plan -d ./..", "atlantis apply -d ./..", - "atlantis import -d ./..", + "atlantis import -d ./.. address id", "atlantis plan -d a/b/../../..", "atlantis apply -d a/../..", - "atlantis import -d a/../..", + "atlantis import -d a/../.. address id", } for _, c := range comments { r := commentParser.Parse(c, models.Github) @@ -351,16 +369,16 @@ func TestParse_InvalidWorkspace(t *testing.T) { comments := []string{ "atlantis plan -w ..", "atlantis apply -w ..", - "atlantis import -w ..", + "atlantis import -w .. address id", "atlantis plan -w /", "atlantis apply -w /", - "atlantis import -w /", + "atlantis import -w / address id", "atlantis plan -w ..abc", "atlantis apply -w abc..", - "atlantis import -w abc..", + "atlantis import -w abc.. address id", "atlantis plan -w abc..abc", "atlantis apply -w ../../../etc/passwd", - "atlantis import -w ../../../etc/passwd", + "atlantis import -w ../../../etc/passwd address id", } for _, c := range comments { r := commentParser.Parse(c, models.Github) @@ -606,7 +624,7 @@ func TestParse_Parsing(t *testing.T) { } for _, test := range cases { - for _, cmdName := range []string{"plan", "apply", "import"} { + for _, cmdName := range []string{"plan", "apply", "import address id"} { comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags) t.Run(comment, func(t *testing.T) { r := commentParser.Parse(comment, models.Github) @@ -615,18 +633,22 @@ func TestParse_Parsing(t *testing.T) { Assert(t, test.expWorkspace == r.Command.Workspace, "exp workspace to equal %q but was %q for comment %q", test.expWorkspace, r.Command.Workspace, comment) Assert(t, test.expVerbose == r.Command.Verbose, "exp verbose to equal %v but was %v for comment %q", test.expVerbose, r.Command.Verbose, comment) actExtraArgs := strings.Join(r.Command.Flags, " ") - Assert(t, test.expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", test.expExtraArgs, actExtraArgs, comment) if cmdName == "plan" { Assert(t, r.Command.Name == command.Plan, "did not parse comment %q as plan command", comment) + Assert(t, test.expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", test.expExtraArgs, actExtraArgs, comment) } if cmdName == "apply" { Assert(t, r.Command.Name == command.Apply, "did not parse comment %q as apply command", comment) + Assert(t, test.expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", test.expExtraArgs, actExtraArgs, comment) } if cmdName == "approve_policies" { Assert(t, r.Command.Name == command.ApprovePolicies, "did not parse comment %q as approve_policies command", comment) + Assert(t, test.expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", test.expExtraArgs, actExtraArgs, comment) } if cmdName == "import" { + expExtraArgs := fmt.Sprintf("%s address id", test.expExtraArgs) 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", test.expExtraArgs, actExtraArgs, comment) } }) } @@ -911,7 +933,7 @@ var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + If you need to unlock a specific project please use the atlantis UI.` + "\n```" -var ImportUsage = `Usage of import -- ADDR ID: +var ImportUsage = `Usage of import ADDRESS ID: -d, --dir string Which directory to run plan in relative to root of repo, ex. 'child/dir'. -p, --project string Which project to run import for. Refers to the name of From 1de643394a46456d8d9449afe7819903302607be Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Wed, 21 Dec 2022 14:54:40 +0900 Subject: [PATCH 07/35] fix link checker --- runatlantis.io/docs/policy-checking.md | 2 +- runatlantis.io/docs/repo-level-atlantis-yaml.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runatlantis.io/docs/policy-checking.md b/runatlantis.io/docs/policy-checking.md index 3be38a1ad0..9bea65c507 100644 --- a/runatlantis.io/docs/policy-checking.md +++ b/runatlantis.io/docs/policy-checking.md @@ -10,7 +10,7 @@ for using this step include: ## How it works? -Enabling "policy checking" in addition to the [mergeable apply requirement](https://www.runatlantis.io/docs/apply-requirements.html#supported-requirements) blocks applies on plans that fail any of the defined conftest policies. +Enabling "policy checking" in addition to the [mergeable apply requirement](/docs/command-requirements.html#supported-requirements) blocks applies on plans that fail any of the defined conftest policies. ![Policy Check Apply Failure](./images/policy-check-apply-failure.png) diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 94ceff5015..4bb3925d20 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -286,7 +286,7 @@ workflow: myworkflow | repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | | autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | | terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | -| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. | +| apply_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | | import_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | | workflow
*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. | From aabc2609246a1338b42dee8662c6f70d26d78cea Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Dec 2022 03:10:46 +0900 Subject: [PATCH 08/35] docs: review feedback --- runatlantis.io/docs/apply-requirements.md | 5 +++++ runatlantis.io/docs/command-requirements.md | 4 ++-- runatlantis.io/docs/custom-workflows.md | 9 +++++++++ runatlantis.io/docs/server-side-repo-config.md | 2 +- runatlantis.io/docs/using-atlantis.md | 12 ++++++------ 5 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 runatlantis.io/docs/apply-requirements.md diff --git a/runatlantis.io/docs/apply-requirements.md b/runatlantis.io/docs/apply-requirements.md new file mode 100644 index 0000000000..870ac4972e --- /dev/null +++ b/runatlantis.io/docs/apply-requirements.md @@ -0,0 +1,5 @@ +# Apply Requirements + +:::warning REDIRECT +This page is moved to [Command Requirements](/docs/command-requirements.html). +::: diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 1a3e6309a4..9e9a96c38a 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -160,12 +160,12 @@ You can set the `undiverged` requirement by: apply_requirements: [undiverged] import_requirements: [undiverged] ``` -1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config: +1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` and `import_requirements` keys in your `repos.yaml` config: #### repos.yaml ```yaml repos: - id: /.*/ - allowed_overrides: [apply_requirements, apply_requirements] + allowed_overrides: [apply_requirements, import_requirements] ``` #### atlantis.yaml diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md index 25d6d4a5e4..b8dd165630 100644 --- a/runatlantis.io/docs/custom-workflows.md +++ b/runatlantis.io/docs/custom-workflows.md @@ -47,6 +47,15 @@ workflows: - init - plan: extra_args: ["-var-file", "production.tfvars"] + apply: + steps: + - apply: + extra_args: ["-var-file", "production.tfvars"] + import: + steps: + - init + - import: + extra_args: ["-var-file", "production.tfvars"] ``` Then in your repo-level `atlantis.yaml` file, you would reference the workflows: ```yaml diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index c6d17d561a..07586a25cf 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -100,7 +100,7 @@ workflows: ## Use Cases Here are some of the reasons you might want to use a repo config. -### Requiring PR Is Approved Before Apply or Import +### Requiring PR Is Approved Before an applicable subcommand If you want to require that all (or specific) repos must have pull requests approved before Atlantis will allow running `apply` or `import`, use the `apply_requirements` or `import_requirements` keys. diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index e02e2ec5c6..1fb0f51468 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -63,7 +63,7 @@ atlantis plan -w staging * `-d directory` Which directory to run plan in relative to root of repo. Use `.` for root. * Ex. `atlantis plan -d child/dir` * `-p project` Which project to run plan for. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.html). Cannot be used at same time as `-d` or `-w` because the project defines this already. -* `-w workspace` Switch to this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) before planning. Defaults to `default`. If not using Terraform workspaces you can ignore this. +* `-w workspace` Switch to this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) before planning. Defaults to `default`. Ignore this if Terraform workspaces are unused. * `--verbose` Append Atlantis log to comment. ::: warning NOTE @@ -110,7 +110,7 @@ atlantis apply -w staging ### Options * `-d directory` Apply the plan for this directory, relative to root of repo. Use `.` for root. * `-p project` Apply the plan for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.html). Cannot be used at same time as `-d` or `-w`. -* `-w workspace` Apply the plan for this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). If not using Terraform workspaces you can ignore this. +* `-w workspace` Apply the plan for this [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused. * `--auto-merge-disabled` Disable [automerge](automerging.html) for this apply command. * `--verbose` Append Atlantis log to comment. @@ -132,14 +132,14 @@ atlantis import [options] ADDRESS ID -- [terraform import flags] ``` ### Explanation Runs `terraform import` that matches the directory/project/workspace. -This command discards terraform plan result. Before apply, required `atlantis plan` again. +This command discards the terraform plan result. After an import and before an apply, another `atlantis plan` must be run again. ### Examples ```bash # Runs import atlantis import ADDRESS ID -# Runs import in the root directory of the repo with workspace `default`. +# Runs import in the root directory of the repo with workspace `default` atlantis import -d . ADDRESS ID # Runs import in the `project1` directory of the repo with workspace `default` @@ -151,8 +151,8 @@ atlantis import -w staging ADDRESS ID ### Options * `-d directory` Import a resource for this directory, relative to root of repo. Use `.` for root. -* `-p project` Import a resource for this project. Refers to the name of the project configured in the repo's [`atlantis.yaml` file](repo-level-atlantis-yaml.html). Cannot be used at same time as `-d` or `-w`. -* `-w workspace` Import a resource for this [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html). If not using Terraform workspaces you can ignore this. +* `-p project` Import 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` Import a resource for a specific [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces). Ignore this if Terraform workspaces are unused. --- ## atlantis unlock From 7d0582fbbe5f6b3a6eddc5944d29bb5dc4179dd8 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Dec 2022 05:30:17 +0900 Subject: [PATCH 09/35] fix atlantis import options order --- runatlantis.io/docs/using-atlantis.md | 17 +++++- .../events/events_controller_e2e_test.go | 23 +++++++- .../exp-output-autoplan.txt | 52 +++++++++++++++++++ .../exp-output-import-count.txt | 20 +++++++ .../exp-output-import-foreach.txt | 20 +++++++ .../exp-output-merge.txt | 3 ++ .../exp-output-plan-again.txt | 22 ++++++++ .../import-single-project-var/main.tf | 15 ++++++ server/events/comment_parser.go | 10 ++-- server/events/comment_parser_test.go | 11 ++-- 10 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-autoplan.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-count.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-foreach.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-merge.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-plan-again.txt create mode 100644 server/controllers/events/testfixtures/test-repos/import-single-project-var/main.tf diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index 1fb0f51468..11c3a1816c 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -72,7 +72,7 @@ A `atlantis plan` (without flags), like autoplans, discards all plans previously ### Additional Terraform flags -If you need to run `terraform plan` with additional arguments, like `-target=resource` or `-var 'foo-bar'` or `-var-file myfile.tfvars` +If you need to run `terraform plan` with additional arguments, like `-target=resource` or `-var 'foo=bar'` or `-var-file myfile.tfvars` you can append them to the end of the comment after `--`, ex. ``` atlantis plan -d dir -- -var foo='bar' @@ -126,7 +126,6 @@ If you would like to specify these flags, do it while running `atlantis plan`. --- ## atlantis import - ```bash atlantis import [options] ADDRESS ID -- [terraform import flags] ``` @@ -149,11 +148,25 @@ atlantis import -d project1 ADDRESS ID atlantis import -w staging ADDRESS ID ``` +::: tip +* If import for_each resources, it requires a single quoted address. + * ex. `atlantis import 'aws_instance.example["foo"]' i-1234567890abcdef0` +::: + ### Options * `-d directory` Import a resource for this directory, relative to root of repo. Use `.` for root. * `-p project` Import 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` Import 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 you need to run `terraform import` with additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars` +you can append them to the end of the comment after `--`, ex. +``` +atlantis imoport -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' +``` +If you always need to append a certain flag, see [Custom Workflow Use Cases](custom-workflows.html#adding-extra-arguments-to-terraform-commands). + --- ## atlantis unlock ```bash diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 3fb6072516..cd78c8438c 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -2,6 +2,7 @@ package events_test import ( "bytes" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -424,6 +425,24 @@ func TestGitHubWorkflow(t *testing.T) { {"exp-output-merge.txt"}, }, }, + { + Description: "import single project with -var", + RepoDir: "import-single-project-var", + ModifiedFiles: []string{"main.tf"}, + ExpAutoplan: true, + Comments: []string{ + "atlantis import 'random_id.for_each[\"overridden\"]' AA -- -var var=overridden", + "atlantis import random_id.count[0] BB", + "atlantis plan -- -var var=overridden", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-import-foreach.txt"}, + {"exp-output-import-count.txt"}, + {"exp-output-plan-again.txt"}, + {"exp-output-merge.txt"}, + }, + }, { Description: "import multiple project", RepoDir: "import-multiple-project", @@ -1266,7 +1285,9 @@ func (w *mockWebhookSender) Send(log logging.SimpleLogging, result webhooks.Appl func GitHubCommentEvent(t *testing.T, comment string) *http.Request { requestJSON, err := os.ReadFile(filepath.Join("testfixtures", "githubIssueCommentEvent.json")) Ok(t, err) - requestJSON = []byte(strings.Replace(string(requestJSON), "###comment body###", comment, 1)) + escapedComment, err := json.Marshal(comment) + Ok(t, err) + requestJSON = []byte(strings.Replace(string(requestJSON), "\"###comment body###\"", string(escapedComment), 1)) req, err := http.NewRequest("POST", "/events", bytes.NewBuffer(requestJSON)) Ok(t, err) req.Header.Set("Content-Type", "application/json") diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-autoplan.txt b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-autoplan.txt new file mode 100644 index 0000000000..4abf8f79fe --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-autoplan.txt @@ -0,0 +1,52 @@ +Ran Plan for dir: `.` workspace: `default` + +
Show Output + +```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 = {} + } + +Plan: 2 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 .` +
+Plan: 2 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` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-count.txt b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-count.txt new file mode 100644 index 0000000000..18fbf47c8e --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-count.txt @@ -0,0 +1,20 @@ +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. + + +``` + +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` + + diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-foreach.txt b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-foreach.txt new file mode 100644 index 0000000000..9e4cf41229 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-import-foreach.txt @@ -0,0 +1,20 @@ +Ran Import for dir: `.` workspace: `default` + +```diff +random_id.for_each["overridden"]: Importing from ID "AA"... +random_id.for_each["overridden"]: Import prepared! + Prepared random_id for import +random_id.for_each["overridden"]: 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. + + +``` + +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` + + diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-merge.txt b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-merge.txt new file mode 100644 index 0000000000..872c5ee40c --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-merge.txt @@ -0,0 +1,3 @@ +Locks and plans deleted for the projects and workspaces modified in this pull request: + +- dir: `.` workspace: `default` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-plan-again.txt b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-plan-again.txt new file mode 100644 index 0000000000..c5c48dadff --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/exp-output-plan-again.txt @@ -0,0 +1,22 @@ +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` diff --git a/server/controllers/events/testfixtures/test-repos/import-single-project-var/main.tf b/server/controllers/events/testfixtures/test-repos/import-single-project-var/main.tf new file mode 100644 index 0000000000..f7bf7839d0 --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/import-single-project-var/main.tf @@ -0,0 +1,15 @@ +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" +} diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 78eb6dec91..caeb527871 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -225,7 +225,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan 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.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") - requiredCommandArgCount = 2 // import requires `atlantis import ` args + requiredCommandArgCount = 2 // import requires `atlantis import ADDRESS ID` args default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", cmd)} } @@ -257,11 +257,15 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com } var extraArgs []string // command extra_args - // pass commandArgs into extraArgs. This is because after comment_parser, we will use extra_args only. - extraArgs = append(extraArgs, commandArgs...) 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) if err != nil { diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index e850fe85da..736e10346f 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -624,7 +624,7 @@ func TestParse_Parsing(t *testing.T) { } for _, test := range cases { - for _, cmdName := range []string{"plan", "apply", "import address id"} { + for _, cmdName := range []string{"plan", "apply", "import 'some[\"addr\"]' id"} { comment := fmt.Sprintf("atlantis %s %s", cmdName, test.flags) t.Run(comment, func(t *testing.T) { r := commentParser.Parse(comment, models.Github) @@ -645,10 +645,13 @@ func TestParse_Parsing(t *testing.T) { Assert(t, r.Command.Name == command.ApprovePolicies, "did not parse comment %q as approve_policies command", comment) Assert(t, test.expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", test.expExtraArgs, actExtraArgs, comment) } - if cmdName == "import" { - expExtraArgs := fmt.Sprintf("%s address id", test.expExtraArgs) + if strings.HasPrefix(cmdName, "import") { + expExtraArgs := "some[\"addr\"] id" // import use default args with `some["addr"] id` + if test.expExtraArgs != "" { + expExtraArgs = fmt.Sprintf("%s %s", test.expExtraArgs, expExtraArgs) + } 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", test.expExtraArgs, actExtraArgs, comment) + Assert(t, expExtraArgs == actExtraArgs, "exp extra args to equal %v but got %v for comment %q", expExtraArgs, actExtraArgs, comment) } }) } From 078c37a21d509aeb710817c0171c02367b7a1069 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 10:29:50 +0900 Subject: [PATCH 10/35] Update runatlantis.io/docs/using-atlantis.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/using-atlantis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index 11c3a1816c..e151ca278d 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -163,7 +163,7 @@ atlantis import -w staging ADDRESS ID If you need to run `terraform import` with additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars` you can append them to the end of the comment after `--`, ex. ``` -atlantis imoport -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' +atlantis import -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' ``` If you always need to append a certain flag, see [Custom Workflow Use Cases](custom-workflows.html#adding-extra-arguments-to-terraform-commands). From a3d92a44c19c9e920cdd7dd36dc1d42ed449185d Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 10:31:27 +0900 Subject: [PATCH 11/35] Update runatlantis.io/docs/using-atlantis.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/using-atlantis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index e151ca278d..b4f54ed784 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -165,7 +165,7 @@ you can append them to the end of the comment after `--`, ex. ``` atlantis import -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' ``` -If you always need to append a certain flag, 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 unlock From 0b51eb95b54b5ccfcc762da051ef27e6ee8eced7 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 10:31:42 +0900 Subject: [PATCH 12/35] Update runatlantis.io/docs/using-atlantis.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/using-atlantis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index b4f54ed784..81e5a1ac78 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -161,7 +161,7 @@ atlantis import -w staging ADDRESS ID ### Additional Terraform flags If you need to run `terraform import` with additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars` -you can append them to the end of the comment after `--`, ex. +append them to the end of the comment after `--`, e.g. ``` atlantis import -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' ``` From 0c3e5d670a34d1b851c3c23b1f86ee76846f5af7 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 10:31:58 +0900 Subject: [PATCH 13/35] Update runatlantis.io/docs/using-atlantis.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/using-atlantis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index 81e5a1ac78..49658df490 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -72,7 +72,7 @@ A `atlantis plan` (without flags), like autoplans, discards all plans previously ### Additional Terraform flags -If you need to run `terraform plan` with additional arguments, like `-target=resource` or `-var 'foo=bar'` or `-var-file myfile.tfvars` +If `terraform plan` requires additional arguments, like `-target=resource` or `-var 'foo=bar'` or `-var-file myfile.tfvars` you can append them to the end of the comment after `--`, ex. ``` atlantis plan -d dir -- -var foo='bar' From e1254436f0e987d621119991fff7005dce542c09 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 10:32:08 +0900 Subject: [PATCH 14/35] Update runatlantis.io/docs/using-atlantis.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/using-atlantis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/using-atlantis.md b/runatlantis.io/docs/using-atlantis.md index 49658df490..ca43da7c9c 100644 --- a/runatlantis.io/docs/using-atlantis.md +++ b/runatlantis.io/docs/using-atlantis.md @@ -160,7 +160,7 @@ atlantis import -w staging ADDRESS ID ### Additional Terraform flags -If you need to run `terraform import` with additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars` +If `terraform import` requires additional arguments, like `-var 'foo=bar'` or `-var-file myfile.tfvars` append them to the end of the comment after `--`, e.g. ``` atlantis import -d dir 'aws_instance.example["foo"]' i-1234567890abcdef0 -- -var foo='bar' From b62f12b5f7b3bcda12038bf2fdb75fb75eb5a015 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:46:14 +0900 Subject: [PATCH 15/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 9e9a96c38a..2aa2692af0 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -175,7 +175,7 @@ You can set the `undiverged` requirement by: - dir: . apply_requirements: [undiverged] import_requirements: [undiverged] - ``` + ``` #### Meaning The `merge` checkout strategy creates a temporary merge commit and runs the `plan` on the Atlantis local version of the PR source and destination branch. The local destination branch can become out of date since changes to the destination branch are not fetched From 2897d7fcc1b553aa4ed9639a520e2de8591eac19 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:46:28 +0900 Subject: [PATCH 16/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 2aa2692af0..34ae855170 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -70,7 +70,7 @@ You can set the `mergeable` requirement by: apply_requirements: [mergeable] ``` -1. Or by allowing an `atlantis.yaml` file to specify `apply_requirements` and `import_requirements` keys in your `repos.yaml` config: +1. Or by allowing an `atlantis.yaml` file to specify `apply_requirements` and `import_requirements` keys in the `repos.yaml` config: #### repos.yaml ```yaml repos: From 48813544151b41cdaf94548de27963312aa08158 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:46:43 +0900 Subject: [PATCH 17/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 34ae855170..7212e664cf 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -92,7 +92,7 @@ Each VCS provider has a different concept of "mergeability": ::: warning Some VCS providers have a feature for branch protection to control "mergeability". If you want to use it, -you probably need to limit the base branch not to bypass the branch protection. +limit the base branch so to not bypass the branch protection. See also the `branch` keyword in [Server Side Repo Config](server-side-repo-config.html#reference) for more details. ::: From 113fc38ead2ef4bc007f9f22e0b119ce8b89990f Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:46:55 +0900 Subject: [PATCH 18/35] Update server/events/comment_parser.go Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- server/events/comment_parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index caeb527871..8ab193f004 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -222,7 +222,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com flagSet = pflag.NewFlagSet(command.Import.String(), pflag.ContinueOnError) flagSet.SetOutput(io.Discard) flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") - flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan 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.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") requiredCommandArgCount = 2 // import requires `atlantis import ADDRESS ID` args From fb2b7525bc35a68574a411de6f708bdb4ad0bd1f Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:47:04 +0900 Subject: [PATCH 19/35] Update server/events/comment_parser.go Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- server/events/comment_parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 8ab193f004..812d2ecd80 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -221,7 +221,7 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com name = command.Import flagSet = pflag.NewFlagSet(command.Import.String(), pflag.ContinueOnError) flagSet.SetOutput(io.Discard) - flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") + flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before importing.") 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.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") From 3a857f79aa20460150f344a94d9ac1a519c376fb Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:47:15 +0900 Subject: [PATCH 20/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 7212e664cf..fa8ad6c18f 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -2,7 +2,7 @@ [[toc]] ## Intro -Atlantis allows you to require certain conditions be satisfied **before** `atlantis apply` and `atlantis import` +Atlantis requires certain conditions be satisfied **before** `atlantis apply` and `atlantis import` commands can be run: * [Approved](#approved) – requires pull requests to be approved by at least one user other than the author From 58ea4667d9c93fd60aeeb6dee3afb394bfaaa34e Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:47:28 +0900 Subject: [PATCH 21/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index fa8ad6c18f..434ce01a11 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -46,7 +46,7 @@ You can set the `approved` requirement by: #### Meaning Each VCS provider has different rules around who can approve: * **GitHub** – **Any user with read permissions** to the repo can approve a pull request -* **GitLab** – You [can set](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) who is allowed to approve +* **GitLab** – The user who can approve can be set in the [repo settings](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) * **Bitbucket Cloud (bitbucket.org)** – A user can approve their own pull request but Atlantis does not count that as an approval and requires an approval from at least one user that is not the author of the pull request From 1dbbe8573046e5587f9aecdbf8e6be3b20069d32 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:47:38 +0900 Subject: [PATCH 22/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 434ce01a11..192bf7fcf4 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -41,7 +41,7 @@ You can set the `approved` requirement by: projects: - dir: . apply_requirements: [approved] - ``` + ``` #### Meaning Each VCS provider has different rules around who can approve: From 2a0553693b0b60be5d669d4d6901922f5de151bc Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:01 +0900 Subject: [PATCH 23/35] Update runatlantis.io/docs/server-side-repo-config.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/server-side-repo-config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 07586a25cf..5bf6205ac9 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -148,7 +148,7 @@ repos: See [Command Requirements](command-requirements.html) for more details. -### Repos Can Set Their Own Apply or Import Requirements +### Repos Can Set Their Own Apply an applicable subcommand If you want all (or specific) repos to be able to override the default apply requirements, use the `allowed_overrides` key. From f5063330303a7e5dbe22ede351bcfc321b30e5c6 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:11 +0900 Subject: [PATCH 24/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 192bf7fcf4..bd774587ac 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -53,7 +53,7 @@ Each VCS provider has different rules around who can approve: * **Azure DevOps** – **All builtin groups include the "Contribute to pull requests"** permission and can approve a pull request :::tip Tip -If you want to require **certain people** to approve the pull request, look at the +To require **certain people** to approve the pull request, look at the [mergeable](#mergeable) requirement. ::: From 76642dcc5d3d6d620b5532b460dda94439810900 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:23 +0900 Subject: [PATCH 25/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index bd774587ac..b21c04fdcc 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -19,7 +19,7 @@ The `approved` requirement will prevent applies unless the pull request is appro by at least one person other than the author. #### Usage -You can set the `approved` requirement by: +The `approved` requirement by: 1. Passing the `--require-approval` flag to `atlantis server` or 1. Creating a `repos.yaml` file with the `apply_requirements` key: ```yaml From 56d563a479b8e24228d6390a721fecc1e4debcce Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:36 +0900 Subject: [PATCH 26/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index b21c04fdcc..3dde8a09e3 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -27,7 +27,7 @@ The `approved` requirement by: - id: /.*/ apply_requirements: [approved] ``` -1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config: +1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in the `repos.yaml` config: #### repos.yaml ```yaml repos: From 66c1cf2866230c5cefe47fec234e14b4f7e907f6 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:46 +0900 Subject: [PATCH 27/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 3dde8a09e3..0d85d95641 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -61,7 +61,7 @@ To require **certain people** to approve the pull request, look at the The `mergeable` requirement will prevent applies unless a pull request is able to be merged. #### Usage -You can set the `mergeable` requirement by: +Set the `mergeable` requirement by: 1. Passing the `--require-mergeable` flag to `atlantis server` or 1. Creating a `repos.yaml` file with the `apply_requirements` key: ```yaml From 3baf52e2cc0a4bab83c7a8d11a28136c78bf72e8 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:48:56 +0900 Subject: [PATCH 28/35] Update runatlantis.io/docs/command-requirements.md Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- runatlantis.io/docs/command-requirements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index 0d85d95641..d4313312e8 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -91,7 +91,7 @@ Set the `mergeable` requirement by: Each VCS provider has a different concept of "mergeability": ::: warning -Some VCS providers have a feature for branch protection to control "mergeability". If you want to use it, +Some VCS providers have a feature for branch protection to control "mergeability". To use it, limit the base branch so to not bypass the branch protection. See also the `branch` keyword in [Server Side Repo Config](server-side-repo-config.html#reference) for more details. ::: From 1456da35546bf86ed6dc10060e247bd53680d4d5 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:49:10 +0900 Subject: [PATCH 29/35] Update server/events/command_requirement_handler.go Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- server/events/command_requirement_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/command_requirement_handler.go b/server/events/command_requirement_handler.go index f67ef531be..5b487c517f 100644 --- a/server/events/command_requirement_handler.go +++ b/server/events/command_requirement_handler.go @@ -27,7 +27,7 @@ func (a *DefaultCommandRequirementHandler) ValidateApplyProject(repoDir string, // this should come before mergeability check since mergeability is a superset of this check. case valid.PoliciesPassedCommandReq: if ctx.ProjectPlanStatus == models.ErroredPolicyCheckStatus { - return "All policies must pass for project before running apply", nil + return "All policies must pass for project before running apply.", nil } case raw.MergeableRequirement: if !ctx.PullReqStatus.Mergeable { From 728c405e800d3445f2ecfe8aefd84319123d977f Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Thu, 22 Dec 2022 16:49:20 +0900 Subject: [PATCH 30/35] Update server/events/command_requirement_handler_test.go Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- server/events/command_requirement_handler_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/command_requirement_handler_test.go b/server/events/command_requirement_handler_test.go index d190cd657e..5ec88dbf74 100644 --- a/server/events/command_requirement_handler_test.go +++ b/server/events/command_requirement_handler_test.go @@ -69,7 +69,7 @@ func TestAggregateApplyRequirements_ValidateApplyProject(t *testing.T) { ApplyRequirements: []string{valid.PoliciesPassedCommandReq}, ProjectPlanStatus: models.ErroredPolicyCheckStatus, }, - wantFailure: "All policies must pass for project before running apply", + wantFailure: "All policies must pass for project before running apply.", wantErr: assert.NoError, }, { From 7ccde7ca4e4bf0ed95e1d639892c93bf9d34d573 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Dec 2022 16:55:10 +0900 Subject: [PATCH 31/35] fix test import usage --- server/events/comment_parser_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index 736e10346f..8edffe2c25 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -937,11 +937,11 @@ var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + "\n```" var ImportUsage = `Usage of import ADDRESS ID: - -d, --dir string Which directory to run plan in relative to root of repo, - ex. 'child/dir'. + -d, --dir string Which directory to run import in relative to root of + repo, ex. 'child/dir'. -p, --project string 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. --verbose Append Atlantis log to comment. - -w, --workspace string Switch to this Terraform workspace before planning. + -w, --workspace string Switch to this Terraform workspace before importing. ` From 9556879afda0b37d113977b7b21e93e538920a9c Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Dec 2022 17:00:31 +0900 Subject: [PATCH 32/35] fix e2e expected txt --- .../policy-checks-apply-reqs/exp-output-apply-failed.txt | 2 +- .../policy-checks-diff-owner/exp-output-apply-failed.txt | 2 +- .../policy-checks-extra-args/exp-output-apply-failed.txt | 2 +- .../policy-checks-multi-projects/exp-output-apply.txt | 2 +- .../test-repos/policy-checks/exp-output-apply-failed.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/controllers/events/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt b/server/controllers/events/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt index 1f57a9176d..3025d6f52a 100644 --- a/server/controllers/events/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt +++ b/server/controllers/events/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt @@ -1,4 +1,4 @@ Ran Apply for dir: `.` workspace: `default` -**Apply Failed**: All policies must pass for project before running apply +**Apply Failed**: All policies must pass for project before running apply. diff --git a/server/controllers/events/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt b/server/controllers/events/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt index 1f57a9176d..3025d6f52a 100644 --- a/server/controllers/events/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt +++ b/server/controllers/events/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt @@ -1,4 +1,4 @@ Ran Apply for dir: `.` workspace: `default` -**Apply Failed**: All policies must pass for project before running apply +**Apply Failed**: All policies must pass for project before running apply. diff --git a/server/controllers/events/testfixtures/test-repos/policy-checks-extra-args/exp-output-apply-failed.txt b/server/controllers/events/testfixtures/test-repos/policy-checks-extra-args/exp-output-apply-failed.txt index 1f57a9176d..3025d6f52a 100644 --- a/server/controllers/events/testfixtures/test-repos/policy-checks-extra-args/exp-output-apply-failed.txt +++ b/server/controllers/events/testfixtures/test-repos/policy-checks-extra-args/exp-output-apply-failed.txt @@ -1,4 +1,4 @@ Ran Apply for dir: `.` workspace: `default` -**Apply Failed**: All policies must pass for project before running apply +**Apply Failed**: All policies must pass for project before running apply. diff --git a/server/controllers/events/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt b/server/controllers/events/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt index 8baa226607..9c9ab05813 100644 --- a/server/controllers/events/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt +++ b/server/controllers/events/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt @@ -18,7 +18,7 @@ workspace = "default" --- ### 2. dir: `dir2` workspace: `default` -**Apply Failed**: All policies must pass for project before running apply +**Apply Failed**: All policies must pass for project before running apply. --- diff --git a/server/controllers/events/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt b/server/controllers/events/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt index 1f57a9176d..3025d6f52a 100644 --- a/server/controllers/events/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt +++ b/server/controllers/events/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt @@ -1,4 +1,4 @@ Ran Apply for dir: `.` workspace: `default` -**Apply Failed**: All policies must pass for project before running apply +**Apply Failed**: All policies must pass for project before running apply. From 7797de7a9c475b06759eb7222748f92639dc3247 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Dec 2022 17:15:39 +0900 Subject: [PATCH 33/35] fix doc link --- runatlantis.io/docs/repo-level-atlantis-yaml.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 4bb3925d20..ca7d27354c 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -220,7 +220,7 @@ projects: ``` :::warning `apply_requirements` and `import_requirements` are restricted keys so this repo will need to be configured -to be allowed to set this key. See [Server-Side Repo Config Use Cases](server-side-repo-config.html#repos-can-set-their-own-apply-or-import-requirements). +to be allowed to set this key. See [Server-Side Repo Config Use Cases](server-side-repo-config.html#repos-can-set-their-own-apply-an-applicable-subcommand). ::: ### Order of planning/applying From 79ba39edfb1ce5c775c46dcbb70fe26f48157361 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Fri, 23 Dec 2022 03:48:48 +0900 Subject: [PATCH 34/35] docs: workflow import stage/step --- runatlantis.io/docs/custom-workflows.md | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md index b8dd165630..88a6087136 100644 --- a/runatlantis.io/docs/custom-workflows.md +++ b/runatlantis.io/docs/custom-workflows.md @@ -373,12 +373,14 @@ projects: ```yaml plan: apply: +import: ``` -| Key | Type | Default | Required | Description | -|-------|-----------------|-----------------------|----------|--------------------------------| -| 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. | +| Key | Type | Default | Required | Description | +|--------|-----------------|-------------------------|----------|----------------------------------| +| 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. | +| import | [Stage](#stage) | `steps: [init, import]` | no | How to import for this project. | ### Stage ```yaml @@ -394,16 +396,17 @@ steps: | steps | array[[Step](#step)] | `[]` | no | List of steps for this stage. If the steps key is empty, no steps will be run for this stage. | ### Step -#### Built-In Commands: init, plan, apply +#### Built-In Commands Steps can be a single string for a built-in command. ```yaml - init - plan - apply +- import ``` -| Key | Type | Default | Required | Description | -| --------------- | ------ | ------- | -------- | ------------------------------------------------------------------------------------------------------ | -| init/plan/apply | string | none | no | Use a built-in command without additional configuration. Only `init`, `plan` and `apply` are supported | +| 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 | #### Built-In Command With Extra Args A map from string to `extra_args` for a built-in command with extra arguments. @@ -414,10 +417,12 @@ A map from string to `extra_args` for a built-in command with extra arguments. extra_args: [arg1, arg2] - apply: extra_args: [arg1, arg2] +- import: + extra_args: [arg1, arg2] ``` -| Key | Type | Default | Required | Description | -|-----------------|------------------------------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| init/plan/apply | map[`extra_args` -> array[string]] | none | no | Use a built-in command and append `extra_args`. Only `init`, `plan` and `apply` are supported as keys and only `extra_args` is supported as a value | +| 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 | #### Custom `run` Command Or a custom command From cd29e469568d63bda8d951dd8fea34f6032848e6 Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Fri, 23 Dec 2022 05:33:55 +0900 Subject: [PATCH 35/35] docs fixup --- runatlantis.io/docs/command-requirements.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runatlantis.io/docs/command-requirements.md b/runatlantis.io/docs/command-requirements.md index d4313312e8..2620cc7554 100644 --- a/runatlantis.io/docs/command-requirements.md +++ b/runatlantis.io/docs/command-requirements.md @@ -68,7 +68,7 @@ Set the `mergeable` requirement by: repos: - id: /.*/ apply_requirements: [mergeable] - ``` + ``` 1. Or by allowing an `atlantis.yaml` file to specify `apply_requirements` and `import_requirements` keys in the `repos.yaml` config: #### repos.yaml @@ -241,7 +241,7 @@ If you only want some projects/repos to have apply requirements, then you must # production directory. apply_requirements: [mergeable] import_requirements: [mergeable] - + ``` ### Multiple Requirements You can set any or all of `approved`, `mergeable`, and `undiverged` requirements.