mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-07 20:28:37 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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. |
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
126
runatlantis.io/docs/repos-yaml-reference.md
Normal file
126
runatlantis.io/docs/repos-yaml-reference.md
Normal 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 /<regex>/ 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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
75
server/events/yaml/raw/repo_config.go
Normal file
75
server/events/yaml/raw/repo_config.go
Normal 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user