Added new --repo-config option and deprecated --allow-repo-config

This enables atlantis.yaml in all repos, but by default restricts
certain sensitive keys from being used.

The keys apply_requirements, workflow, and workflows can only be
specified in an atlantis.yaml file if explicitly allowed by a
server side repo config.

The repo config file provides the ability to specify a default set of
workflows, and default values for apply_requirements and workflow to use
use on a per repo basis.  It also supports applying to a collection of
repos by using regex to match a repo name.

If more than one repo name matches, the values from last repo matched
are used.

This deprecates the --allow-repo-config option
This commit is contained in:
John Julien
2019-02-15 15:04:51 -06:00
committed by Luke Kysow
parent a793607ab2
commit 1fcdaba719
19 changed files with 1269 additions and 251 deletions

View File

@@ -58,6 +58,7 @@ const (
GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec
LogLevelFlag = "log-level"
PortFlag = "port"
RepoConfigFlag = "repo-config"
RepoWhitelistFlag = "repo-whitelist"
RequireApprovalFlag = "require-approval"
RequireMergeableFlag = "require-mergeable"
@@ -168,6 +169,10 @@ var stringFlags = []stringFlag{
description: "Log level. Either debug, info, warn, or error.",
defaultValue: DefaultLogLevel,
},
{
name: RepoConfigFlag,
description: "Path to a repo config file, used to configure how atlantis.yaml will behave on repos. Repos can be specified as an exact string or using regular expressions",
},
{
name: RepoWhitelistFlag,
description: "Comma separated list of repositories that Atlantis will operate on. " +
@@ -211,6 +216,7 @@ var boolFlags = []boolFlag{
" Should only be enabled in a trusted environment since it enables a pull request to run arbitrary commands" +
" on the Atlantis server.",
defaultValue: false,
deprecated: fmt.Sprintf("use --%s to allow sensitive keys in atlantis.yaml", RepoConfigFlag),
},
{
name: AutomergeFlag,
@@ -245,16 +251,19 @@ type stringFlag struct {
name string
description string
defaultValue string
deprecated string
}
type intFlag struct {
name string
description string
defaultValue int
deprecated string
}
type boolFlag struct {
name string
description string
defaultValue bool
deprecated string
}
// ServerCmd is an abstraction that helps us test. It allows
@@ -330,6 +339,9 @@ func (s *ServerCmd) Init() *cobra.Command {
usage = fmt.Sprintf("%s (default \"%s\")", usage, f.defaultValue)
}
c.Flags().String(f.name, "", usage+"\n")
if f.deprecated != "" {
c.Flags().MarkDeprecated(f.name, f.deprecated) // nolint: errcheck
}
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name)) // nolint: errcheck
}
@@ -340,12 +352,18 @@ func (s *ServerCmd) Init() *cobra.Command {
usage = fmt.Sprintf("%s (default %d)", usage, f.defaultValue)
}
c.Flags().Int(f.name, 0, usage+"\n")
if f.deprecated != "" {
c.Flags().MarkDeprecated(f.name, f.deprecated) // nolint: errcheck
}
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name)) // nolint: errcheck
}
// Set bool flags.
for _, f := range boolFlags {
c.Flags().Bool(f.name, f.defaultValue, f.description+"\n")
if f.deprecated != "" {
c.Flags().MarkDeprecated(f.name, f.deprecated) // nolint: errcheck
}
s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name)) // nolint: errcheck
}
@@ -438,6 +456,9 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
if (userConfig.SSLKeyFile == "") != (userConfig.SSLCertFile == "") {
return fmt.Errorf("--%s and --%s are both required for ssl", SSLKeyFileFlag, SSLCertFileFlag)
}
if userConfig.AllowRepoConfig && userConfig.RepoConfig != "" {
return fmt.Errorf("You cannot use both --%s and --%s together. --%s is deprecated and will be removed in a later version, you should use --%s instead", AllowRepoConfigFlag, RepoConfigFlag, AllowRepoConfigFlag, RepoConfigFlag)
}
// The following combinations are valid.
// 1. github user and token set

View File

@@ -914,6 +914,16 @@ func TestExecute_BitbucketServerBaseURLPort(t *testing.T) {
Equals(t, "http://mydomain.com:7990", passedConfig.BitbucketBaseURL)
}
// Cannot use both --allow-repo-config and --repo-config
func TestExecute_AllowRepoConfigWithAllowRestrictedRepoConfig(t *testing.T) {
c := setup(map[string]interface{}{
cmd.AllowRepoConfigFlag: true,
cmd.RepoConfigFlag: "somefile",
})
err := c.Execute()
ErrEquals(t, "You cannot use both --allow-repo-config and --repo-config together. --allow-repo-config is deprecated and will be removed in a later version, you should use --repo-config instead", err)
}
func setup(flags map[string]interface{}) *cobra.Command {
vipr := viper.New()
for k, v := range flags {

View File

@@ -69,6 +69,7 @@ module.exports = {
collapsable: true,
children: [
['customizing-atlantis', 'Overview'],
'repos-yaml-reference',
'atlantis-yaml-reference',
'upgrading-atlantis-yaml-to-version-2',
'apply-requirements',

View File

@@ -20,7 +20,21 @@ by at least one person other than the author.
#### Usage
You can set the `approved` requirement by:
1. Passing the `--require-approval` flag to `atlantis server` or
1. Creating an `atlantis.yaml` file with the `apply_requirements` key:
1. Creating a `repos.yaml` file with the `apply_requirements` key:
```yaml
repos:
- id: /.*/
apply_requirements: [approved]
```
1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config:
#### repos.yaml
```yaml
repos:
- id: /.*/
allowed_overrides: [apply_requirements]
```
#### atlantis.yaml
```yaml
version: 2
projects:
@@ -47,14 +61,29 @@ The `mergeable` requirement will prevent applies unless a pull request is able t
#### Usage
You can set the `mergeable` requirement by:
1. Passing the `--require-mergeable` flag to `atlantis server` or
1. Creating an `atlantis.yaml` file with the `apply_requirements` key:
1. Creating a `repos.yaml` file with the `apply_requirements` key:
```yaml
repos:
- id: /.*/
apply_requirements: [mergeable]
```
1. Or by allowing an `atlantis.yaml` file to specify the `apply_requirements` key in your `repos.yaml` config:
#### repos.yaml
```yaml
repos:
- id: /.*/
allowed_overrides: [apply_requirements]
```
#### atlantis.yaml
```yaml
version: 2
projects:
- dir: .
apply_requirements: [mergeable]
```
```
#### Meaning
Each VCS provider has a different concept of "mergeability":
#### GitHub
@@ -92,18 +121,47 @@ If you need a specific check, please
[open an issue](https://github.com/runatlantis/atlantis/issues/new).
## Setting Apply Requirements
As mentioned above, you can set apply requirements via flags or `atlantis.yaml`.
As mentioned above, you can set apply requirements via flags, in `repos.yaml`, or in `atlantis.yaml` if `repos.yaml`
allows the override.
### Flags Override
Flags **override** any `atlantis.yaml` settings so they are equivalent to always
Flags **override** any `repos.yaml` or `atlantis.yaml` settings so they are equivalent to always
having that apply requirement set.
### Project-Specific Settings
If you only want some projects/repos to have apply requirements, then you must
1. Not set the `--require-approval` or `--require-mergeable` flags, since those
will override any `atlantis.yaml` settings
1. Specify which projects have which requirements via an `atlantis.yaml` file.
will override any `repos.yaml` or `atlantis.yaml` settings
1. Specifying which repos have which requirements via the `repos.yaml` file.
```yaml
repos:
- id: /.*/
apply_requirements: [approved]
# Regex that defaults all repos to requiring approval
- id: /github.com/runatlantis/.*/
# Regex to match any repo under the atlantis namespace, and not require approval
# except for repos that might match later in the chain
apply_requirements: []
- id: github.com/runatlantis/atlantis
apply_requirements: [approved]
# Exact string match of the github.com/runatlantis/atlantis repo
# that sets apply_requirements to approved
```
1. Specify which projects have which requirements via an `atlantis.yaml` file, and allowing
`apply_requirements` to be set in in `atlantis.yaml` by the server side `repos.yaml`
config.
For example if I have two directories, `staging` and `production`, I might use:
#### repos.yaml
```yaml
repos:
- id: /.*/
allowed_overrides: [apply_requirements]
# Allow any repo to specify apply_requirements in atlantis.yaml
```
#### atlatis.yaml
```yaml
version: 2
projects:

View File

@@ -10,8 +10,12 @@ See [www.runatlantis.io/guide/atlantis-yaml-use-cases.html](../guide/atlantis-ya
:::
## Enabling atlantis.yaml
The atlantis server must be running with `--allow-repo-config` to allow Atlantis
to use `atlantis.yaml` files.
By default all repos are allowed to have an `atlantis.yaml` file, but not all of the keys are enabled by default due to
the sensitive nature of some keys.
Restricted keys can be set in the server side `repos.yaml` file, and you can enable `atlantis.yaml` to override restricted
keys by setting `allowed_overrides` in the `repos.yaml`. See the [repos.yaml reference](repos-yaml-reference.html) for
more information.
## Example Using All Keys
```yaml
@@ -58,11 +62,10 @@ likely hold your highest privilege credentials.
The risk is increased because Atlantis uses the `atlantis.yaml` file from the
pull request so anyone that can submit a pull request can submit a malicious file.
As such, **`atlantis.yaml` files should only be enabled in a trusted environment**.
::: danger
It should be noted that `atlantis apply` itself could be exploited if run on a malicious terraform file. See [Security](security.html#exploits).
:::
By default, the keys that are sensitive in nature are restricted from being used in the `atlantis.yaml` file.
Restricted keys can be set in the server side `repos.yaml` file, and you can enable `atlantis.yaml` to override restricted
keys by setting `allowed_overrides` in the `repos.yaml`. See the [repos.yaml reference](repos-yaml-reference.html) for
more information.
## Reference
### Top-Level Keys
@@ -72,12 +75,12 @@ automerge:
projects:
workflows:
```
| Key | Type | Default | Required | Description |
| --------- | ---------------------------------------------------------------- | ------- | -------- | ----------------------------------------------------------- |
| version | int | none | yes | This key is required and must be set to `2` |
| automerge | bool | false | no | Automatically merge pull request when all plans are applied |
| projects | array[[Project](atlantis-yaml-reference.html#project)] | [] | no | Lists the projects in this repo |
| workflows | map[string -> [Workflow](atlantis-yaml-reference.html#workflow)] | {} | no | Custom workflows |
| Key | Type | Default | Required | Description |
| ----------------------------- | ---------------------------------------------------------------- | ------- | -------- | ----------------------------------------------------------- |
| version | int | none | yes | This key is required and must be set to `2` |
| automerge | bool | false | no | Automatically merge pull request when all plans are applied |
| projects | array[[Project](atlantis-yaml-reference.html#project)] | [] | no | Lists the projects in this repo |
| workflows<br />*(restricted)* | map[string -> [Workflow](atlantis-yaml-reference.html#workflow)] | {} | no | Custom workflows |
### Project
```yaml
@@ -90,15 +93,15 @@ apply_requirements: ["approved"]
workflow: myworkflow
```
| Key | Type | Default | Required | Description |
| ------------------ | ------------------------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. |
| dir | string | none | yes | The directory of this project relative to the repo root. Use `.` for the root. For example if the project was under `./project1` then use `project1` |
| workspace | string | default | no | The [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. |
| autoplan | [Autoplan](atlantis-yaml-reference.html#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the default algorithm. See [Autoplanning](autoplanning.html). |
| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. |
| apply_requirements | array[string] | [] | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. |
| workflow | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. |
| Key | Type | Default | Required | Description |
| -------------------------------------- | ------------------------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | none | maybe | Required if there is more than one project with the same `dir` and `workspace`. This project name can be used with the `-p` flag. |
| dir | string | none | yes | The directory of this project relative to the repo root. Use `.` for the root. For example if the project was under `./project1` then use `project1` |
| workspace | string | default | no | The [Terraform workspace](https://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. |
| autoplan | [Autoplan](atlantis-yaml-reference.html#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the default algorithm. See [Autoplanning](autoplanning.html). |
| terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. |
| apply_requirements<br />*(restricted)* | array[string] | [] | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. |
| workflow <br />*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. |
::: tip
A project represents a Terraform state. Typically, there is one state per directory and workspace however it's possible to
@@ -112,7 +115,7 @@ enabled: true
when_modified: ["*.tf"]
```
| Key | Type | Default | Required | Description |
| ------------- | ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| ------------- | ------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| enabled | boolean | true | no | Whether autoplanning is enabled for this project. |
| when_modified | array[string] | no | no | Uses [.dockerignore](https://docs.docker.com/engine/reference/builder/#dockerignore-file) syntax. If any modified file in the pull request matches, this project will be planned. If not specified, Atlantis will use its own algorithm. See [Autoplanning](autoplanning.html). Paths are relative to the project's dir. |
@@ -122,8 +125,8 @@ plan:
apply:
```
| Key | Type | Default | Required | Description |
| ----- | ------------------------------------------- | --------------------- | -------- | ------------------------------ |
| Key | Type | Default | Required | Description |
| ----- | ------------------------------------------- | --------------------- | -------- | ------------------------------ |
| plan | [Stage](atlantis-yaml-reference.html#stage) | `steps: [init, plan]` | no | How to plan for this project. |
| apply | [Stage](atlantis-yaml-reference.html#stage) | `steps: [apply]` | no | How to apply for this project. |

View File

@@ -1,7 +1,8 @@
# Customizing Atlantis
How Atlantis exactly operates for each repo can be customized via an
`atlantis.yaml` file placed at the root of each repo.
How Atlantis exactly operates for each repo can be customized by using a `repos.yaml` file when starting the Atlantis
server and via an `atlantis.yaml` file placed at the root of each repo.
* Read about the possible [use cases](/guide/atlantis-yaml-use-cases.html)
* Check out the [atlantis.yaml reference](atlantis-yaml-reference.html)
* Check out the [repos.yaml reference](repos-yaml-reference.html)

View File

@@ -0,0 +1,126 @@
# repos.yaml Reference
[[toc]]
::: tip Do I need a repos.yaml file?
A `repos.yaml` file is only required if you wish to customize some aspect of Atlantis.
You can provide even more customizations by combining a server side `repos.yaml` file with an
`atlantis.yaml` file in a repository. See the [atlantis.yaml reference](atlantis-yaml-reference.html) for
more information on additional customizations.
:::
## Enabling repos.yaml
The atlantis server must be running with the `--allow-restricted-repo-config` option to allow Atlantis to use
`repos.yaml` and `atlantis.yaml` files. The location of the `repos.yaml` file on the Atlantis server must be provided
to the `--repos-config` option.
## Overview
The `repos.yaml` file lets you provide customized settings to be applied globally to repositories that match a
regular expression or an exact string. An `atlantis.yaml` file allows you to provide more fine grained configuration
about how atlantis should act on a specific repository.
Some of the settings in `repos.yaml` are sensitive in nature, and would allow users to run arbitrary code on the
Atlantis server if they were allowed in the `atlantis.yaml` file. These settings are restricted to the `repos.yaml`
file by default, but the `repos.yaml` file can allow them to be set in an `atlantis.yaml` file.
The `repos.yaml` file allows a list of repos to be defined, using either a regex or exact string to match a repository
path. If a repository matches multiple entries, the settings from the last entry in the list take precedence.
## Example Using All Keys
```yaml
repos:
- id: /.*/
apply_requirements: [approved, mergeable]
workflow: repoworkflow
allowed_overrides: [apply_requirements, workflow]
allow_custom_workflows: true
workflows:
repoworkflow:
plan:
steps:
- run: my-custom-command arg1 arg2
- init
- plan:
extra_args: ["-lock", "false"]
- run: my-custom-command arg1 arg2
apply:
steps:
- run: echo hi
- apply
```
## Reference
### Top-Level Keys
| Key | Type | Default | Required | Description |
| --------- | --------------------------------------------- | ------- | -------- | --------------------------------- |
| repos | array[[Repo](repos-yaml-reference.html#repo)] |[] | no | List of repos to apply settings to|
| workflows | map[string -> [Workflow](repos-yaml-reference.html#workflow)]
### Repo
| Key | Type | Default | Required | Description |
| ------------------ | -------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | string | none | yes | Value can be a regular expersion when specified as /&lt;regex&gt;/ or an exact string match |
| workflow | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow |
| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved` and `mergeable`. See [Apply Requirements](apply-requirements.html) for more details. |
| allowed_overrides | []string | none | no | A list of restricted keys to allow the `atlantis.yaml` file to override settings from the `repos.yaml` file. <br /><br />Restricted Keys Include:<br /><ul><li>apply_requirements</li><li>workflow</li></ul> |
### Workflow
```yaml
plan:
apply:
```
| Key | Type | Default | Required | Description |
| ----- | ------------------------------------------- | --------------------- | -------- | ------------------------------ |
| plan | [Stage](atlantis-yaml-reference.html#stage) | `steps: [init, plan]` | no | How to plan for this project. |
| apply | [Stage](atlantis-yaml-reference.html#stage) | `steps: [apply]` | no | How to apply for this project. |
### Stage
```yaml
steps:
- run: custom-command
- init
- plan:
extra_args: [-lock=false]
```
| Key | Type | Default | Required | Description |
| ----- | ------------------------------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------- |
| steps | array[[Step](atlantis-yaml-reference.html#step)] | `[]` | no | List of steps for this stage. If the steps key is empty, no steps will be run for this stage. |
### Step
#### Built-In Commands: init, plan, apply
Steps can be a single string for a built-in command.
```yaml
- init
- plan
- apply
```
| Key | Type | Default | Required | Description |
| --------------- | ------ | ------- | -------- | ------------------------------------------------------------------------------------------------------ |
| init/plan/apply | string | none | no | Use a built-in command without additional configuration. Only `init`, `plan` and `apply` are supported |
#### Built-In Command With Extra Args
A map from string to `extra_args` for a built-in command with extra arguments.
```yaml
- init:
extra_args: [arg1, arg2]
- plan:
extra_args: [arg1, arg2]
- apply:
extra_args: [arg1, arg2]
```
| Key | Type | Default | Required | Description |
| --------------- | ---------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| init/plan/apply | map[`extra_args` -> array[string]] | none | no | Use a built-in command and append `extra_args`. Only `init`, `plan` and `apply` are supported as keys and only `extra_args` is supported as a value |
#### Custom `run` Command
Or a custom command
```yaml
- run: custom-command
```
| Key | Type | Default | Required | Description |
| --- | ------ | ------- | -------- | -------------------- |
| run | string | none | no | Run a custom command |

View File

@@ -46,6 +46,7 @@ func TestParseGithubRepo(t *testing.T) {
Equals(t, models.Repo{
Owner: "owner",
FullName: "owner/repo",
FullNameWithHost: "github.com/owner/repo",
CloneURL: "https://github-user:github-token@github.com/owner/repo.git",
SanitizedCloneURL: Repo.GetCloneURL(),
Name: "repo",
@@ -95,6 +96,7 @@ func TestParseGithubIssueCommentEvent(t *testing.T) {
Equals(t, models.Repo{
Owner: *comment.Repo.Owner.Login,
FullName: *comment.Repo.FullName,
FullNameWithHost: "github.com/owner/repo",
CloneURL: "https://github-user:github-token@github.com/owner/repo.git",
SanitizedCloneURL: *comment.Repo.CloneURL,
Name: "repo",
@@ -133,6 +135,7 @@ func TestParseGithubPullEvent(t *testing.T) {
expBaseRepo := models.Repo{
Owner: "owner",
FullName: "owner/repo",
FullNameWithHost: "github.com/owner/repo",
CloneURL: "https://github-user:github-token@github.com/owner/repo.git",
SanitizedCloneURL: Repo.GetCloneURL(),
Name: "repo",
@@ -251,6 +254,7 @@ func TestParseGithubPull(t *testing.T) {
expBaseRepo := models.Repo{
Owner: "owner",
FullName: "owner/repo",
FullNameWithHost: "github.com/owner/repo",
CloneURL: "https://github-user:github-token@github.com/owner/repo.git",
SanitizedCloneURL: Repo.GetCloneURL(),
Name: "repo",
@@ -286,6 +290,7 @@ func TestParseGitlabMergeEvent(t *testing.T) {
expBaseRepo := models.Repo{
FullName: "lkysow/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow/atlantis-example.git",
Owner: "lkysow",
@@ -311,6 +316,7 @@ func TestParseGitlabMergeEvent(t *testing.T) {
Equals(t, expBaseRepo, actBaseRepo)
Equals(t, models.Repo{
FullName: "sourceorg/atlantis-example",
FullNameWithHost: "gitlab.com/sourceorg/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/sourceorg/atlantis-example.git",
Owner: "sourceorg",
@@ -343,6 +349,7 @@ func TestParseGitlabMergeEvent_Subgroup(t *testing.T) {
expBaseRepo := models.Repo{
FullName: "lkysow-test/subgroup/sub-subgroup/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git",
Owner: "lkysow-test/subgroup/sub-subgroup",
@@ -368,6 +375,7 @@ func TestParseGitlabMergeEvent_Subgroup(t *testing.T) {
Equals(t, expBaseRepo, actBaseRepo)
Equals(t, models.Repo{
FullName: "lkysow-test/subgroup/sub-subgroup/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git",
Owner: "lkysow-test/subgroup/sub-subgroup",
@@ -439,6 +447,7 @@ func TestParseGitlabMergeRequest(t *testing.T) {
Ok(t, err)
repo := models.Repo{
FullName: "gitlabhq/gitlab-test",
FullNameWithHost: "example.com/gitlabhq/gitlab-test",
Name: "gitlab-test",
SanitizedCloneURL: "https://example.com/gitlabhq/gitlab-test.git",
Owner: "gitlabhq",
@@ -478,6 +487,7 @@ func TestParseGitlabMergeRequest_Subgroup(t *testing.T) {
repo := models.Repo{
FullName: "lkysow-test/subgroup/sub-subgroup/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git",
Owner: "lkysow-test/subgroup/sub-subgroup",
@@ -512,6 +522,7 @@ func TestParseGitlabMergeCommentEvent(t *testing.T) {
Ok(t, err)
Equals(t, models.Repo{
FullName: "gitlabhq/gitlab-test",
FullNameWithHost: "example.com/gitlabhq/gitlab-test",
Name: "gitlab-test",
SanitizedCloneURL: "https://example.com/gitlabhq/gitlab-test.git",
Owner: "gitlabhq",
@@ -523,6 +534,7 @@ func TestParseGitlabMergeCommentEvent(t *testing.T) {
}, baseRepo)
Equals(t, models.Repo{
FullName: "gitlab-org/gitlab-test",
FullNameWithHost: "example.com/gitlab-org/gitlab-test",
Name: "gitlab-test",
SanitizedCloneURL: "https://example.com/gitlab-org/gitlab-test.git",
Owner: "gitlab-org",
@@ -550,6 +562,7 @@ func TestParseGitlabMergeCommentEvent_Subgroup(t *testing.T) {
Equals(t, models.Repo{
FullName: "lkysow-test/subgroup/sub-subgroup/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git",
Owner: "lkysow-test/subgroup/sub-subgroup",
@@ -561,6 +574,7 @@ func TestParseGitlabMergeCommentEvent_Subgroup(t *testing.T) {
}, baseRepo)
Equals(t, models.Repo{
FullName: "lkysow-test/subgroup/sub-subgroup/atlantis-example",
FullNameWithHost: "gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example",
Name: "atlantis-example",
SanitizedCloneURL: "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example.git",
Owner: "lkysow-test/subgroup/sub-subgroup",
@@ -704,6 +718,7 @@ func TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) {
Ok(t, err)
expBaseRepo := models.Repo{
FullName: "lkysow/atlantis-example",
FullNameWithHost: "bitbucket.org/lkysow/atlantis-example",
Owner: "lkysow",
Name: "atlantis-example",
CloneURL: "https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow/atlantis-example.git",
@@ -726,6 +741,7 @@ func TestParseBitbucketCloudCommentEvent_ValidEvent(t *testing.T) {
}, pull)
Equals(t, models.Repo{
FullName: "lkysow-fork/atlantis-example",
FullNameWithHost: "bitbucket.org/lkysow-fork/atlantis-example",
Owner: "lkysow-fork",
Name: "atlantis-example",
CloneURL: "https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow-fork/atlantis-example.git",
@@ -790,6 +806,7 @@ func TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) {
Ok(t, err)
expBaseRepo := models.Repo{
FullName: "lkysow/atlantis-example",
FullNameWithHost: "bitbucket.org/lkysow/atlantis-example",
Owner: "lkysow",
Name: "atlantis-example",
CloneURL: "https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow/atlantis-example.git",
@@ -812,6 +829,7 @@ func TestParseBitbucketCloudPullEvent_ValidEvent(t *testing.T) {
}, pull)
Equals(t, models.Repo{
FullName: "lkysow-fork/atlantis-example",
FullNameWithHost: "bitbucket.org/lkysow-fork/atlantis-example",
Owner: "lkysow-fork",
Name: "atlantis-example",
CloneURL: "https://bitbucket-user:bitbucket-token@bitbucket.org/lkysow-fork/atlantis-example.git",
@@ -891,6 +909,7 @@ func TestParseBitbucketServerCommentEvent_ValidEvent(t *testing.T) {
Ok(t, err)
expBaseRepo := models.Repo{
FullName: "atlantis/atlantis-example",
FullNameWithHost: "mycorp.com:7490/atlantis/atlantis-example",
Owner: "atlantis",
Name: "atlantis-example",
CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git",
@@ -913,6 +932,7 @@ func TestParseBitbucketServerCommentEvent_ValidEvent(t *testing.T) {
}, pull)
Equals(t, models.Repo{
FullName: "atlantis-fork/atlantis-example",
FullNameWithHost: "mycorp.com:7490/atlantis-fork/atlantis-example",
Owner: "atlantis-fork",
Name: "atlantis-example",
CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git",
@@ -973,6 +993,7 @@ func TestParseBitbucketServerPullEvent_ValidEvent(t *testing.T) {
Ok(t, err)
expBaseRepo := models.Repo{
FullName: "atlantis/atlantis-example",
FullNameWithHost: "mycorp.com:7490/atlantis/atlantis-example",
Owner: "atlantis",
Name: "atlantis-example",
CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/at/atlantis-example.git",
@@ -995,6 +1016,7 @@ func TestParseBitbucketServerPullEvent_ValidEvent(t *testing.T) {
}, pull)
Equals(t, models.Repo{
FullName: "atlantis-fork/atlantis-example",
FullNameWithHost: "mycorp.com:7490/atlantis-fork/atlantis-example",
Owner: "atlantis-fork",
Name: "atlantis-example",
CloneURL: "http://bitbucket-user:bitbucket-token@mycorp.com:7490/scm/fk/atlantis-example.git",

View File

@@ -35,6 +35,9 @@ type Repo struct {
// FullName is the owner and repo name separated
// by a "/", ex. "runatlantis/atlantis", "gitlab/subgroup/atlantis", "Bitbucket Server/atlantis".
FullName string
// FullNameWithHost
// This is the full name of the repo including the hostname. ex github.com/runatlantis/atlantis
FullNameWithHost string
// Owner is just the repo owner, ex. "runatlantis" or "gitlab/subgroup".
// This may contain /'s in the case of GitLab subgroups.
// This may contain spaces in the case of Bitbucket Server.
@@ -73,6 +76,12 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU
return Repo{}, errors.Wrap(err, "invalid clone url")
}
repoHostname := cloneURLParsed.Hostname()
if cloneURLParsed.Port() != "" {
repoHostname = fmt.Sprintf("%s:%s", repoHostname, cloneURLParsed.Port())
}
repoFullNameWithHost := fmt.Sprintf("%s/%s", repoHostname, repoFullName)
// Ensure the Clone URL is for the same repo to avoid something malicious.
// We skip this check for Bitbucket Server because its format is different
// and because the caller in that case actually constructs the clone url
@@ -111,6 +120,7 @@ func NewRepo(vcsHostType VCSHostType, repoFullName string, cloneURL string, vcsU
return Repo{
FullName: repoFullName,
FullNameWithHost: repoFullNameWithHost,
Owner: owner,
Name: repo,
CloneURL: authedCloneURL,

View File

@@ -49,6 +49,7 @@ func TestNewRepo_CloneURLBitbucketServer(t *testing.T) {
Ok(t, err)
Equals(t, models.Repo{
FullName: "owner/repo",
FullNameWithHost: "mycorp.com:7990/owner/repo",
Owner: "owner",
Name: "repo",
CloneURL: "http://u:p@mycorp.com:7990/scm/at/atlantis-example.git",
@@ -119,6 +120,7 @@ func TestNewRepo_HTTPAuth(t *testing.T) {
SanitizedCloneURL: "http://github.com/owner/repo.git",
CloneURL: "http://u:p@github.com/owner/repo.git",
FullName: "owner/repo",
FullNameWithHost: "github.com/owner/repo",
Owner: "owner",
Name: "repo",
}, repo)
@@ -136,6 +138,7 @@ func TestNewRepo_HTTPSAuth(t *testing.T) {
SanitizedCloneURL: "https://github.com/owner/repo.git",
CloneURL: "https://u:p@github.com/owner/repo.git",
FullName: "owner/repo",
FullNameWithHost: "github.com/owner/repo",
Owner: "owner",
Name: "repo",
}, repo)

View File

@@ -2,6 +2,8 @@ package events
import (
"fmt"
"github.com/runatlantis/atlantis/server/events/yaml/raw"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
"strings"
"github.com/hashicorp/go-version"
@@ -9,7 +11,6 @@ import (
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/events/yaml"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
"github.com/runatlantis/atlantis/server/logging"
)
@@ -50,6 +51,7 @@ type DefaultProjectCommandBuilder struct {
WorkingDirLocker WorkingDirLocker
AllowRepoConfig bool
AllowRepoConfigFlag string
RepoConfig raw.RepoConfig
PendingPlanFinder *DefaultPendingPlanFinder
CommentBuilder CommentBuilder
}
@@ -102,10 +104,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
}
if hasConfigFile {
if !p.AllowRepoConfig {
return nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag)
}
config, err = p.ParserValidator.ReadConfig(repoDir)
config, err = p.ParserValidator.ReadConfig(repoDir, p.RepoConfig, ctx.BaseRepo.FullNameWithHost, p.AllowRepoConfig)
if err != nil {
return nil, err
}
@@ -130,6 +129,24 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.BaseRepo.FullName, repoDir)
ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects)
for _, mp := range modifiedProjects {
var globalConfig valid.Config
var projectConfig *valid.Project
// If there is a server side repo config that matches, then the project should be planned using
// a config that honors those values. Creating a single project config with no settings other than
// dir and merging with the server side repo yaml achieves this
version := 2
config := raw.Config{
Version: &version,
Projects: []raw.Project{{Dir: &mp.Path}},
}
config, err = p.ParserValidator.ValidateOverridesAndMergeConfig(config, p.RepoConfig, ctx.BaseRepo.FullNameWithHost, p.AllowRepoConfig)
if err != nil {
return nil, err
}
globalConfig = config.ToValid()
projectConfig = &globalConfig.Projects[0]
projCtxs = append(projCtxs, models.ProjectCommandContext{
BaseRepo: ctx.BaseRepo,
HeadRepo: ctx.HeadRepo,
@@ -137,8 +154,8 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
User: ctx.User,
Log: ctx.Log,
RepoRelDir: mp.Path,
ProjectConfig: nil,
GlobalConfig: nil,
ProjectConfig: projectConfig,
GlobalConfig: &globalConfig,
CommentArgs: commentFlags,
Workspace: DefaultWorkspace,
Verbose: verbose,
@@ -293,7 +310,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *CommandCont
}
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContext, projectName string, commentFlags []string, repoDir string, repoRelDir string, workspace string) (models.ProjectCommandContext, error) {
projCfg, globalCfg, err := p.getCfg(projectName, repoRelDir, workspace, repoDir)
projCfg, globalCfg, err := p.getCfg(ctx, projectName, repoRelDir, workspace, repoDir)
if err != nil {
return models.ProjectCommandContext{}, err
}
@@ -327,26 +344,38 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
}, nil
}
func (p *DefaultProjectCommandBuilder) getCfg(projectName string, dir string, workspace string, repoDir string) (projectCfg *valid.Project, globalCfg *valid.Config, err error) {
// This function is used to get the project config file when apply is being run
func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName string, dir string, workspace string, repoDir string) (projectCfg *valid.Project, globalCfg *valid.Config, err error) {
hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir)
if err != nil {
err = errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
return
}
if !hasConfigFile {
if projectName != "" {
err = fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename)
return
if !hasConfigFile && projectName != "" {
err = fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename)
return
}
var globalCfgStruct valid.Config
// If we have a config file, read it and let any repo restricted config be merged and validated
if hasConfigFile {
globalCfgStruct, err = p.ParserValidator.ReadConfig(repoDir, p.RepoConfig, ctx.BaseRepo.FullNameWithHost, p.AllowRepoConfig)
} else {
// If no atlantis.yaml file exists, we generate a skeleton config and merge all of the server side repo config
// settings into. If no server side repo config was provided, a default one was generated at server start
version := 2
rawConfig := raw.Config{
Version: &version,
Projects: []raw.Project{
{
Dir: &dir,
Workspace: &workspace,
},
},
}
return
rawConfig, err = p.ParserValidator.ValidateOverridesAndMergeConfig(rawConfig, p.RepoConfig, ctx.BaseRepo.FullNameWithHost, p.AllowRepoConfig)
globalCfgStruct = rawConfig.ToValid()
}
if !p.AllowRepoConfig {
err = fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag)
return
}
globalCfgStruct, err := p.ParserValidator.ReadConfig(repoDir)
if err != nil {
return
}

View File

@@ -1,6 +1,7 @@
package events_test
import (
"github.com/runatlantis/atlantis/server/events/yaml/raw"
"io/ioutil"
"path/filepath"
"testing"
@@ -35,9 +36,13 @@ func TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) {
AtlantisYAML: "",
exp: []exp{
{
projectConfig: nil,
dir: ".",
workspace: "default",
projectConfig: &valid.Project{
Dir: ".",
Workspace: "default",
Autoplan: getDefaultAutoPlan(),
},
dir: ".",
workspace: "default",
},
},
},
@@ -229,6 +234,175 @@ projects:
}
}
func TestDefaultProjectCommandBuilder_RepoRestrictionsBuildPlanCommands(t *testing.T) {
workflow := "repoworkflow"
repoConfig := raw.RepoConfig{
Repos: []raw.Repo{{
ID: "/.*/",
Workflow: &workflow,
}},
}
expWorkspace := "default"
expDir := "."
expProjectCfg := &valid.Project{
Dir: ".",
Workflow: repoConfig.Repos[0].Workflow,
Workspace: expWorkspace,
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf*"},
},
}
t.Run("run plan with server side repo config and no atlantis.yaml", func(t *testing.T) {
RegisterMockTestingT(t)
tmpDir, cleanup := TempDir(t)
defer cleanup()
baseRepo := models.Repo{}
headRepo := models.Repo{}
pull := models.PullRequest{}
logger := logging.NewNoopLogger()
workingDir := mocks.NewMockWorkingDir()
When(workingDir.Clone(logger, baseRepo, headRepo, pull, "default")).ThenReturn(tmpDir, nil)
vcsClient := vcsmocks.NewMockClient()
When(vcsClient.GetModifiedFiles(baseRepo, pull)).ThenReturn([]string{"main.tf"}, nil)
builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
AllowRepoConfig: false,
RepoConfig: repoConfig,
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
AllowRepoConfigFlag: "allow-repo-config",
CommentBuilder: &events.CommentParser{},
}
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
BaseRepo: baseRepo,
HeadRepo: headRepo,
Pull: pull,
User: models.User{},
Log: logger,
})
Ok(t, err)
for _, actCtx := range ctxs {
Equals(t, baseRepo, actCtx.BaseRepo)
Equals(t, baseRepo, actCtx.HeadRepo)
Equals(t, pull, actCtx.Pull)
Equals(t, models.User{}, actCtx.User)
Equals(t, logger, actCtx.Log)
Equals(t, 0, len(actCtx.CommentArgs))
Equals(t, expProjectCfg, actCtx.ProjectConfig)
Equals(t, expDir, actCtx.RepoRelDir)
Equals(t, expWorkspace, actCtx.Workspace)
}
})
}
func TestDefaultProjectCommandBuilder_BuildSingleApplyCommandRepoRestrictions(t *testing.T) {
workflow := "repoworkflow"
repoConfig := raw.RepoConfig{
Repos: []raw.Repo{{
ID: "/.*/",
Workflow: &workflow,
}},
}
expWorkspace := "default"
expDir := "."
expProjectCfg := &valid.Project{
Dir: ".",
Workflow: repoConfig.Repos[0].Workflow,
Workspace: expWorkspace,
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf*"},
},
}
expCommentArgs := "commentarg"
cmd := events.CommentCommand{
RepoRelDir: ".",
Flags: []string{expCommentArgs},
Name: models.PlanCommand,
}
for _, cmdName := range []models.CommandName{models.PlanCommand, models.ApplyCommand} {
t.Run("run apply with server side repo config and no atlantis.yaml", func(t *testing.T) {
RegisterMockTestingT(t)
tmpDir, cleanup := TempDir(t)
defer cleanup()
baseRepo := models.Repo{}
headRepo := models.Repo{}
pull := models.PullRequest{}
logger := logging.NewNoopLogger()
workingDir := mocks.NewMockWorkingDir()
if cmdName == models.PlanCommand {
When(workingDir.Clone(logger, baseRepo, headRepo, pull, expWorkspace)).ThenReturn(tmpDir, nil)
} else {
When(workingDir.GetWorkingDir(baseRepo, pull, expWorkspace)).ThenReturn(tmpDir, nil)
}
vcsClient := vcsmocks.NewMockClient()
When(vcsClient.GetModifiedFiles(baseRepo, pull)).ThenReturn([]string{"main.tf"}, nil)
builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
AllowRepoConfig: false,
RepoConfig: repoConfig,
AllowRepoConfigFlag: "allow-repo-config",
CommentBuilder: &events.CommentParser{},
}
cmdCtx := &events.CommandContext{
BaseRepo: baseRepo,
HeadRepo: headRepo,
Pull: pull,
User: models.User{},
Log: logger,
}
var actCtxs []models.ProjectCommandContext
var err error
if cmdName == models.PlanCommand {
actCtxs, err = builder.BuildPlanCommands(cmdCtx, &cmd)
} else {
actCtxs, err = builder.BuildApplyCommands(cmdCtx, &cmd)
}
Ok(t, err)
Equals(t, 1, len(actCtxs))
actCtx := actCtxs[0]
Equals(t, baseRepo, actCtx.BaseRepo)
Equals(t, baseRepo, actCtx.HeadRepo)
Equals(t, pull, actCtx.Pull)
Equals(t, models.User{}, actCtx.User)
Equals(t, logger, actCtx.Log)
Equals(t, expProjectCfg, actCtx.ProjectConfig)
Equals(t, expDir, actCtx.RepoRelDir)
Equals(t, expWorkspace, actCtx.Workspace)
Equals(t, []string{expCommentArgs}, actCtx.CommentArgs)
})
}
}
// Test building a plan and apply command for one project.
func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T) {
cases := []struct {
@@ -249,11 +423,15 @@ func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T)
Name: models.PlanCommand,
Workspace: "myworkspace",
},
AtlantisYAML: "",
ExpProjectConfig: nil,
ExpCommentArgs: []string{"commentarg"},
ExpWorkspace: "myworkspace",
ExpDir: ".",
AtlantisYAML: "",
ExpProjectConfig: &valid.Project{
Dir: ".",
Workspace: "myworkspace",
Autoplan: getDefaultAutoPlan(),
},
ExpCommentArgs: []string{"commentarg"},
ExpWorkspace: "myworkspace",
ExpDir: ".",
},
{
Description: "no atlantis.yaml with project flag",
@@ -571,11 +749,21 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanNoAtlantisYAML(t *testing.T)
Equals(t, 2, len(ctxs))
Equals(t, "project1", ctxs[0].RepoRelDir)
Equals(t, "default", ctxs[0].Workspace)
var nilProjectConfig *valid.Project
Equals(t, nilProjectConfig, ctxs[0].ProjectConfig)
project1Config := valid.Project{
Dir: "project1",
Workspace: events.DefaultWorkspace,
Autoplan: getDefaultAutoPlan(),
}
project2Config := valid.Project{
Dir: "project2",
Workspace: events.DefaultWorkspace,
Autoplan: getDefaultAutoPlan(),
}
Equals(t, project1Config, *ctxs[0].ProjectConfig)
Equals(t, "project2", ctxs[1].RepoRelDir)
Equals(t, "default", ctxs[1].Workspace)
Equals(t, nilProjectConfig, ctxs[1].ProjectConfig)
Equals(t, project2Config, *ctxs[1].ProjectConfig)
}
// Test building plan command for multiple projects when the comment
@@ -839,69 +1027,6 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
Equals(t, "workspace2", ctxs[3].Workspace)
}
// Test that if repo config is disabled we error out if there's an atlantis.yaml
// file.
func TestDefaultProjectCommandBuilder_RepoConfigDisabled(t *testing.T) {
RegisterMockTestingT(t)
workingDir := mocks.NewMockWorkingDir()
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
"pulldir": map[string]interface{}{
"workspace": map[string]interface{}{},
},
})
defer cleanup()
repoDir := filepath.Join(tmpDir, "pulldir/workspace")
err := ioutil.WriteFile(filepath.Join(repoDir, yaml.AtlantisYAMLFilename), nil, 0600)
Ok(t, err)
When(workingDir.Clone(
matchers.AnyPtrToLoggingSimpleLogger(),
matchers.AnyModelsRepo(),
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString())).ThenReturn(repoDir, nil)
When(workingDir.GetWorkingDir(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString())).ThenReturn(repoDir, nil)
builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: nil,
ProjectFinder: &events.DefaultProjectFinder{},
AllowRepoConfig: false,
AllowRepoConfigFlag: "allow-repo-config",
CommentBuilder: &events.CommentParser{},
}
ctx := &events.CommandContext{
BaseRepo: models.Repo{},
HeadRepo: models.Repo{},
Pull: models.PullRequest{},
User: models.User{},
Log: logging.NewNoopLogger(),
}
_, err = builder.BuildAutoplanCommands(ctx)
ErrEquals(t, "atlantis.yaml files not allowed because Atlantis is not running with --allow-repo-config", err)
commentCmd := &events.CommentCommand{
RepoRelDir: "",
Flags: nil,
Name: 0,
Verbose: false,
Workspace: "workspace",
ProjectName: "",
}
_, err = builder.BuildPlanCommands(ctx, commentCmd)
ErrEquals(t, "atlantis.yaml files not allowed because Atlantis is not running with --allow-repo-config", err)
_, err = builder.BuildApplyCommands(ctx, commentCmd)
ErrEquals(t, "atlantis.yaml files not allowed because Atlantis is not running with --allow-repo-config", err)
}
// Test that if a directory has a list of workspaces configured then we don't
// allow plans for other workspace names.
func TestDefaultProjectCommandBuilder_WrongWorkspaceName(t *testing.T) {
@@ -967,3 +1092,10 @@ projects:
}
func String(v string) *string { return &v }
func getDefaultAutoPlan() valid.Autoplan {
return valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
}
}

View File

@@ -22,7 +22,7 @@ type ParserValidator struct{}
// If there was no config file, then this can be detected by checking the type
// of error: os.IsNotExist(error) but it's instead preferred to check with
// HasConfigFile.
func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) {
func (p *ParserValidator) ReadConfig(repoDir string, repoConfig raw.RepoConfig, repoName string, allowAllRepoConfig bool) (valid.Config, error) {
configFile := p.configFilePath(repoDir)
configData, err := ioutil.ReadFile(configFile) // nolint: gosec
@@ -38,13 +38,33 @@ func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) {
}
// If the config file exists, parse it.
config, err := p.parseAndValidate(configData)
config, err := p.parseAndValidate(configData, repoConfig, repoName, allowAllRepoConfig)
if err != nil {
return valid.Config{}, errors.Wrapf(err, "parsing %s", AtlantisYAMLFilename)
}
return config, err
}
func (p *ParserValidator) ReadServerConfig(configFile string) (raw.RepoConfig, error) {
configData, err := ioutil.ReadFile(configFile) // nolint: gosec
// NOTE: the error we return here must also be os.IsNotExist since that's
// what our callers use to detect a missing config file.
if err != nil && os.IsNotExist(err) {
return raw.RepoConfig{}, err
}
// If it exists but we couldn't read it return an error.
if err != nil {
return raw.RepoConfig{}, errors.Wrapf(err, "unable to read %s file", configFile)
}
config, err := p.parseAndValidateServerConfig(configData)
if err != nil {
return raw.RepoConfig{}, errors.Wrapf(err, "parsing %s", configFile)
}
return config, err
}
func (p *ParserValidator) HasConfigFile(repoDir string) (bool, error) {
_, err := os.Stat(p.configFilePath(repoDir))
if os.IsNotExist(err) {
@@ -60,7 +80,25 @@ func (p *ParserValidator) configFilePath(repoDir string) string {
return filepath.Join(repoDir, AtlantisYAMLFilename)
}
func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, error) {
func (p *ParserValidator) parseAndValidateServerConfig(configData []byte) (raw.RepoConfig, error) {
var config raw.RepoConfig
if err := yaml.UnmarshalStrict(configData, &config); err != nil {
return raw.RepoConfig{}, err
}
validation.ErrorTag = "yaml"
if err := config.Validate(); err != nil {
return raw.RepoConfig{}, err
}
if err := p.validateRepoWorkflows(config); err != nil {
return raw.RepoConfig{}, err
}
return config, nil
}
func (p *ParserValidator) parseAndValidate(configData []byte, repoConfig raw.RepoConfig, repoName string, allowAllRepoConfig bool) (valid.Config, error) {
var rawConfig raw.Config
if err := yaml.UnmarshalStrict(configData, &rawConfig); err != nil {
return valid.Config{}, err
@@ -69,6 +107,12 @@ func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, err
// Set ErrorTag to yaml so it uses the YAML field names in error messages.
validation.ErrorTag = "yaml"
var err error
rawConfig, err = p.ValidateOverridesAndMergeConfig(rawConfig, repoConfig, repoName, allowAllRepoConfig)
if err != nil {
return valid.Config{}, err
}
if err := rawConfig.Validate(); err != nil {
return valid.Config{}, err
}
@@ -86,6 +130,57 @@ func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, err
return validConfig, nil
}
func (p *ParserValidator) getOverrideErrorMessage(key string) error {
return fmt.Errorf("%q cannot be specified in %q by default. To enable this, add %q to %q in the server side repo config", key, AtlantisYAMLFilename, key, raw.AllowedOverridesKey)
}
// Checks any sensitive fields present in atlantis.yaml against the list of allowed overrides and merge the configuration
// from the server side repo config with project settings found in atlantis.yaml
func (p *ParserValidator) ValidateOverridesAndMergeConfig(config raw.Config, repoConfig raw.RepoConfig, repoName string, allowAllRepoConfig bool) (raw.Config, error) {
var finalProjects []raw.Project
// Start with a repo regex that will match everything, but sets no allowed_overrides. This will
// provide a default behavior of "deny all overrides" if no server side defined repos are matched
lastMatchingRepo := raw.Repo{ID: "/.*/"}
// Find the last repo to match. If multiple are found, the last matched repo's settings will be used
for _, repo := range repoConfig.Repos {
matches, err := repo.Matches(repoName)
if err != nil {
return config, err
} else if matches {
lastMatchingRepo = repo
}
}
for _, project := range config.Projects {
// If atlantis.yaml has apply requirements, only honor them if this key is allowed in a server side
// --repo-config or if --allow-repo-config is specified.
if len(project.ApplyRequirements) > 0 && !(allowAllRepoConfig || lastMatchingRepo.IsOverrideAllowed(raw.ApplyRequirementsKey)) {
return config, p.getOverrideErrorMessage(raw.ApplyRequirementsKey)
}
// Do not allow projects to specify a workflow unless it is explicitly allowed
if project.Workflow != nil && !(allowAllRepoConfig || lastMatchingRepo.IsOverrideAllowed(raw.WorkflowKey)) {
return config, p.getOverrideErrorMessage(raw.WorkflowKey)
} else if project.Workflow == nil && lastMatchingRepo.Workflow != nil {
project.Workflow = lastMatchingRepo.Workflow
}
finalProjects = append(finalProjects, project)
}
config.Projects = finalProjects
if len(config.Workflows) > 0 && !(allowAllRepoConfig || lastMatchingRepo.AllowCustomWorkflows) {
return config, fmt.Errorf("%q cannot be specified in %q by default. To enable this, set %q to true in the server side repo config", raw.CustomWorkflowsKey, AtlantisYAMLFilename, raw.CustomWorkflowsKey)
} else if len(config.Workflows) == 0 {
if len(repoConfig.Workflows) > 0 {
config.Workflows = repoConfig.Workflows
}
}
return config, nil
}
func (p *ParserValidator) validateProjectNames(config valid.Config) error {
// First, validate that all names are unique.
seen := make(map[string]bool)
@@ -132,6 +227,28 @@ func (p *ParserValidator) validateWorkflows(config raw.Config) error {
return nil
}
func (p *ParserValidator) validateRepoWorkflows(config raw.RepoConfig) error {
for _, repo := range config.Repos {
if err := p.validateRepoWorkflowExists(repo, config.Workflows); err != nil {
return err
}
}
return nil
}
func (p *ParserValidator) validateRepoWorkflowExists(repo raw.Repo, workflows map[string]raw.Workflow) error {
if repo.Workflow == nil {
return nil
}
workflow := *repo.Workflow
for w := range workflows {
if w == workflow {
return nil
}
}
return fmt.Errorf("workflow %q is not defined", workflow)
}
func (p *ParserValidator) validateWorkflowExists(project raw.Project, workflows map[string]raw.Workflow) error {
if project.Workflow == nil {
return nil

View File

@@ -1,6 +1,8 @@
package yaml_test
import (
"fmt"
"github.com/runatlantis/atlantis/server/events/yaml/raw"
"io/ioutil"
"os"
"path/filepath"
@@ -14,7 +16,7 @@ import (
func TestReadConfig_DirDoesNotExist(t *testing.T) {
r := yaml.ParserValidator{}
_, err := r.ReadConfig("/not/exist")
_, err := r.ReadConfig("/not/exist", raw.RepoConfig{}, "", false)
Assert(t, os.IsNotExist(err), "exp nil ptr")
exists, err := r.HasConfigFile("/not/exist")
@@ -27,7 +29,7 @@ func TestReadConfig_FileDoesNotExist(t *testing.T) {
defer cleanup()
r := yaml.ParserValidator{}
_, err := r.ReadConfig(tmpDir)
_, err := r.ReadConfig(tmpDir, raw.RepoConfig{}, "", false)
Assert(t, os.IsNotExist(err), "exp nil ptr")
exists, err := r.HasConfigFile(tmpDir)
@@ -42,7 +44,7 @@ func TestReadConfig_BadPermissions(t *testing.T) {
Ok(t, err)
r := yaml.ParserValidator{}
_, err = r.ReadConfig(tmpDir)
_, err = r.ReadConfig(tmpDir, raw.RepoConfig{}, "", false)
ErrContains(t, "unable to read atlantis.yaml file: ", err)
}
@@ -74,14 +76,13 @@ func TestReadConfig_UnmarshalErrors(t *testing.T) {
err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.input), 0600)
Ok(t, err)
r := yaml.ParserValidator{}
_, err = r.ReadConfig(tmpDir)
_, err = r.ReadConfig(tmpDir, raw.RepoConfig{}, "", false)
ErrEquals(t, c.expErr, err)
})
}
}
func TestReadConfig(t *testing.T) {
tfVersion, _ := version.NewVersion("v0.11.0")
func TestReadConfig_CommonKeys(t *testing.T) {
cases := []struct {
description string
input string
@@ -160,6 +161,143 @@ projects:
Workflows: map[string]valid.Workflow{},
},
},
{
description: "project dir with ..",
input: `
version: 2
projects:
- dir: ..`,
expErr: "projects: (0: (dir: cannot contain '..'.).).",
},
// Project must have dir set.
{
description: "project with no config",
input: `
version: 2
projects:
-`,
expErr: "projects: (0: (dir: cannot be blank.).).",
},
{
description: "project with no config at index 1",
input: `
version: 2
projects:
- dir: "."
-`,
expErr: "projects: (1: (dir: cannot be blank.).).",
},
{
description: "project with unknown key",
input: `
version: 2
projects:
- unknown: value`,
expErr: "yaml: unmarshal errors:\n line 4: field unknown not found in struct raw.Project",
},
{
description: "two projects with same dir/workspace without names",
input: `
version: 2
projects:
- dir: .
workspace: workspace
- dir: .
workspace: workspace`,
expErr: "there are two or more projects with dir: \".\" workspace: \"workspace\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately",
},
{
description: "two projects with same dir/workspace only one with name",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- dir: .
workspace: workspace`,
expErr: "there are two or more projects with dir: \".\" workspace: \"workspace\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately",
},
{
description: "two projects with same dir/workspace both with same name",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- name: myname
dir: .
workspace: workspace`,
expErr: "found two or more projects with name \"myname\"; project names must be unique",
},
{
description: "two projects with same dir/workspace with different names",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- name: myname2
dir: .
workspace: workspace`,
exp: valid.Config{
Version: 2,
Projects: []valid.Project{
{
Name: String("myname"),
Dir: ".",
Workspace: "workspace",
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
{
Name: String("myname2"),
Dir: ".",
Workspace: "workspace",
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
},
Workflows: map[string]valid.Workflow{},
},
},
}
tmpDir, cleanup := TempDir(t)
defer cleanup()
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.input), 0600)
Ok(t, err)
r := yaml.ParserValidator{}
act, err := r.ReadConfig(tmpDir, raw.RepoConfig{}, "", false)
if c.expErr != "" {
ErrEquals(t, "parsing atlantis.yaml: "+c.expErr, err)
return
}
Ok(t, err)
Equals(t, c.exp, act)
})
}
}
func TestReadConfig_AllowRepoConfig(t *testing.T) {
tfVersion, _ := version.NewVersion("v0.11.0")
cases := []struct {
description string
input string
expErr string
exp valid.Config
}{
{
description: "project fields set except autoplan",
input: `
@@ -294,41 +432,6 @@ workflows:
},
},
},
{
description: "project dir with ..",
input: `
version: 2
projects:
- dir: ..`,
expErr: "projects: (0: (dir: cannot contain '..'.).).",
},
// Project must have dir set.
{
description: "project with no config",
input: `
version: 2
projects:
-`,
expErr: "projects: (0: (dir: cannot be blank.).).",
},
{
description: "project with no config at index 1",
input: `
version: 2
projects:
- dir: "."
-`,
expErr: "projects: (1: (dir: cannot be blank.).).",
},
{
description: "project with unknown key",
input: `
version: 2
projects:
- unknown: value`,
expErr: "yaml: unmarshal errors:\n line 4: field unknown not found in struct raw.Project",
},
{
description: "referencing workflow that doesn't exist",
input: `
@@ -338,78 +441,6 @@ projects:
workflow: undefined`,
expErr: "workflow \"undefined\" is not defined",
},
{
description: "two projects with same dir/workspace without names",
input: `
version: 2
projects:
- dir: .
workspace: workspace
- dir: .
workspace: workspace`,
expErr: "there are two or more projects with dir: \".\" workspace: \"workspace\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately",
},
{
description: "two projects with same dir/workspace only one with name",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- dir: .
workspace: workspace`,
expErr: "there are two or more projects with dir: \".\" workspace: \"workspace\" that are not all named; they must have a 'name' key so they can be targeted for apply's separately",
},
{
description: "two projects with same dir/workspace both with same name",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- name: myname
dir: .
workspace: workspace`,
expErr: "found two or more projects with name \"myname\"; project names must be unique",
},
{
description: "two projects with same dir/workspace with different names",
input: `
version: 2
projects:
- name: myname
dir: .
workspace: workspace
- name: myname2
dir: .
workspace: workspace`,
exp: valid.Config{
Version: 2,
Projects: []valid.Project{
{
Name: String("myname"),
Dir: ".",
Workspace: "workspace",
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
{
Name: String("myname2"),
Dir: ".",
Workspace: "workspace",
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
},
Workflows: map[string]valid.Workflow{},
},
},
}
tmpDir, cleanup := TempDir(t)
@@ -421,7 +452,7 @@ projects:
Ok(t, err)
r := yaml.ParserValidator{}
act, err := r.ReadConfig(tmpDir)
act, err := r.ReadConfig(tmpDir, raw.RepoConfig{}, "", true)
if c.expErr != "" {
ErrEquals(t, "parsing atlantis.yaml: "+c.expErr, err)
return
@@ -430,9 +461,9 @@ projects:
Equals(t, c.exp, act)
})
}
}
func TestReadConfig_Successes(t *testing.T) {
}
func TestReadConfig_Successes_AllowRepoConfig(t *testing.T) {
basicProjects := []valid.Project{
{
Autoplan: valid.Autoplan{
@@ -685,13 +716,342 @@ workflows:
Ok(t, err)
r := yaml.ParserValidator{}
act, err := r.ReadConfig(tmpDir)
act, err := r.ReadConfig(tmpDir, raw.RepoConfig{}, "", true)
Ok(t, err)
Equals(t, c.expOutput, act)
})
}
}
func TestReadConfig_ServerSideRepoConfig(t *testing.T) {
cases := []struct {
description string
atlantisYaml string
repoYaml string
repoName string
expErr string
exp valid.Config
}{
{
description: "atlantis config with workflow denied by repo config",
repoName: "anything",
atlantisYaml: `
version: 2
projects:
- dir: .
workflow: projworkflow
`,
repoYaml: `
repos:
- id: /.*/
`,
expErr: `"workflow" cannot be specified in "atlantis.yaml" by default. To enable this, add "workflow" to "allowed_overrides" in the server side repo config`,
},
{
description: "atlantis config with custom workflows denied by repo config",
repoName: "anything",
atlantisYaml: `
version: 2
projects:
- dir: .
workflow: projworkflow
workflows:
projworkflow: ~
`,
repoYaml: `
repos:
- id: /.*/
allowed_overrides: ["workflow"]
`,
expErr: `"workflows" cannot be specified in "atlantis.yaml" by default. To enable this, set "workflows" to true in the server side repo config`,
},
{
description: "atlantis config with workflow override allowed by repo config",
repoName: "thisproject",
atlantisYaml: `
version: 2
projects:
- dir: .
workflow: workflow2
`,
repoYaml: `
repos:
- id: /.*/
workflow: workflow1
allowed_overrides: ["workflow"]
workflows:
workflow1: ~
workflow2: ~
`,
exp: valid.Config{
Version: 2,
Projects: []valid.Project{
{
Dir: ".",
Workspace: "default",
Workflow: String("workflow2"),
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
},
Workflows: map[string]valid.Workflow{
"workflow1": {},
"workflow2": {},
},
},
},
{
description: "atlantis config with no workflow, using workflow from repo config",
repoName: "thisproject",
atlantisYaml: `
version: 2
projects:
- dir: .
`,
repoYaml: `
repos:
- id: /.*/
workflow: workflow1
allowed_overrides: ["workflow"]
workflows:
workflow1: ~
workflow2: ~
`,
exp: valid.Config{
Version: 2,
Projects: []valid.Project{
{
Dir: ".",
Workspace: "default",
Workflow: String("workflow1"),
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
},
Workflows: map[string]valid.Workflow{
"workflow1": {},
"workflow2": {},
},
},
},
{
description: "atlantis config with apply_requirements denied by repo config",
repoName: "anything",
atlantisYaml: `
version: 2
projects:
- dir: .
apply_requirements: ["approved"]
`,
repoYaml: `
repos:
- id: /.*/
`,
expErr: `"apply_requirements" cannot be specified in "atlantis.yaml" by default. To enable this, add "apply_requirements" to "allowed_overrides" in the server side repo config`,
},
{
description: "last matching repo should be used",
repoName: "thisproject",
atlantisYaml: `
version: 2
projects:
- dir: .
`,
repoYaml: `
repos:
- id: /.*/
workflow: workflow1
- id: "thisproject"
workflow: workflow2
workflows:
workflow1: ~
workflow2: ~
`,
exp: valid.Config{
Version: 2,
Projects: []valid.Project{
{
Dir: ".",
Workspace: "default",
Workflow: String("workflow2"),
Autoplan: valid.Autoplan{
WhenModified: []string{"**/*.tf*"},
Enabled: true,
},
},
},
Workflows: map[string]valid.Workflow{
"workflow1": {},
"workflow2": {},
},
},
},
{
description: "atlantis config uses a workflow that doesn't exist in atlantis.yaml or repo config",
repoName: "anything",
atlantisYaml: `
version: 2
projects:
- dir: .
workflow: notexist
`,
repoYaml: `
repos:
- id: /.*/
allowed_overrides: ["workflow"]
`,
expErr: `workflow "notexist" is not defined`,
},
{
description: "repo config contains invalid regex",
repoName: "anything",
atlantisYaml: `
version: 2
projects:
- dir: .
`,
repoYaml: `
repos:
- id: /inva\lid.regex/
`,
expErr: "regex compile of repo.ID `/inva\\lid.regex/`: error parsing regexp: invalid escape sequence: `\\l`",
},
}
tmpDir, cleanup := TempDir(t)
defer cleanup()
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.atlantisYaml), 0600)
Ok(t, err)
err = ioutil.WriteFile(filepath.Join(tmpDir, "repo.yaml"), []byte(c.repoYaml), 0600)
Ok(t, err)
r := yaml.ParserValidator{}
repoConfig, err := r.ReadServerConfig(filepath.Join(tmpDir, "repo.yaml"))
Ok(t, err)
act, err := r.ReadConfig(tmpDir, repoConfig, c.repoName, false)
if c.expErr != "" {
ErrEquals(t, "parsing atlantis.yaml: "+c.expErr, err)
return
}
Equals(t, c.exp, act)
})
}
}
func TestReadServerConfig_DirDoesNotExist(t *testing.T) {
r := yaml.ParserValidator{}
_, err := r.ReadServerConfig("/not/exist")
Assert(t, os.IsNotExist(err), "exp nil ptr")
}
func TestReadServerConfig_FileDoesNotExist(t *testing.T) {
tmpDir, cleanup := TempDir(t)
defer cleanup()
r := yaml.ParserValidator{}
_, err := r.ReadServerConfig(tmpDir + "repos.yaml")
Assert(t, os.IsNotExist(err), "exp nil ptr")
}
func TestReadServerConfig_BadPermissions(t *testing.T) {
tmpDir, cleanup := TempDir(t)
defer cleanup()
repoYamlFile := filepath.Join(tmpDir, "repos.yaml")
err := ioutil.WriteFile(repoYamlFile, nil, 0000)
Ok(t, err)
r := yaml.ParserValidator{}
_, err = r.ReadServerConfig(repoYamlFile)
ErrContains(t, "unable to read "+repoYamlFile, err)
}
func TestServerReadConfig_UnmarshalErrors(t *testing.T) {
// We only have a few cases here because we assume the YAML library to be
// well tested. See https://github.com/go-yaml/yaml/blob/v2/decode_test.go#L810.
cases := []struct {
description string
input string
expErr string
}{
{
"random characters",
"slkjds",
"yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `slkjds` into raw.RepoConfig",
},
{
"just a colon",
":",
"yaml: did not find expected key",
},
}
tmpDir, cleanup := TempDir(t)
defer cleanup()
repoYamlFile := filepath.Join(tmpDir, "repos.yaml")
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
err := ioutil.WriteFile(repoYamlFile, []byte(c.input), 0600)
Ok(t, err)
r := yaml.ParserValidator{}
_, err = r.ReadServerConfig(repoYamlFile)
c.expErr = fmt.Sprintf("parsing %s: %s", repoYamlFile, c.expErr)
ErrEquals(t, c.expErr, err)
})
}
}
func TestReadServerConfigValidation(t *testing.T) {
cases := []struct {
description string
input string
expErr string
}{
{
"workflow doesn't exist",
`
repos:
- id: /.*/
workflow: notexist
`,
`workflow "notexist" is not defined`,
},
{
description: "invalid override",
input: `
repos:
- id: /.*/
allowed_overrides: ["notvalid"]
`,
expErr: "repos: (0: (allowed_overrides: value must be one of [apply_requirements workflow].).).",
},
}
tmpDir, cleanup := TempDir(t)
defer cleanup()
repoYamlFile := filepath.Join(tmpDir, "repos.yaml")
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
err := ioutil.WriteFile(repoYamlFile, []byte(c.input), 0600)
Ok(t, err)
r := yaml.ParserValidator{}
_, err = r.ReadServerConfig(repoYamlFile)
c.expErr = fmt.Sprintf("parsing %s: %s", repoYamlFile, c.expErr)
ErrEquals(t, c.expErr, err)
})
}
}
// 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 }

View File

@@ -0,0 +1,75 @@
package raw
import (
"fmt"
"github.com/go-ozzo/ozzo-validation"
"regexp"
"strings"
)
const ApplyRequirementsKey string = "apply_requirements"
const WorkflowKey string = "workflow"
const CustomWorkflowsKey string = "workflows"
const AllowedOverridesKey string = "allowed_overrides"
type RepoConfig struct {
Repos []Repo `yaml:"repos"`
Workflows map[string]Workflow `yaml:"workflows"`
}
type Repo struct {
ID string `yaml:"id"`
ApplyRequirements []string `yaml:"apply_requirements"`
Workflow *string `yaml:"workflow,omitempty"`
AllowedOverrides []string `yaml:"allowed_overrides"`
AllowCustomWorkflows bool `yaml:"allow_custom_workflows"`
}
func (r RepoConfig) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Repos),
validation.Field(&r.Workflows))
}
func (r Repo) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.ID, validation.Required),
validation.Field(&r.AllowedOverrides, validation.By(checkOverrideValues)),
)
}
func checkOverrideValues(values interface{}) error {
if allValues, _ := values.([]string); allValues != nil {
validOverrides := []string{"apply_requirements", "workflow"}
for _, value := range allValues {
for _, validStr := range validOverrides {
if value == validStr {
return nil
}
}
}
return fmt.Errorf("value must be one of %v", validOverrides)
}
return nil
}
func (r *Repo) IsOverrideAllowed(override string) bool {
for _, allowed := range r.AllowedOverrides {
if allowed == override {
return true
}
}
return false
}
func (r *Repo) Matches(repoName string) (bool, error) {
if strings.Index(r.ID, "/") == 0 && strings.LastIndex(r.ID, "/") == len(r.ID)-1 {
matchString := strings.Trim(r.ID, "/")
compiled, err := regexp.Compile(matchString)
if err != nil {
return false, fmt.Errorf("regex compile of repo.ID `%s`: %s", r.ID, err)
}
return compiled.MatchString(repoName), nil
}
return repoName == r.ID, nil
}

View File

@@ -578,6 +578,7 @@ func TestPost_BBServerPullClosed(t *testing.T) {
expRepo := models.Repo{
FullName: "project/repository",
FullNameWithHost: "bbserver.com/project/repository",
Owner: "project",
Name: "repository",
CloneURL: "https://bb-user:bb-token@bbserver.com/scm/proj/repository.git",

View File

@@ -21,6 +21,7 @@ import (
"flag"
"fmt"
"github.com/runatlantis/atlantis/server/events/db"
"github.com/runatlantis/atlantis/server/events/yaml/raw"
"log"
"net/http"
"net/url"
@@ -194,6 +195,21 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
return nil, errors.Wrapf(err,
"parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL)
}
validator := &yaml.ParserValidator{}
// This is a default config that will allow safe keys to be used in atlantis.yaml by default
// but restrict all sensitive keys. This is used if the server is started without --repo-config.
repoConfig := raw.RepoConfig{
Repos: []raw.Repo{{ID: "/.*/"}},
}
if userConfig.RepoConfig != "" {
repoConfig, err = validator.ReadServerConfig(userConfig.RepoConfig)
if err != nil {
return nil, err
}
}
underlyingRouter := mux.NewRouter()
router := &Router{
AtlantisURL: parsedURL,
@@ -235,12 +251,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
AllowForkPRs: userConfig.AllowForkPRs,
AllowForkPRsFlag: config.AllowForkPRsFlag,
ProjectCommandBuilder: &events.DefaultProjectCommandBuilder{
ParserValidator: &yaml.ParserValidator{},
ParserValidator: validator,
ProjectFinder: &events.DefaultProjectFinder{},
VCSClient: vcsClient,
WorkingDir: workingDir,
WorkingDirLocker: workingDirLocker,
AllowRepoConfig: userConfig.AllowRepoConfig,
RepoConfig: repoConfig,
AllowRepoConfigFlag: config.AllowRepoConfigFlag,
PendingPlanFinder: pendingPlanFinder,
CommentBuilder: commentParser,

View File

@@ -16,10 +16,13 @@ package server_test
import (
"bytes"
"errors"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/yaml/raw"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
"time"
@@ -44,6 +47,34 @@ func TestNewServer(t *testing.T) {
Ok(t, err)
}
func TestRepoConfig(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
Ok(t, err)
repoYaml := `
repos:
- id: "https://github.com/runatlantis/atlantis"
`
expConfig := raw.RepoConfig{
Repos: []raw.Repo{
{
ID: "https://github.com/runatlantis/atlantis",
},
},
}
repoFileLocation := filepath.Join(tmpDir, "repos.yaml")
err = ioutil.WriteFile(repoFileLocation, []byte(repoYaml), 0600)
Ok(t, err)
s, err := server.NewServer(server.UserConfig{
DataDir: tmpDir,
RepoConfig: repoFileLocation,
AtlantisURL: "http://example.com",
}, server.Config{})
Ok(t, err)
Equals(t, s.CommandRunner.ProjectCommandBuilder.(*events.DefaultProjectCommandBuilder).RepoConfig, expConfig)
}
func TestNewServer_InvalidAtlantisURL(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "")
Ok(t, err)

View File

@@ -26,6 +26,7 @@ type UserConfig struct {
GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"`
LogLevel string `mapstructure:"log-level"`
Port int `mapstructure:"port"`
RepoConfig string `mapstructure:"repo-config"`
RepoWhitelist string `mapstructure:"repo-whitelist"`
// RequireApproval is whether to require pull request approval before
// allowing terraform apply's to be run.