Skip to content

Commit

Permalink
feat: customize atlantis.yaml file name in server side config (#2798)
Browse files Browse the repository at this point in the history
* feat: customize atlantis.yaml file name in server side config

* Update server/events/vcs/bitbucketcloud/client.go

* Update server/events/vcs/bitbucketserver/client.go

* Update server/events/vcs/not_configured_vcs_client.go

* Update server/events/vcs/azuredevops_client.go

* docs: repo_config_file multiple atlantis servers usecase example

* docs: multiple atlantis server usage which show how to call one atlantis server

* Update runatlantis.io/docs/server-side-repo-config.md

* Apply suggestions from code review

* Update runatlantis.io/docs/server-side-repo-config.md

Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com>
  • Loading branch information
krrrr38 and nitrocode committed Dec 19, 2022
1 parent 01a9a5f commit 08e9c5a
Show file tree
Hide file tree
Showing 29 changed files with 627 additions and 272 deletions.
4 changes: 2 additions & 2 deletions runatlantis.io/docs/repo-level-atlantis-yaml.md
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
55 changes: 54 additions & 1 deletion runatlantis.io/docs/server-side-repo-config.md
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 @@ -357,6 +361,54 @@ workflows:
See [Custom Workflows](custom-workflows.html) for more details on writing
custom workflows.

### Multiple Atlantis Servers Handle The Same Repository
Running multiple Atlantis servers to handle the same repository can be done to separate permissions for each Atlantis server.
In this case, a different [atlantis.yaml](repo-level-atlantis-yaml.html) repository config file can be used by using different `repos.yaml` files.

For example, consider a situation where a separate `production-server` atlantis uses repo config `atlantis-production.yaml` and `staging-server` atlantis uses repo config `atlantis-staging.yaml`.

Firstly, deploy 2 Atlantis servers, `production-server` and `staging-server`.
Each server has different permissions and a different `repos.yaml` file.
The `repos.yaml` contains `repo_config_file` key to specify the repository atlantis config file path.

```yaml
# repos.yaml
repos:
- id: /.*/
# for production-server
repo_config_file: atlantis-production.yaml
# for staging-server
# repo_config_file: atlantis-staging.yaml
```

Then, create `atlantis-production.yaml` and `atlantis-staging.yaml` files in the repository.
See the configuration examples in [atlantis.yaml](repo-level-atlantis-yaml.html).

```yaml
# atlantis-production.yaml
version: 3
projects:
- name: project
branch: /production/
dir: infrastructure/production
---
# atlantis-staging.yaml
version: 3
projects:
- name: project
branch: /staging/
dir: infrastructure/staging
```

Now, 2 webhook URLs can be setup for the repository, which send events to `production-server` and `staging-server` respectively.
Each servers handle different repository config files.

:::tip Notes
* If `no projects` comments are annoying, set [--silence-no-projects](server-configuration.html#silence-no-projects).
* The command trigger executable name can be reconfigured from `atlantis` to something else by setting [Executable Name](server-configuration.html#executable-name).
* When using different atlantis server vcs users such as `@atlantis-staging`, the comment `@atlantis-staging plan` can be used instead `atlantis plan` to call `staging-server` only.
:::

## Reference

### Top-Level Keys
Expand Down Expand Up @@ -400,6 +452,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 /&lt;regex&gt;/ 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. 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. |
| 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
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 @@ -917,9 +937,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
@@ -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.


```

---

@@ -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`
@@ -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`
@@ -0,0 +1,4 @@
version: 3
projects:
- dir: infrastructure/staging
- dir: infrastructure/production
@@ -0,0 +1,3 @@
resource "null_resource" "production" {
count = "1"
}
@@ -0,0 +1,3 @@
resource "null_resource" "staging" {
count = "1"
}
14 changes: 6 additions & 8 deletions server/core/config/parser_validator.go
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

0 comments on commit 08e9c5a

Please sign in to comment.