Skip to content

Commit

Permalink
feat: customize atlantis.yaml file name in server side config
Browse files Browse the repository at this point in the history
  • Loading branch information
krrrr38 committed Dec 14, 2022
1 parent bab5c62 commit d2f3ce9
Show file tree
Hide file tree
Showing 29 changed files with 579 additions and 272 deletions.
4 changes: 2 additions & 2 deletions runatlantis.io/docs/repo-level-atlantis-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ keys by setting the `allowed_overrides` key there. See the [Server Side Repo Con
more details.

**Notes**
* `atlantis.yaml` files must be placed at the root of the repo
* The only supported name is `atlantis.yaml`. Not `atlantis.yml` or `.atlantis.yaml`.
* By default, repo root `atlantis.yaml` file is used.
* You can change this behaviour by setting [Server Side Repo Config](server-side-repo-config.html)

::: danger DANGER
Atlantis uses the `atlantis.yaml` version from the pull request, similar to other
Expand Down
7 changes: 6 additions & 1 deletion runatlantis.io/docs/server-side-repo-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ repos:
# By default, all branches are matched
branch: /.*/

# repo_config_file specifies which repo config file to use for this repo.
# By default, atlantis.yaml is used.
repo_config_file: path/to/atlantis.yaml

# apply_requirements sets the Apply Requirements for all repos that match.
apply_requirements: [approved, mergeable]

Expand Down Expand Up @@ -168,7 +172,7 @@ repos:
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
# atlantis.yaml in the repo root or set repo_config_file in repos.yaml
version: 3
projects:
- dir: .
Expand Down Expand Up @@ -400,6 +404,7 @@ If you set a workflow with the key `default`, it will override this.
|-------------------------------|----------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | string | none | yes | Value can be a regular expression when specified as /<regex>/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
| 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. |
| 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. |
| 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` |
Expand Down
37 changes: 29 additions & 8 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ func TestGitHubWorkflow(t *testing.T) {
Description string
// RepoDir is relative to testfixtures/test-repos.
RepoDir string
// RepoConfigFile is path for atlantis.yaml
RepoConfigFile string
// ModifiedFiles are the list of files that have been modified in this
// pull request.
ModifiedFiles []string
Expand Down Expand Up @@ -218,6 +220,24 @@ func TestGitHubWorkflow(t *testing.T) {
{"exp-output-merge.txt"},
},
},
{
Description: "custom repo config file",
RepoDir: "repo-config-file",
RepoConfigFile: "infrastructure/custom-name-atlantis.yaml",
ModifiedFiles: []string{
"infrastructure/staging/main.tf",
"infrastructure/production/main.tf",
},
ExpAutoplan: true,
Comments: []string{
"atlantis apply",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-apply.txt"},
{"exp-output-merge.txt"},
},
},
{
Description: "modules staging only",
RepoDir: "modules",
Expand Down Expand Up @@ -393,7 +413,7 @@ func TestGitHubWorkflow(t *testing.T) {
userConfig = server.UserConfig{}
userConfig.DisableApply = c.DisableApply

ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir)
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, c.RepoConfigFile)
// Set the repo to be cloned through the testing backdoor.
repoDir, headSHA := initializeRepo(t, c.RepoDir)
atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir)
Expand Down Expand Up @@ -542,7 +562,7 @@ func TestSimlpleWorkflow_terraformLockFile(t *testing.T) {
userConfig = server.UserConfig{}
userConfig.DisableApply = true

ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir)
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, "")
// Set the repo to be cloned through the testing backdoor.
repoDir, headSHA := initializeRepo(t, c.RepoDir)

Expand Down Expand Up @@ -785,15 +805,15 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
userConfig.EnablePolicyChecksFlag = true
userConfig.QuietPolicyChecks = c.ExpQuietPolicyChecks

ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir)
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, "")

// Set the repo to be cloned through the testing backdoor.
repoDir, headSHA := initializeRepo(t, c.RepoDir)
atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir)

// Setup test dependencies.
w := httptest.NewRecorder()
When(vcsClient.PullIsMergeable(AnyRepo(), matchers.AnyModelsPullRequest(), "atlantis-test")).ThenReturn(true, nil)
When(vcsClient.PullIsMergeable(AnyRepo(), matchers.AnyModelsPullRequest(), EqString("atlantis-test"))).ThenReturn(true, nil)
When(vcsClient.PullIsApproved(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(models.ApprovalStatus{
IsApproved: true,
}, nil)
Expand Down Expand Up @@ -862,7 +882,7 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
}
}

func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {
func setupE2E(t *testing.T, repoDir, repoConfigFile string) (events_controllers.VCSEventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {
allowForkPRs := false
dataDir, binDir, cacheDir := mkSubDirs(t)

Expand Down Expand Up @@ -916,9 +936,10 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl
parser := &config.ParserValidator{}

globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: true,
MergeableReq: false,
ApprovedReq: false,
RepoConfigFile: repoConfigFile,
AllowRepoCfg: true,
MergeableReq: false,
ApprovedReq: false,
PreWorkflowHooks: []*valid.WorkflowHook{
{
StepName: "global_hook",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Ran Apply for 2 projects:

1. dir: `infrastructure/production` workspace: `default`
1. dir: `infrastructure/staging` workspace: `default`

### 1. dir: `infrastructure/production` workspace: `default`
```diff
null_resource.production[0]: Creating...
null_resource.production[0]: Creation complete after *s [id=*******************]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


```

---
### 2. dir: `infrastructure/staging` workspace: `default`
```diff
null_resource.staging[0]: Creating...
null_resource.staging[0]: Creation complete after *s [id=*******************]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.


```

---

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
Ran Plan for 2 projects:

1. dir: `infrastructure/staging` workspace: `default`
1. dir: `infrastructure/production` workspace: `default`

### 1. dir: `infrastructure/staging` workspace: `default`
<details><summary>Show Output</summary>

```diff

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# null_resource.staging[0] will be created
+ resource "null_resource" "staging" {
+ id = (known after apply)
}

Plan: 1 to add, 0 to change, 0 to destroy.


```

* :arrow_forward: To **apply** this plan, comment:
* `atlantis apply -d infrastructure/staging`
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d infrastructure/staging`
</details>
Plan: 1 to add, 0 to change, 0 to destroy.

---
### 2. dir: `infrastructure/production` workspace: `default`
<details><summary>Show Output</summary>

```diff

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# null_resource.production[0] will be created
+ resource "null_resource" "production" {
+ id = (known after apply)
}

Plan: 1 to add, 0 to change, 0 to destroy.


```

* :arrow_forward: To **apply** this plan, comment:
* `atlantis apply -d infrastructure/production`
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* `atlantis plan -d infrastructure/production`
</details>
Plan: 1 to add, 0 to change, 0 to destroy.

---
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
* `atlantis apply`
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
* `atlantis unlock`
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:

- dir: `infrastructure/production` workspace: `default`
- dir: `infrastructure/staging` workspace: `default`
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: 3
projects:
- dir: infrastructure/staging
- dir: infrastructure/production
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "null_resource" "production" {
count = "1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
resource "null_resource" "staging" {
count = "1"
}
14 changes: 6 additions & 8 deletions server/core/config/parser_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,22 @@ import (
yaml "gopkg.in/yaml.v2"
)

// AtlantisYAMLFilename is the name of the config file for each repo.
const AtlantisYAMLFilename = "atlantis.yaml"

// ParserValidator parses and validates server-side repo config files and
// repo-level atlantis.yaml files.
type ParserValidator struct{}

// HasRepoCfg returns true if there is a repo config (atlantis.yaml) file
// for the repo at absRepoDir.
// Returns an error if for some reason it can't read that directory.
func (p *ParserValidator) HasRepoCfg(absRepoDir string) (bool, error) {
func (p *ParserValidator) HasRepoCfg(absRepoDir, repoConfigFile string) (bool, error) {
// Checks for a config file with an invalid extension (atlantis.yml)
const invalidExtensionFilename = "atlantis.yml"
_, err := os.Stat(p.repoCfgPath(absRepoDir, invalidExtensionFilename))
if err == nil {
return false, errors.Errorf("found %q as config file; rename using the .yaml extension - %q", invalidExtensionFilename, AtlantisYAMLFilename)
return false, errors.Errorf("found %q as config file; rename using the .yaml extension", invalidExtensionFilename)
}

_, err = os.Stat(p.repoCfgPath(absRepoDir, AtlantisYAMLFilename))
_, err = os.Stat(p.repoCfgPath(absRepoDir, repoConfigFile))
if os.IsNotExist(err) {
return false, nil
}
Expand All @@ -44,12 +41,13 @@ func (p *ParserValidator) HasRepoCfg(absRepoDir string) (bool, error) {
// repo at absRepoDir.
// If there was no config file, it will return an os.IsNotExist(error).
func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {
configFile := p.repoCfgPath(absRepoDir, AtlantisYAMLFilename)
repoConfigFile := globalCfg.RepoConfigFile(repoID)
configFile := p.repoCfgPath(absRepoDir, repoConfigFile)
configData, err := os.ReadFile(configFile) // nolint: gosec

if err != nil {
if !os.IsNotExist(err) {
return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename)
return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", repoConfigFile)
}
// Don't wrap os.IsNotExist errors because we want our callers to be
// able to detect if it's a NotExist err.
Expand Down
26 changes: 20 additions & 6 deletions server/core/config/parser_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,28 @@ var globalCfg = valid.NewGlobalCfgFromArgs(globalCfgArgs)

func TestHasRepoCfg_DirDoesNotExist(t *testing.T) {
r := config.ParserValidator{}
exists, err := r.HasRepoCfg("/not/exist")
exists, err := r.HasRepoCfg("/not/exist", "unused.yaml")
Ok(t, err)
Equals(t, false, exists)
}

func TestHasRepoCfg_FileDoesNotExist(t *testing.T) {
tmpDir := t.TempDir()
r := config.ParserValidator{}
exists, err := r.HasRepoCfg(tmpDir)
exists, err := r.HasRepoCfg(tmpDir, "not-exist.yaml")
Ok(t, err)
Equals(t, false, exists)
}

func TestHasRepoCfg_InvalidFileExtension(t *testing.T) {
tmpDir := t.TempDir()
_, err := os.Create(filepath.Join(tmpDir, "atlantis.yml"))
repoConfigFile := "atlantis.yml"
_, err := os.Create(filepath.Join(tmpDir, repoConfigFile))
Ok(t, err)

r := config.ParserValidator{}
_, err = r.HasRepoCfg(tmpDir)
ErrContains(t, "found \"atlantis.yml\" as config file; rename using the .yaml extension - \"atlantis.yaml\"", err)
_, err = r.HasRepoCfg(tmpDir, repoConfigFile)
ErrContains(t, "found \"atlantis.yml\" as config file; rename using the .yaml extension", err)
}

func TestParseRepoCfg_DirDoesNotExist(t *testing.T) {
Expand Down Expand Up @@ -1212,6 +1213,18 @@ func TestParseGlobalCfg(t *testing.T) {
branch: /?/`,
expErr: "repos: (0: (branch: parsing: /?/: error parsing regexp: missing argument to repetition operator: `?`.).).",
},
"invalid repo_config_file which starts with a slash": {
input: `repos:
- id: /.*/
repo_config_file: /etc/passwd`,
expErr: "repos: (0: (repo_config_file: must not starts with a slash '/'.).).",
},
"invalid repo_config_file which contains parent directory path": {
input: `repos:
- id: /.*/
repo_config_file: ../../etc/passwd`,
expErr: "repos: (0: (repo_config_file: must not contains parent directory path like '../'.).).",
},
"workflow doesn't exist": {
input: `repos:
- id: /.*/
Expand Down Expand Up @@ -1300,7 +1313,7 @@ workflows:
input: `
repos:
- id: github.com/owner/repo
repo_config_file: "path/to/atlantis.yaml"
apply_requirements: [approved, mergeable]
pre_workflow_hooks:
- run: custom workflow command
Expand Down Expand Up @@ -1345,6 +1358,7 @@ policies:
defaultCfg.Repos[0],
{
ID: "github.com/owner/repo",
RepoConfigFile: "path/to/atlantis.yaml",
ApplyRequirements: []string{"approved", "mergeable"},
PreWorkflowHooks: preWorkflowHooks,
Workflow: &customWorkflow1,
Expand Down
Loading

0 comments on commit d2f3ce9

Please sign in to comment.