From 25fed3572830861c88e54a835608e006e5e4f6ef Mon Sep 17 00:00:00 2001
From: Luke Kysow <1034429+lkysow@users.noreply.github.com>
Date: Thu, 28 Mar 2019 12:09:03 -0500
Subject: [PATCH] New version 3 of atlantis.yaml
This version changes how we parse custom run steps.
Previously we used the go-shlex library to parse the steps but this
caused users issues because they'd have to use weird escaping.
Now we simply pass the step directy to sh -c without any parsing. The
parsing was unnecessary.
---
CHANGELOG.md | 2 +-
.../.vuepress/components/HomeCustom.vue | 2 +-
runatlantis.io/.vuepress/config.js | 2 +-
runatlantis.io/docs/apply-requirements.md | 6 +-
runatlantis.io/docs/automerging.md | 2 +-
runatlantis.io/docs/custom-workflows.md | 6 +-
.../docs/repo-level-atlantis-yaml.md | 12 +-
.../docs/server-side-repo-config.md | 6 +-
runatlantis.io/docs/terraform-versions.md | 2 +-
...ersion-2.md => upgrading-atlantis-yaml.md} | 53 ++++-
.../events/mocks/mock_custom_step_runner.go | 117 +++++++++++
server/events/models/models.go | 3 +
server/events/project_command_builder.go | 1 +
.../project_command_builder_internal_test.go | 27 ++-
server/events/project_command_builder_test.go | 28 +--
server/events/project_command_runner.go | 10 +-
server/events/project_command_runner_test.go | 12 +-
server/events/runtime/run_step_runner.go | 20 +-
server/events/runtime/run_step_runner_test.go | 20 +-
server/events/yaml/parser_validator.go | 43 ++++
server/events/yaml/parser_validator_test.go | 185 +++++++++++++-----
server/events/yaml/raw/repo_cfg.go | 6 +-
server/events/yaml/raw/repo_cfg_test.go | 23 ++-
server/events/yaml/raw/step.go | 12 +-
server/events/yaml/raw/step_test.go | 5 +-
server/events/yaml/valid/global_cfg.go | 2 +
server/events/yaml/valid/repo_cfg.go | 5 +-
.../test-repos/automerge/atlantis.yaml | 2 +-
.../test-repos/modules-yaml/atlantis.yaml | 2 +-
.../test-repos/simple-yaml/atlantis.yaml | 2 +-
.../tfvars-yaml-no-autoplan/atlantis.yaml | 2 +-
.../test-repos/tfvars-yaml/atlantis.yaml | 2 +-
32 files changed, 466 insertions(+), 156 deletions(-)
rename runatlantis.io/docs/{upgrading-atlantis-yaml-to-version-2.md => upgrading-atlantis-yaml.md} (72%)
create mode 100644 server/events/mocks/mock_custom_step_runner.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbc034440..56cef4efe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -808,7 +808,7 @@ when new commits are pushed to the pull request.
## Backwards Incompatibilities / Notes:
- The old `atlantis.yaml` config file format is not supported. You will need to migrate to the new config
-format, see: https://www.runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.html
+format, see: https://www.runatlantis.io/docs/upgrading-atlantis-yaml.html
- To use the new config file, you must run Atlantis with `--allow-repo-config`.
- Atlantis will now try to automatically plan. To disable this, you'll need to create an `atlantis.yaml` file
as follows:
diff --git a/runatlantis.io/.vuepress/components/HomeCustom.vue b/runatlantis.io/.vuepress/components/HomeCustom.vue
index a8b2c1777..bfac96b59 100644
--- a/runatlantis.io/.vuepress/components/HomeCustom.vue
+++ b/runatlantis.io/.vuepress/components/HomeCustom.vue
@@ -5,7 +5,7 @@
{{ data.heroText || $title || 'Hello' }}
- Start working on Terraform as a team.
+ Terraform Pull Request Automation
Get Started →
diff --git a/runatlantis.io/.vuepress/config.js b/runatlantis.io/.vuepress/config.js
index 79d77227d..8aba78573 100644
--- a/runatlantis.io/.vuepress/config.js
+++ b/runatlantis.io/.vuepress/config.js
@@ -62,7 +62,7 @@ module.exports = {
'server-side-repo-config',
'custom-workflows',
'repo-level-atlantis-yaml',
- 'upgrading-atlantis-yaml-to-version-2',
+ 'upgrading-atlantis-yaml',
'apply-requirements',
'checkout-strategy',
'terraform-versions',
diff --git a/runatlantis.io/docs/apply-requirements.md b/runatlantis.io/docs/apply-requirements.md
index 08c0d9956..ce6df1d11 100644
--- a/runatlantis.io/docs/apply-requirements.md
+++ b/runatlantis.io/docs/apply-requirements.md
@@ -36,7 +36,7 @@ You can set the `approved` requirement by:
#### atlantis.yaml
```yaml
- version: 2
+ version: 3
projects:
- dir: .
apply_requirements: [approved]
@@ -78,7 +78,7 @@ You can set the `mergeable` requirement by:
#### atlantis.yaml
```yaml
- version: 2
+ version: 3
projects:
- dir: .
apply_requirements: [mergeable]
@@ -163,7 +163,7 @@ If you only want some projects/repos to have apply requirements, then you must
#### atlatis.yaml
```yaml
- version: 2
+ version: 3
projects:
- dir: staging
# By default, apply_requirements is empty so this
diff --git a/runatlantis.io/docs/automerging.md b/runatlantis.io/docs/automerging.md
index aff40372f..440e8b9b0 100644
--- a/runatlantis.io/docs/automerging.md
+++ b/runatlantis.io/docs/automerging.md
@@ -11,7 +11,7 @@ Automerging can be enabled either by:
pull requests to be automerged and any repo config will be ignored.
1. Setting `automerge: true` in the repo's `atlantis.yaml` file:
```yaml
- version: 2
+ version: 3
automerge: true
projects:
- dir: .
diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md
index 5b199c714..f84af536e 100644
--- a/runatlantis.io/docs/custom-workflows.md
+++ b/runatlantis.io/docs/custom-workflows.md
@@ -51,7 +51,7 @@ workflows:
Then in your repo-level `atlantis.yaml` file, you would reference the workflows:
```yaml
# atlantis.yaml
-version: 2
+version: 3
projects:
# If two or more projects have the same dir and workspace, they must also have
# a 'name' key to differentiate them.
@@ -163,7 +163,7 @@ workflows:
Which you would then reference in your repo-level `atlantis.yaml`:
```yaml
-version: 2
+version: 3
projects:
- dir: live/staging
workflow: terragrunt
@@ -233,7 +233,7 @@ will complain in-between commands since the backend config has changed.
You would then reference the workflows in your repo-level `atlantis.yaml`:
```yaml
-version: 2
+version: 3
projects:
- name: staging
dir: .
diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md
index 608313755..6da2644c0 100644
--- a/runatlantis.io/docs/repo-level-atlantis-yaml.md
+++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md
@@ -44,7 +44,7 @@ need to be defined.
## Example Using All Keys
```yaml
-version: 2
+version: 3
automerge: true
projects:
- name: my-project-name
@@ -74,7 +74,7 @@ workflows:
## Use Cases
### Disabling Autoplanning
```yaml
-version: 2
+version: 3
projects:
- dir: project1
autoplan:
@@ -101,7 +101,7 @@ If you wanted Atlantis to autoplan `project1/` whenever any `.tf` file under `mo
changed or any `.tf` or `.tfvars` file under `project1/` changed, you could use the following configuration:
```yaml
-version: 2
+version: 3
projects:
- dir: project1
autoplan:
@@ -113,7 +113,7 @@ Note:
### Supporting Terraform Workspaces
```yaml
-version: 2
+version: 3
projects:
- dir: project1
workspace: staging
@@ -152,7 +152,7 @@ If you'd like to use a different version of Terraform than what is in Atlantis'
`PATH` or is set by the `--default-tf-version` flag, then set the `terraform_version` key:
```yaml
-version: 2
+version: 3
projects:
- dir: project1
terraform_version: 0.10.0
@@ -163,7 +163,7 @@ Atlantis will automatically download and use this version.
### Requiring Approvals For Production
In this example, we only want to require `apply` approvals for the `production` directory.
```yaml
-version: 2
+version: 3
projects:
- dir: staging
- dir: production
diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md
index 1c59f456f..0a2946b3c 100644
--- a/runatlantis.io/docs/server-side-repo-config.md
+++ b/runatlantis.io/docs/server-side-repo-config.md
@@ -143,7 +143,7 @@ Then each allowed repo can have an `atlantis.yaml` file that
sets `apply_requirements` to an empty array (disabling the requirement).
```yaml
# atlantis.yaml in the repo root
-version: 2
+version: 3
projects:
- dir: .
apply_requirements: []
@@ -210,7 +210,7 @@ files:
```yaml
# atlantis.yaml
-version: 2
+version: 3
projects:
- dir: .
workflow: custom1 # could also be custom2 OR default
@@ -250,7 +250,7 @@ repos:
Then each allowed repo can define and use a custom workflow in their `atlantis.yaml` files:
```yaml
# atlantis.yaml
-version: 2
+version: 3
projects:
- dir: .
workflow: custom1
diff --git a/runatlantis.io/docs/terraform-versions.md b/runatlantis.io/docs/terraform-versions.md
index c6685e629..03e29ec74 100644
--- a/runatlantis.io/docs/terraform-versions.md
+++ b/runatlantis.io/docs/terraform-versions.md
@@ -6,7 +6,7 @@ the `--default-tf-version` flag (ex. `--default-tf-version=v0.12.0`).
If you wish to use a different version than the default for a specific repo or project, you need
to create an `atlantis.yaml` file and set the `terraform_version` key:
```yaml
-version: 2
+version: 3
projects:
- dir: .
terraform_version: v0.10.5
diff --git a/runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.md b/runatlantis.io/docs/upgrading-atlantis-yaml.md
similarity index 72%
rename from runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.md
rename to runatlantis.io/docs/upgrading-atlantis-yaml.md
index 4da3c2b9f..1d78b6155 100644
--- a/runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.md
+++ b/runatlantis.io/docs/upgrading-atlantis-yaml.md
@@ -1,8 +1,45 @@
-# Upgrading atlantis.yaml To Version 2
-These docs describe how to upgrade your `atlantis.yaml` file from the format used
-in Atlantis `<=v0.3.10`.
+# Upgrading atlantis.yaml
-## Single atlantis.yaml
+## Upgrading From v2 To v3
+Atlantis version `v0.7.0` introduced a new version 3 of `atlantis.yaml`.
+
+**If you're not using [custom `run` steps](custom-workflows.html#custom-run-command),
+ then you can upgrade from `version: 2` to `version: 3` without any changes.**
+
+**NOTE:** Version 2 **is not being deprecated** and there is no need to upgrade your version
+if you don't wish to do so.
+
+The only change from v2 to v3 is that we're parsing custom `run` steps differently.
+```yaml
+# atlantis.yaml
+worklows:
+ custom:
+ plan:
+ steps:
+ - run: my custom command
+```
+
An example workflow using a custom run step
+
+Previously, we used a library that would parse the custom step prior to running
+it. Now, we just run the step directly. This will only affect your steps if they were using shell escaping of some sort.
+For example, if your step was previously:
+```yaml
+# version: 2
+- run: "printf \'print me\'"
+```
+
+You can now write this in version 3 as:
+```yaml
+# version: 3
+- run: "printf 'print me'"
+```
+
+
+## Upgrading From V1 To V3
+If you are upgrading from an **old** Atlantis version `<=v0.3.10` (from before July 4, 2018)
+you'll need to follow the following steps.
+
+### Single atlantis.yaml
If you had multiple `atlantis.yaml` files per directory then you'll need to
consolidate them into a single `atlantis.yaml` file at the root of the repo.
@@ -42,7 +79,7 @@ workflows:
We will talk more about `workflows` below.
-## Terraform Version
+### Terraform Version
The `terraform_version` key moved from being a top-level key to being per `project`
so if before your `atlantis.yaml` was in directory `mydir` and looked like:
```yaml
@@ -57,7 +94,7 @@ projects:
terraform_version: 0.11.0
```
-## Workflows
+### Workflows
Workflows are the new way to set all `pre_*`, `post_*` and `extra_arguments`.
Each `project` can have a custom workflow via the `workflow` key.
@@ -105,7 +142,7 @@ workflows:
...
```
-### Extra Arguments
+#### Extra Arguments
`extra_arguments` is now specified as follows. Given a previous config:
```yaml
extra_arguments:
@@ -138,7 +175,7 @@ workflows:
```
-### Pre/Post Commands
+#### Pre/Post Commands
Instead of using `pre_*` or `post_*`, you now can insert your custom commands
before/after the built-in commands. Given a previous config:
diff --git a/server/events/mocks/mock_custom_step_runner.go b/server/events/mocks/mock_custom_step_runner.go
new file mode 100644
index 000000000..abe32003a
--- /dev/null
+++ b/server/events/mocks/mock_custom_step_runner.go
@@ -0,0 +1,117 @@
+// Code generated by pegomock. DO NOT EDIT.
+// Source: github.com/runatlantis/atlantis/server/events (interfaces: CustomStepRunner)
+
+package mocks
+
+import (
+ pegomock "github.com/petergtz/pegomock"
+ models "github.com/runatlantis/atlantis/server/events/models"
+ "reflect"
+ "time"
+)
+
+type MockCustomStepRunner struct {
+ fail func(message string, callerSkip ...int)
+}
+
+func NewMockCustomStepRunner(options ...pegomock.Option) *MockCustomStepRunner {
+ mock := &MockCustomStepRunner{}
+ for _, option := range options {
+ option.Apply(mock)
+ }
+ return mock
+}
+
+func (mock *MockCustomStepRunner) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
+func (mock *MockCustomStepRunner) FailHandler() pegomock.FailHandler { return mock.fail }
+
+func (mock *MockCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string) (string, error) {
+ if mock == nil {
+ panic("mock must not be nil. Use myMock := NewMockCustomStepRunner().")
+ }
+ params := []pegomock.Param{ctx, cmd, path}
+ result := pegomock.GetGenericMockFrom(mock).Invoke("Run", 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 *MockCustomStepRunner) VerifyWasCalledOnce() *VerifierCustomStepRunner {
+ return &VerifierCustomStepRunner{
+ mock: mock,
+ invocationCountMatcher: pegomock.Times(1),
+ }
+}
+
+func (mock *MockCustomStepRunner) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierCustomStepRunner {
+ return &VerifierCustomStepRunner{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ }
+}
+
+func (mock *MockCustomStepRunner) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierCustomStepRunner {
+ return &VerifierCustomStepRunner{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ inOrderContext: inOrderContext,
+ }
+}
+
+func (mock *MockCustomStepRunner) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierCustomStepRunner {
+ return &VerifierCustomStepRunner{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ timeout: timeout,
+ }
+}
+
+type VerifierCustomStepRunner struct {
+ mock *MockCustomStepRunner
+ invocationCountMatcher pegomock.Matcher
+ inOrderContext *pegomock.InOrderContext
+ timeout time.Duration
+}
+
+func (verifier *VerifierCustomStepRunner) Run(ctx models.ProjectCommandContext, cmd string, path string) *CustomStepRunner_Run_OngoingVerification {
+ params := []pegomock.Param{ctx, cmd, path}
+ methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout)
+ return &CustomStepRunner_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
+}
+
+type CustomStepRunner_Run_OngoingVerification struct {
+ mock *MockCustomStepRunner
+ methodInvocations []pegomock.MethodInvocation
+}
+
+func (c *CustomStepRunner_Run_OngoingVerification) GetCapturedArguments() (models.ProjectCommandContext, string, string) {
+ ctx, cmd, path := c.GetAllCapturedArguments()
+ return ctx[len(ctx)-1], cmd[len(cmd)-1], path[len(path)-1]
+}
+
+func (c *CustomStepRunner_Run_OngoingVerification) GetAllCapturedArguments() (_param0 []models.ProjectCommandContext, _param1 []string, _param2 []string) {
+ params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
+ if len(params) > 0 {
+ _param0 = make([]models.ProjectCommandContext, len(params[0]))
+ for u, param := range params[0] {
+ _param0[u] = param.(models.ProjectCommandContext)
+ }
+ _param1 = make([]string, len(params[1]))
+ for u, param := range params[1] {
+ _param1[u] = param.(string)
+ }
+ _param2 = make([]string, len(params[2]))
+ 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 ba84d066b..4756b1386 100644
--- a/server/events/models/models.go
+++ b/server/events/models/models.go
@@ -317,6 +317,9 @@ type ProjectCommandContext struct {
// ProjectName is the name of the project set in atlantis.yaml. If there was
// no name this will be an empty string.
ProjectName string
+ // RepoConfigVersion is the version of the repo's atlantis.yaml file. If
+ // there was no file, this will be 0.
+ RepoConfigVersion int
// RePlanCmd is the command that users should run to re-plan this project.
// If this is an apply then this will be empty.
RePlanCmd string
diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go
index a15e1f77f..a842861df 100644
--- a/server/events/project_command_builder.go
+++ b/server/events/project_command_builder.go
@@ -394,6 +394,7 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
ApplyRequirements: projCfg.ApplyRequirements,
RePlanCmd: p.CommentBuilder.BuildPlanComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name, commentArgs),
RepoRelDir: projCfg.RepoRelDir,
+ RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,
diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go
index 2c05697e3..64f470759 100644
--- a/server/events/project_command_builder_internal_test.go
+++ b/server/events/project_command_builder_internal_test.go
@@ -87,7 +87,7 @@ workflows:
steps:
- apply`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -109,6 +109,7 @@ projects:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -137,7 +138,7 @@ workflows:
steps:
- apply`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -159,6 +160,7 @@ projects:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{"approved", "mergeable"},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -195,7 +197,7 @@ workflows:
apply:
steps: []`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -217,6 +219,7 @@ projects:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{"approved"},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -246,7 +249,7 @@ workflows:
steps:
- apply`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -273,7 +276,7 @@ workflows:
steps:
- apply`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -301,7 +304,7 @@ workflows:
steps:
- apply`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -330,7 +333,7 @@ workflows:
steps: []
`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -362,6 +365,7 @@ workflows:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -393,7 +397,7 @@ workflows:
steps: [apply]
`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -416,6 +420,7 @@ projects:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -444,7 +449,7 @@ workflows:
steps: [apply]
`,
repoCfg: `
-version: 2
+version: 3
automerge: true
projects:
- dir: project1
@@ -473,6 +478,7 @@ workflows:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
@@ -497,7 +503,7 @@ workflows:
steps: [plan]
`,
repoCfg: `
-version: 2
+version: 3
projects:
- dir: project1
workspace: myworkspace
@@ -514,6 +520,7 @@ projects:
Pull: models.PullRequest{},
ProjectName: "",
ApplyRequirements: []string{"approved"},
+ RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -d project1 -w myworkspace -- flag",
RepoRelDir: "project1",
User: models.User{},
diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go
index 8f5d0abb6..246466294 100644
--- a/server/events/project_command_builder_test.go
+++ b/server/events/project_command_builder_test.go
@@ -35,7 +35,7 @@ func TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) {
{
Description: "simple atlantis.yaml",
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
`,
@@ -50,7 +50,7 @@ projects:
{
Description: "some projects disabled",
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
autoplan:
@@ -79,7 +79,7 @@ projects:
{
Description: "some projects disabled",
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
autoplan:
@@ -107,7 +107,7 @@ projects:
{
Description: "no projects modified",
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: mydir
`,
@@ -203,7 +203,7 @@ func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T)
Workspace: "myworkspace",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
workspace: myworkspace
@@ -220,7 +220,7 @@ projects:
Workspace: "myworkspace",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: notroot
workspace: myworkspace
@@ -237,7 +237,7 @@ projects:
Workspace: "myworkspace",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
workspace: notmyworkspace
@@ -251,7 +251,7 @@ projects:
ProjectName: "myproject",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- name: myproject
dir: .
@@ -269,7 +269,7 @@ projects:
ProjectName: "myproject",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- name: myproject
dir: .
@@ -287,7 +287,7 @@ projects:
ProjectName: "myproject",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- name: myproject
dir: .
@@ -306,7 +306,7 @@ projects:
Workspace: "myworkspace",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- name: myproject
dir: .
@@ -327,7 +327,7 @@ projects:
ProjectName: "notconfigured",
},
AtlantisYAML: `
-version: 2
+version: 3
projects:
- dir: .
`,
@@ -447,7 +447,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands(t *testing.T) {
"main.tf": nil,
},
},
- AtlantisYAML: `version: 2
+ AtlantisYAML: `version: 3
projects:
- dir: project1 # project1 uses the defaults
- dir: project2 # project2 has autoplan disabled but should use default when_modified
@@ -606,7 +606,7 @@ func TestDefaultProjectCommandBuilder_WrongWorkspaceName(t *testing.T) {
defer cleanup()
repoDir := filepath.Join(tmpDir, "pulldir/notconfigured")
- yamlCfg := `version: 2
+ yamlCfg := `version: 3
projects:
- dir: .
workspace: default
diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go
index a789376f9..cde476949 100644
--- a/server/events/project_command_runner.go
+++ b/server/events/project_command_runner.go
@@ -55,6 +55,14 @@ type StepRunner interface {
Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error)
}
+//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_custom_step_runner.go CustomStepRunner
+
+// CustomStepRunner runs custom run steps.
+type CustomStepRunner interface {
+ // Run cmd in path.
+ Run(ctx models.ProjectCommandContext, cmd string, path string) (string, error)
+}
+
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_webhooks_sender.go WebhooksSender
// WebhooksSender sends webhook.
@@ -81,7 +89,7 @@ type DefaultProjectCommandRunner struct {
InitStepRunner StepRunner
PlanStepRunner StepRunner
ApplyStepRunner StepRunner
- RunStepRunner StepRunner
+ RunStepRunner CustomStepRunner
PullApprovedChecker runtime.PullApprovedChecker
WorkingDir WorkingDir
Webhooks WebhooksSender
diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go
index 22797ffce..b2ac2766d 100644
--- a/server/events/project_command_runner_test.go
+++ b/server/events/project_command_runner_test.go
@@ -34,7 +34,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
mockInit := mocks.NewMockStepRunner()
mockPlan := mocks.NewMockStepRunner()
mockApply := mocks.NewMockStepRunner()
- mockRun := mocks.NewMockStepRunner()
+ mockRun := mocks.NewMockCustomStepRunner()
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
@@ -94,7 +94,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
When(mockInit.Run(ctx, nil, repoDir)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir)).ThenReturn("apply", nil)
- When(mockRun.Run(ctx, nil, repoDir)).ThenReturn("run", nil)
+ When(mockRun.Run(ctx, "", repoDir)).ThenReturn("run", nil)
res := runner.Plan(ctx)
@@ -112,7 +112,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir)
case "run":
- mockRun.VerifyWasCalledOnce().Run(ctx, nil, repoDir)
+ mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir)
}
}
}
@@ -250,7 +250,7 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
mockInit := mocks.NewMockStepRunner()
mockPlan := mocks.NewMockStepRunner()
mockApply := mocks.NewMockStepRunner()
- mockRun := mocks.NewMockStepRunner()
+ mockRun := mocks.NewMockCustomStepRunner()
mockApproved := mocks2.NewMockPullApprovedChecker()
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
@@ -287,7 +287,7 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
When(mockInit.Run(ctx, nil, repoDir)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir)).ThenReturn("apply", nil)
- When(mockRun.Run(ctx, nil, repoDir)).ThenReturn("run", nil)
+ When(mockRun.Run(ctx, "", repoDir)).ThenReturn("run", nil)
When(mockApproved.PullIsApproved(ctx.BaseRepo, ctx.Pull)).ThenReturn(true, nil)
res := runner.Apply(ctx)
@@ -305,7 +305,7 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir)
case "run":
- mockRun.VerifyWasCalledOnce().Run(ctx, nil, repoDir)
+ mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir)
}
}
})
diff --git a/server/events/runtime/run_step_runner.go b/server/events/runtime/run_step_runner.go
index 3a18ddf93..438e4d898 100644
--- a/server/events/runtime/run_step_runner.go
+++ b/server/events/runtime/run_step_runner.go
@@ -2,14 +2,11 @@ package runtime
import (
"fmt"
+ "github.com/hashicorp/go-version"
+ "github.com/runatlantis/atlantis/server/events/models"
"os"
"os/exec"
"path/filepath"
- "strings"
-
- "github.com/hashicorp/go-version"
- "github.com/pkg/errors"
- "github.com/runatlantis/atlantis/server/events/models"
)
// RunStepRunner runs custom commands.
@@ -17,12 +14,8 @@ type RunStepRunner struct {
DefaultTFVersion *version.Version
}
-func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command []string, path string) (string, error) {
- if len(command) < 1 {
- return "", errors.New("no commands for run step")
- }
-
- cmd := exec.Command("sh", "-c", strings.Join(command, " ")) // #nosec
+func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command string, path string) (string, error) {
+ cmd := exec.Command("sh", "-c", command) // #nosec
cmd.Dir = path
tfVersion := r.DefaultTFVersion.String()
if ctx.TerraformVersion != nil {
@@ -52,12 +45,11 @@ func (r *RunStepRunner) Run(ctx models.ProjectCommandContext, command []string,
cmd.Env = finalEnvVars
out, err := cmd.CombinedOutput()
- commandStr := strings.Join(command, " ")
if err != nil {
- err = fmt.Errorf("%s: running %q in %q: \n%s", err, commandStr, path, out)
+ err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, out)
ctx.Log.Debug("error: %s", err)
return string(out), err
}
- ctx.Log.Info("successfully ran %q in %q", commandStr, path)
+ ctx.Log.Info("successfully ran %q in %q", command, path)
return string(out), nil
}
diff --git a/server/events/runtime/run_step_runner_test.go b/server/events/runtime/run_step_runner_test.go
index 10a87c798..e961ccdde 100644
--- a/server/events/runtime/run_step_runner_test.go
+++ b/server/events/runtime/run_step_runner_test.go
@@ -19,12 +19,24 @@ func TestRunStepRunner_Run(t *testing.T) {
}{
{
Command: "",
- ExpErr: "no commands for run step",
+ ExpOut: "",
},
{
Command: "echo hi",
ExpOut: "hi\n",
},
+ {
+ Command: `printf \'your main.tf file does not provide default region.\\ncheck\'`,
+ ExpOut: `'your`,
+ },
+ {
+ Command: `printf 'your main.tf file does not provide default region.\ncheck'`,
+ ExpOut: "your main.tf file does not provide default region.\ncheck",
+ },
+ {
+ Command: "echo 'a",
+ ExpErr: "exit status 2: running \"echo 'a\" in",
+ },
{
Command: "echo hi >> file && cat file",
ExpOut: "hi\n",
@@ -80,11 +92,7 @@ func TestRunStepRunner_Run(t *testing.T) {
t.Run(c.Command, func(t *testing.T) {
tmpDir, cleanup := TempDir(t)
defer cleanup()
- var split []string
- if c.Command != "" {
- split = strings.Split(c.Command, " ")
- }
- out, err := r.Run(ctx, split, tmpDir)
+ out, err := r.Run(ctx, c.Command, tmpDir)
if c.ExpErr != "" {
ErrContains(t, c.ExpErr, err)
return
diff --git a/server/events/yaml/parser_validator.go b/server/events/yaml/parser_validator.go
index a6cf99c8a..48d27f86a 100644
--- a/server/events/yaml/parser_validator.go
+++ b/server/events/yaml/parser_validator.go
@@ -3,9 +3,11 @@ package yaml
import (
"encoding/json"
"fmt"
+ "github.com/flynn-archive/go-shlex"
"io/ioutil"
"os"
"path/filepath"
+ "strings"
"github.com/go-ozzo/ozzo-validation"
"github.com/pkg/errors"
@@ -66,6 +68,14 @@ func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.Global
if err := p.validateProjectNames(validConfig); err != nil {
return valid.RepoCfg{}, err
}
+ if validConfig.Version == 2 {
+ // The only difference between v2 and v3 is how we parse custom run
+ // commands.
+ if err := p.applyLegacyShellParsing(&validConfig); err != nil {
+ return validConfig, err
+ }
+ }
+
err = globalCfg.ValidateRepoCfg(validConfig, repoID)
return validConfig, err
}
@@ -161,3 +171,36 @@ func (p *ParserValidator) validateProjectNames(config valid.RepoCfg) error {
return nil
}
+
+// applyLegacyShellParsing changes any custom run commands in cfg to use the old
+// parsing method with shlex.Split().
+func (p *ParserValidator) applyLegacyShellParsing(cfg *valid.RepoCfg) error {
+ legacyParseF := func(s *valid.Step) error {
+ if s.StepName == "run" {
+ split, err := shlex.Split(s.RunCommand)
+ if err != nil {
+ return errors.Wrapf(err, "unable to parse %q", s.RunCommand)
+ }
+ s.RunCommand = strings.Join(split, " ")
+ }
+ return nil
+ }
+
+ for k := range cfg.Workflows {
+ w := cfg.Workflows[k]
+ for i := range w.Plan.Steps {
+ s := &w.Plan.Steps[i]
+ if err := legacyParseF(s); err != nil {
+ return err
+ }
+ }
+ for i := range w.Apply.Steps {
+ s := &w.Apply.Steps[i]
+ if err := legacyParseF(s); err != nil {
+ return err
+ }
+ }
+ cfg.Workflows[k] = w
+ }
+ return nil
+}
diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go
index 2067e567b..f46c59680 100644
--- a/server/events/yaml/parser_validator_test.go
+++ b/server/events/yaml/parser_validator_test.go
@@ -1,6 +1,7 @@
package yaml_test
import (
+ "fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -110,7 +111,7 @@ func TestParseRepoCfg(t *testing.T) {
projects:
- dir: "."
`,
- expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 2. See www.runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.html.",
+ expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html.",
},
{
description: "unsupported version",
@@ -119,7 +120,7 @@ version: 0
projects:
- dir: "."
`,
- expErr: "version: must equal 2.",
+ expErr: "version: only versions 2 and 3 are supported.",
},
{
description: "empty version",
@@ -128,17 +129,45 @@ version: ~
projects:
- dir: "."
`,
- expErr: "version: must equal 2.",
+ expErr: "version: only versions 2 and 3 are supported.",
+ },
+ {
+ description: "version 2",
+ input: `
+version: 2
+workflows:
+ custom:
+ plan:
+ steps:
+ - run: old 'shell parsing'
+`,
+ exp: valid.RepoCfg{
+ Version: 2,
+ Workflows: map[string]valid.Workflow{
+ "custom": {
+ Name: "custom",
+ Apply: valid.DefaultApplyStage,
+ Plan: valid.Stage{
+ Steps: []valid.Step{
+ {
+ StepName: "run",
+ RunCommand: "old shell parsing",
+ },
+ },
+ },
+ },
+ },
+ },
},
// Projects key.
{
description: "empty projects list",
input: `
-version: 2
+version: 3
projects:`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: nil,
Workflows: map[string]valid.Workflow{},
},
@@ -146,7 +175,7 @@ projects:`,
{
description: "project dir not set",
input: `
-version: 2
+version: 3
projects:
- `,
expErr: "projects: (0: (dir: cannot be blank.).).",
@@ -154,11 +183,11 @@ projects:
{
description: "project dir set",
input: `
-version: 2
+version: 3
projects:
- dir: .`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -178,14 +207,14 @@ projects:
{
description: "autoplan should be enabled by default",
input: `
-version: 2
+version: 3
projects:
- dir: "."
autoplan:
when_modified: ["**/*.tf*"]
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -202,12 +231,12 @@ projects:
{
description: "if workflows not defined there are none",
input: `
-version: 2
+version: 3
projects:
- dir: "."
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -224,13 +253,13 @@ projects:
{
description: "if workflows key set but with no workflows there are none",
input: `
-version: 2
+version: 3
projects:
- dir: "."
workflows: ~
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -247,7 +276,7 @@ workflows: ~
{
description: "if a plan or apply explicitly defines an empty steps key then it gets the defaults",
input: `
-version: 2
+version: 3
projects:
- dir: "."
workflows:
@@ -258,7 +287,7 @@ workflows:
steps:
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -281,7 +310,7 @@ workflows:
{
description: "project fields set except autoplan",
input: `
-version: 2
+version: 3
projects:
- dir: .
workspace: myworkspace
@@ -291,7 +320,7 @@ projects:
workflows:
myworkflow: ~`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -317,7 +346,7 @@ workflows:
{
description: "project field with autoplan",
input: `
-version: 2
+version: 3
projects:
- dir: .
workspace: myworkspace
@@ -329,7 +358,7 @@ projects:
workflows:
myworkflow: ~`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -355,7 +384,7 @@ workflows:
{
description: "project field with mergeable apply requirement",
input: `
-version: 2
+version: 3
projects:
- dir: .
workspace: myworkspace
@@ -367,7 +396,7 @@ projects:
workflows:
myworkflow: ~`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -393,7 +422,7 @@ workflows:
{
description: "project field with mergeable and approved apply requirements",
input: `
-version: 2
+version: 3
projects:
- dir: .
workspace: myworkspace
@@ -405,7 +434,7 @@ projects:
workflows:
myworkflow: ~`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -431,7 +460,7 @@ workflows:
{
description: "project dir with ..",
input: `
-version: 2
+version: 3
projects:
- dir: ..`,
expErr: "projects: (0: (dir: cannot contain '..'.).).",
@@ -441,7 +470,7 @@ projects:
{
description: "project with no config",
input: `
-version: 2
+version: 3
projects:
-`,
expErr: "projects: (0: (dir: cannot be blank.).).",
@@ -449,7 +478,7 @@ projects:
{
description: "project with no config at index 1",
input: `
-version: 2
+version: 3
projects:
- dir: "."
-`,
@@ -458,7 +487,7 @@ projects:
{
description: "project with unknown key",
input: `
-version: 2
+version: 3
projects:
- unknown: value`,
expErr: "yaml: unmarshal errors:\n line 4: field unknown not found in struct raw.Project",
@@ -466,7 +495,7 @@ projects:
{
description: "referencing workflow that doesn't exist",
input: `
-version: 2
+version: 3
projects:
- dir: .
workflow: undefined`,
@@ -475,7 +504,7 @@ projects:
{
description: "two projects with same dir/workspace without names",
input: `
-version: 2
+version: 3
projects:
- dir: .
workspace: workspace
@@ -486,7 +515,7 @@ projects:
{
description: "two projects with same dir/workspace only one with name",
input: `
-version: 2
+version: 3
projects:
- name: myname
dir: .
@@ -498,7 +527,7 @@ projects:
{
description: "two projects with same dir/workspace both with same name",
input: `
-version: 2
+version: 3
projects:
- name: myname
dir: .
@@ -511,7 +540,7 @@ projects:
{
description: "two projects with same dir/workspace with different names",
input: `
-version: 2
+version: 3
projects:
- name: myname
dir: .
@@ -520,7 +549,7 @@ projects:
dir: .
workspace: workspace`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Name: String("myname"),
@@ -547,7 +576,7 @@ projects:
{
description: "if steps are set then we parse them properly",
input: `
-version: 2
+version: 3
projects:
- dir: "."
workflows:
@@ -562,7 +591,7 @@ workflows:
- apply
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -603,7 +632,7 @@ workflows:
{
description: "we parse extra_args for the steps",
input: `
-version: 2
+version: 3
projects:
- dir: "."
workflows:
@@ -624,7 +653,7 @@ workflows:
extra_args: ["a", "b"]
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -669,7 +698,7 @@ workflows:
{
description: "custom steps are parsed",
input: `
-version: 2
+version: 3
projects:
- dir: "."
workflows:
@@ -682,7 +711,7 @@ workflows:
- run: echo apply "arg 2"
`,
exp: valid.RepoCfg{
- Version: 2,
+ Version: 3,
Projects: []valid.Project{
{
Dir: ".",
@@ -700,7 +729,7 @@ workflows:
Steps: []valid.Step{
{
StepName: "run",
- RunCommand: []string{"echo", "plan hi"},
+ RunCommand: "echo \"plan hi\"",
},
},
},
@@ -708,7 +737,7 @@ workflows:
Steps: []valid.Step{
{
StepName: "run",
- RunCommand: []string{"echo", "apply", "arg 2"},
+ RunCommand: "echo apply \"arg 2\"",
},
},
},
@@ -745,7 +774,7 @@ func TestParseRepoCfg_GlobalValidation(t *testing.T) {
defer cleanup()
repoCfg := `
-version: 2
+version: 3
projects:
- dir: .
workflow: custom
@@ -773,7 +802,7 @@ func TestParseGlobalCfg(t *testing.T) {
Steps: []valid.Step{
{
StepName: "run",
- RunCommand: []string{"custom", "command"},
+ RunCommand: "custom command",
},
{
StepName: "init",
@@ -788,7 +817,7 @@ func TestParseGlobalCfg(t *testing.T) {
Steps: []valid.Step{
{
StepName: "run",
- RunCommand: []string{"custom", "command"},
+ RunCommand: "custom command",
},
{
StepName: "apply",
@@ -1005,7 +1034,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
},
{
StepName: "run",
- RunCommand: []string{"custom", "plan"},
+ RunCommand: "custom plan",
},
},
},
@@ -1013,7 +1042,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
Steps: []valid.Step{
{
StepName: "run",
- RunCommand: []string{"my", "custom", "command"},
+ RunCommand: "my custom command",
},
},
},
@@ -1104,6 +1133,68 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
}
}
+// Test legacy shell parsing vs v3 parsing.
+func TestParseRepoCfg_V2ShellParsing(t *testing.T) {
+ cases := []struct {
+ in string
+ expV2 string
+ expV2Err string
+ }{
+ {
+ in: "echo a b",
+ expV2: "echo a b",
+ },
+ {
+ in: "echo 'a b'",
+ expV2: "echo a b",
+ },
+ {
+ in: "echo 'a b",
+ expV2Err: "unable to parse \"echo 'a b\": EOF found when expecting closing quote.",
+ },
+ {
+ in: `mkdir a/b/c || printf \'your main.tf file does not provide default region.\\ncheck\'`,
+ expV2: `mkdir a/b/c || printf 'your main.tf file does not provide default region.\ncheck'`,
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.in, func(t *testing.T) {
+ v2Dir, cleanup2 := TempDir(t)
+ defer cleanup2()
+ v3Dir, cleanup3 := TempDir(t)
+ defer cleanup3()
+ v2Path := filepath.Join(v2Dir, "atlantis.yaml")
+ v3Path := filepath.Join(v3Dir, "atlantis.yaml")
+ cfg := fmt.Sprintf(`workflows:
+ custom:
+ plan:
+ steps:
+ - run: %s
+ apply:
+ steps:
+ - run: %s`, c.in, c.in)
+ Ok(t, ioutil.WriteFile(v2Path, []byte("version: 2\n"+cfg), 0600))
+ Ok(t, ioutil.WriteFile(v3Path, []byte("version: 3\n"+cfg), 0600))
+
+ p := &yaml.ParserValidator{}
+ v2Cfg, err := p.ParseRepoCfg(v2Dir, valid.NewGlobalCfg(true, false, false), "")
+ if c.expV2Err != "" {
+ ErrEquals(t, c.expV2Err, err)
+ } else {
+ Ok(t, err)
+ Equals(t, c.expV2, v2Cfg.Workflows["custom"].Plan.Steps[0].RunCommand)
+ Equals(t, c.expV2, v2Cfg.Workflows["custom"].Apply.Steps[0].RunCommand)
+ }
+
+ v3Cfg, err := p.ParseRepoCfg(v3Dir, valid.NewGlobalCfg(true, false, false), "")
+ Ok(t, err)
+ Equals(t, c.in, v3Cfg.Workflows["custom"].Plan.Steps[0].RunCommand)
+ Equals(t, c.in, v3Cfg.Workflows["custom"].Apply.Steps[0].RunCommand)
+ })
+ }
+}
+
// String is a helper routine that allocates a new string value
// to store v and returns a pointer to it.
func String(v string) *string { return &v }
diff --git a/server/events/yaml/raw/repo_cfg.go b/server/events/yaml/raw/repo_cfg.go
index d17304953..57b861044 100644
--- a/server/events/yaml/raw/repo_cfg.go
+++ b/server/events/yaml/raw/repo_cfg.go
@@ -22,10 +22,10 @@ func (r RepoCfg) Validate() error {
equals2 := func(value interface{}) error {
asIntPtr := value.(*int)
if asIntPtr == nil {
- return errors.New("is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 2. See www.runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.html")
+ return errors.New("is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html")
}
- if *asIntPtr != 2 {
- return errors.New("must equal 2")
+ if *asIntPtr != 2 && *asIntPtr != 3 {
+ return errors.New("only versions 2 and 3 are supported")
}
return nil
}
diff --git a/server/events/yaml/raw/repo_cfg_test.go b/server/events/yaml/raw/repo_cfg_test.go
index 2da1207ef..059c3e9ce 100644
--- a/server/events/yaml/raw/repo_cfg_test.go
+++ b/server/events/yaml/raw/repo_cfg_test.go
@@ -46,7 +46,7 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
expErr: "yaml: unmarshal errors:\n line 1: field invalid not found in struct raw.RepoCfg",
},
{
- description: "version set",
+ description: "version set to 2",
input: "version: 2",
exp: raw.RepoCfg{
Version: Int(2),
@@ -54,6 +54,15 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
Workflows: nil,
},
},
+ {
+ description: "version set to 3",
+ input: "version: 3",
+ exp: raw.RepoCfg{
+ Version: Int(3),
+ Projects: nil,
+ Workflows: nil,
+ },
+ },
{
description: "projects key without value",
input: "projects:",
@@ -94,7 +103,7 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
},
{
description: "automerge not a boolean",
- input: "version: 2\nautomerge: notabool",
+ input: "version: 3\nautomerge: notabool",
exp: raw.RepoCfg{
Version: nil,
Projects: nil,
@@ -105,7 +114,7 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
{
description: "should use values if set",
input: `
-version: 2
+version: 3
automerge: true
projects:
- dir: mydir
@@ -123,7 +132,7 @@ workflows:
apply:
steps: []`,
exp: raw.RepoCfg{
- Version: Int(2),
+ Version: Int(3),
Automerge: Bool(true),
Projects: []raw.Project{
{
@@ -176,14 +185,14 @@ func TestConfig_Validate(t *testing.T) {
input: raw.RepoCfg{
Version: nil,
},
- expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 2. See www.runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.html.",
+ expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 3. See www.runatlantis.io/docs/upgrading-atlantis-yaml.html.",
},
{
- description: "version not 1",
+ description: "version not 2 or 3",
input: raw.RepoCfg{
Version: Int(1),
},
- expErr: "version: must equal 2.",
+ expErr: "version: only versions 2 and 3 are supported.",
},
}
validation.ErrorTag = "yaml"
diff --git a/server/events/yaml/raw/step.go b/server/events/yaml/raw/step.go
index 0f6c866b2..b553aead6 100644
--- a/server/events/yaml/raw/step.go
+++ b/server/events/yaml/raw/step.go
@@ -7,7 +7,6 @@ import (
"sort"
"strings"
- "github.com/flynn-archive/go-shlex"
"github.com/go-ozzo/ozzo-validation"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
)
@@ -108,14 +107,10 @@ func (s Step) Validate() error {
return fmt.Errorf("step element can only contain a single key, found %d: %s",
len(keys), strings.Join(keys, ","))
}
- for stepName, args := range elem {
+ for stepName := range elem {
if stepName != RunStepName {
return fmt.Errorf("%q is not a valid step type", stepName)
}
- _, err := shlex.Split(args)
- if err != nil {
- return fmt.Errorf("unable to parse as shell command: %s", err)
- }
}
return nil
}
@@ -157,12 +152,9 @@ func (s Step) ToValid() valid.Step {
// After validation we assume there's only one key and it's a valid
// step name so we just use the first one.
for _, v := range s.StringVal {
- // We ignore the error here because it should have been checked in
- // Validate().
- split, _ := shlex.Split(v)
return valid.Step{
StepName: RunStepName,
- RunCommand: split,
+ RunCommand: v,
}
}
}
diff --git a/server/events/yaml/raw/step_test.go b/server/events/yaml/raw/step_test.go
index 32e040305..d4f9723bf 100644
--- a/server/events/yaml/raw/step_test.go
+++ b/server/events/yaml/raw/step_test.go
@@ -268,13 +268,14 @@ func TestStep_Validate(t *testing.T) {
expErr: "built-in steps only support a single extra_args key, found \"invalid\" in step init",
},
{
+ // For atlantis.yaml v2, this wouldn't parse, but now there should
+ // be no error.
description: "unparseable shell command",
input: raw.Step{
StringVal: map[string]string{
"run": "my 'c",
},
},
- expErr: "unable to parse as shell command: EOF found when expecting closing quote.",
},
}
for _, c := range cases {
@@ -373,7 +374,7 @@ func TestStep_ToValid(t *testing.T) {
},
exp: valid.Step{
StepName: "run",
- RunCommand: []string{"my", "run command"},
+ RunCommand: "my 'run command'",
},
},
}
diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go
index 2bb299d18..6af6ac5e6 100644
--- a/server/events/yaml/valid/global_cfg.go
+++ b/server/events/yaml/valid/global_cfg.go
@@ -45,6 +45,7 @@ type MergedProjectCfg struct {
Name string
AutoplanEnabled bool
TerraformVersion *version.Version
+ RepoCfgVersion int
}
// DefaultApplyStage is the Atlantis default apply stage.
@@ -178,6 +179,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
Name: proj.GetName(),
AutoplanEnabled: proj.Autoplan.Enabled,
TerraformVersion: proj.TerraformVersion,
+ RepoCfgVersion: rCfg.Version,
}
}
diff --git a/server/events/yaml/valid/repo_cfg.go b/server/events/yaml/valid/repo_cfg.go
index 47cc9b52c..cd953b6ba 100644
--- a/server/events/yaml/valid/repo_cfg.go
+++ b/server/events/yaml/valid/repo_cfg.go
@@ -6,8 +6,7 @@ import "github.com/hashicorp/go-version"
// RepoCfg is the atlantis.yaml config after it's been parsed and validated.
type RepoCfg struct {
- // Version is the version of the atlantis YAML file. Will always be equal
- // to 2.
+ // Version is the version of the atlantis YAML file.
Version int
Projects []Project
Workflows map[string]Workflow
@@ -75,7 +74,7 @@ type Stage struct {
type Step struct {
StepName string
ExtraArgs []string
- RunCommand []string
+ RunCommand string
}
type Workflow struct {
diff --git a/server/testfixtures/test-repos/automerge/atlantis.yaml b/server/testfixtures/test-repos/automerge/atlantis.yaml
index ffc80acf5..a113da5fd 100644
--- a/server/testfixtures/test-repos/automerge/atlantis.yaml
+++ b/server/testfixtures/test-repos/automerge/atlantis.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 3
automerge: true
projects:
- dir: dir1
diff --git a/server/testfixtures/test-repos/modules-yaml/atlantis.yaml b/server/testfixtures/test-repos/modules-yaml/atlantis.yaml
index e38c90f56..6a453315a 100644
--- a/server/testfixtures/test-repos/modules-yaml/atlantis.yaml
+++ b/server/testfixtures/test-repos/modules-yaml/atlantis.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 3
projects:
- dir: staging
autoplan:
diff --git a/server/testfixtures/test-repos/simple-yaml/atlantis.yaml b/server/testfixtures/test-repos/simple-yaml/atlantis.yaml
index 2ffd1cba8..37cb6c730 100644
--- a/server/testfixtures/test-repos/simple-yaml/atlantis.yaml
+++ b/server/testfixtures/test-repos/simple-yaml/atlantis.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 3
projects:
- dir: .
workspace: default
diff --git a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/atlantis.yaml b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/atlantis.yaml
index 8dbfe353e..082854ae6 100644
--- a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/atlantis.yaml
+++ b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/atlantis.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 3
projects:
- dir: .
name: default
diff --git a/server/testfixtures/test-repos/tfvars-yaml/atlantis.yaml b/server/testfixtures/test-repos/tfvars-yaml/atlantis.yaml
index a6f517140..960c9c871 100644
--- a/server/testfixtures/test-repos/tfvars-yaml/atlantis.yaml
+++ b/server/testfixtures/test-repos/tfvars-yaml/atlantis.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 3
projects:
- dir: .
name: default