Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: customize atlantis.yaml file name in server side config #2798

Merged
merged 10 commits into from
Dec 19, 2022
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
55 changes: 54 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 @@ -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).
krrrr38 marked this conversation as resolved.
Show resolved Hide resolved
* 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 /<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. 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
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 @@ -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
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
Loading