feat: Add option to lock on apply instead of plan (#3879)

Co-authored-by: PePe Amengual <jose.amengual@gmail.com>
This commit is contained in:
Pierre Guinoiseau
2024-05-17 17:27:27 +12:00
committed by GitHub
parent 271f89e306
commit 82bec78dff
20 changed files with 524 additions and 117 deletions

View File

@@ -68,7 +68,9 @@ projects:
workspace: default
terraform_version: v0.11.0
delete_source_branch_on_merge: true
repo_locking: true
repo_locking: true # deprecated: use repo_locks instead
repo_locks:
mode: on_plan
custom_policy_check: false
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf", ".terraform.lock.hcl"]
@@ -422,7 +424,9 @@ dir: mydir
workspace: myworkspace
execution_order_group: 0
delete_source_branch_on_merge: false
repo_locking: true
repo_locking: true # deprecated: use repo_locks instead
repo_locks:
mode: on_plan
custom_policy_check: false
autoplan:
terraform_version: 0.11.0
@@ -432,22 +436,23 @@ import_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. |
| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. |
| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. |
| workspace | string | `"default"` | no | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. |
| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. |
| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. |
| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. |
| custom_policy_check | bool | `false` | no | Enable using policy check tools other than Conftest |
| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.md). |
| 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`. |
| plan_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| apply_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| import_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| workflow <br />*(restricted)* | 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. |
| branch | string | none | no | Regex matching projects by the base branch of pull request (the branch the pull request is getting merged into). Only projects that match the PR's branch will be considered. By default, all branches are matched. |
| dir | string | none | **yes** | The directory of this project relative to the repo root. For example if the project was under `./project1` then use `project1`. Use `.` to indicate the repo root. |
| workspace | string | `"default"` | no | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. |
| execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. |
| delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. |
| repo_locking | bool | `true` | no | (deprecated) Get a repository lock in this project when plan. |
| repo_locks | [RepoLocks](#repolocks) | `mode: on_plan` | no | Get a repository lock in this project on plan or apply. See [RepoLocks](#repolocks) for more details. |
| custom_policy_check | bool | `false` | no | Enable using policy check tools other than Conftest |
| autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.md). |
| 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`. |
| plan_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| apply_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| import_requirements<br />*(restricted)* | array\[string\] | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) 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
@@ -462,7 +467,17 @@ enabled: true
when_modified: ["*.tf", "terragrunt.hcl", ".terraform.lock.hcl"]
```
| Key | Type | Default | Required | Description |
|-----------------------|---------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | boolean | `true` | no | Whether autoplanning is enabled for this project. |
| Key | Type | Default | Required | Description |
|-----------------------|-----------------|----------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| enabled | boolean | `true` | no | Whether autoplanning is enabled for this project. |
| when_modified | array\[string\] | `["**/*.tf*"]` | 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. See [Autoplanning](autoplanning.md). Paths are relative to the project's dir. |
### RepoLocks
```yaml
mode: on_apply
```
| Key | Type | Default | Required | Description |
|------|--------|-----------|----------|---------------------------------------------------------------------------------------------------------------------------------------|
| mode | `Mode` | `on_plan` | no | Whether or not repository locks are enabled for this project on plan or apply. Valid values are `disabled`, `on_plan` and `on_apply`. |

View File

@@ -56,7 +56,7 @@ repos:
# allowed_overrides specifies which keys can be overridden by this repo in
# its atlantis.yaml file.
allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking, custom_policy_check]
allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking, repo_locks, custom_policy_check]
# allowed_workflows specifies which workflows the repos that match
# are allowed to select.
@@ -73,8 +73,14 @@ repos:
# repo_locking defines whether lock repository when planning.
# If true (default), atlantis try to get a lock.
# deprecated: use repo_locks instead
repo_locking: true
# repo_locks defines whether the repository would be locked on apply instead of plan, or disabled
# Valid values are on_plan (default), on_apply or disabled.
repo_locks:
mode: on_plan
# custom_policy_check defines whether policy checking tools besides Conftest are enabled in checks
# If false (default), only Conftest JSON output is allowed
custom_policy_check: false
@@ -518,23 +524,24 @@ If you set a workflow with the key `default`, it will override this.
### Repo
| Key | Type | Default | Required | Description |
|-------------------------------|--------------|---------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | string | none | yes | Value can be a regular expression when specified as /&lt;regex&gt;/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
| branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched |
| repo_config_file | string | none | no | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names. |
| workflow | string | none | no | A custom workflow. |
| plan_requirements | []string | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| import_requirements | []string | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge`,`repo_locking`, and `custom_policy_check` |
| allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. |
| allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.md). |
| delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge. |
| repo_locking | bool | false | no | Whether or not to get a lock. |
| policy_check | bool | false | no | Whether or not to run policy checks on this repository. |
| custom_policy_check | bool | false | no | Whether or not to enable custom policy check tools outside of Conftest on this repository. |
| autodiscover | AutoDiscover | none | no | Auto discover settings for this repo |
| Key | Type | Default | Required | Description |
|-------------------------------|-------------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | string | none | yes | Value can be a regular expression when specified as /&lt;regex&gt;/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
| branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched |
| repo_config_file | string | none | no | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names. |
| workflow | string | none | no | A custom workflow. |
| plan_requirements | []string | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| import_requirements | []string | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.md) for more details. |
| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge`,`repo_locking`, `repo_locks`, and `custom_policy_check` |
| allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. |
| allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.md). |
| delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge. |
| repo_locking | bool | false | no | (deprecated) Whether or not to get a lock. |
| repo_locks | [RepoLocks](#repolocks) | `mode: on_plan` | no | Whether or not repository locks are enabled for this project on plan or apply. See [RepoLocks](#repolocks) for more details. |
| policy_check | bool | false | no | Whether or not to run policy checks on this repository. |
| custom_policy_check | bool | false | no | Whether or not to enable custom policy check tools outside of Conftest on this repository. |
| autodiscover | AutoDiscover | none | no | Auto discover settings for this repo |
:::tip Notes
@@ -571,6 +578,16 @@ If you set a workflow with the key `default`, it will override this.
by the `id: github.com/owner/repo` config because it didn't define that key.
:::
### RepoLocks
```yaml
mode: on_apply
```
| Key | Type | Default | Required | Description |
|------|--------|-----------|----------|---------------------------------------------------------------------------------------------------------------------------------------|
| mode | `Mode` | `on_plan` | no | Whether or not repository locks are enabled for this project on plan or apply. Valid values are `disabled`, `on_plan` and `on_apply`. |
### Policies
| Key | Type | Default | Required | Description |

View File

@@ -1286,7 +1286,7 @@ func TestParseGlobalCfg(t *testing.T) {
input: `repos:
- id: /.*/
allowed_overrides: [invalid]`,
expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\", \"repo_locking\", \"policy_check\", and \"custom_policy_check\" are supported.).).",
expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\", \"repo_locking\", \"repo_locks\", \"policy_check\", and \"custom_policy_check\" are supported.).).",
},
"invalid plan_requirement": {
input: `repos:
@@ -1322,6 +1322,22 @@ func TestParseGlobalCfg(t *testing.T) {
Workflows: defaultCfg.Workflows,
},
},
"disable repo locks": {
input: `repos:
- id: /.*/
repo_locks:
mode: disabled`,
exp: valid.GlobalCfg{
Repos: []valid.Repo{
defaultCfg.Repos[0],
{
IDRegex: regexp.MustCompile(".*"),
RepoLocks: &valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},
},
},
Workflows: defaultCfg.Workflows,
},
},
"no workflows key": {
input: `repos: []`,
exp: defaultCfg,
@@ -1399,6 +1415,8 @@ repos:
policy_check: true
autodiscover:
mode: enabled
repo_locks:
mode: on_apply
- id: /.*/
branch: /(master|main)/
pre_workflow_hooks:
@@ -1408,6 +1426,8 @@ repos:
policy_check: false
autodiscover:
mode: disabled
repo_locks:
mode: disabled
workflows:
custom1:
plan:
@@ -1455,6 +1475,7 @@ policies:
AllowCustomWorkflows: Bool(true),
PolicyCheck: Bool(true),
AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode},
RepoLocks: &valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},
},
{
IDRegex: regexp.MustCompile(".*"),
@@ -1463,6 +1484,7 @@ policies:
PostWorkflowHooks: postWorkflowHooks,
PolicyCheck: Bool(false),
AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverDisabledMode},
RepoLocks: &valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},
},
},
Workflows: map[string]valid.Workflow{
@@ -1570,7 +1592,7 @@ workflows:
AllowedOverrides: []string{},
AllowCustomWorkflows: Bool(false),
DeleteSourceBranchOnMerge: Bool(false),
RepoLocking: Bool(true),
RepoLocks: &valid.DefaultRepoLocks,
PolicyCheck: Bool(false),
CustomPolicyCheck: Bool(false),
AutoDiscover: raw.DefaultAutoDiscover(),
@@ -1721,6 +1743,9 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
"allow_custom_workflows": true,
"autodiscover": {
"mode": "enabled"
},
"repo_locks": {
"mode": "on_apply"
}
},
{
@@ -1782,6 +1807,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
AllowedOverrides: []string{"workflow", "apply_requirements"},
AllowCustomWorkflows: Bool(true),
AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode},
RepoLocks: &valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},
},
{
ID: "github.com/owner/repo",
@@ -1790,6 +1816,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
AllowedOverrides: nil,
AllowCustomWorkflows: nil,
AutoDiscover: nil,
RepoLocks: nil,
},
},
Workflows: map[string]valid.Workflow{

View File

@@ -34,6 +34,7 @@ type Repo struct {
AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty" json:"delete_source_branch_on_merge,omitempty"`
RepoLocking *bool `yaml:"repo_locking,omitempty" json:"repo_locking,omitempty"`
RepoLocks *RepoLocks `yaml:"repo_locks,omitempty" json:"repo_locks,omitempty"`
PolicyCheck *bool `yaml:"policy_check,omitempty" json:"policy_check,omitempty"`
CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty" json:"custom_policy_check,omitempty"`
AutoDiscover *AutoDiscover `yaml:"autodiscover,omitempty" json:"autodiscover,omitempty"`
@@ -194,8 +195,8 @@ func (r Repo) Validate() error {
overridesValid := func(value interface{}) error {
overrides := value.([]string)
for _, o := range overrides {
if o != valid.PlanRequirementsKey && o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey {
return fmt.Errorf("%q is not a valid override, only %q, %q, %q, %q, %q, %q, %q, and %q are supported", o, valid.PlanRequirementsKey, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey, valid.PolicyCheckKey, valid.CustomPolicyCheckKey)
if o != valid.PlanRequirementsKey && o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey && o != valid.RepoLocksKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey {
return fmt.Errorf("%q is not a valid override, only %q, %q, %q, %q, %q, %q, %q, %q, and %q are supported", o, valid.PlanRequirementsKey, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey, valid.RepoLocksKey, valid.PolicyCheckKey, valid.CustomPolicyCheckKey)
}
}
return nil
@@ -220,6 +221,14 @@ func (r Repo) Validate() error {
return nil
}
repoLocksValid := func(value interface{}) error {
repoLocks := value.(*RepoLocks)
if repoLocks != nil {
return repoLocks.Validate()
}
return nil
}
return validation.ValidateStruct(&r,
validation.Field(&r.ID, validation.Required, validation.By(idValid)),
validation.Field(&r.Branch, validation.By(branchValid)),
@@ -231,6 +240,7 @@ func (r Repo) Validate() error {
validation.Field(&r.Workflow, validation.By(workflowExists)),
validation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)),
validation.Field(&r.AutoDiscover, validation.By(autoDiscoverValid)),
validation.Field(&r.RepoLocks, validation.By(repoLocksValid)),
)
}
@@ -330,6 +340,11 @@ OuterGlobalImportReqs:
autoDiscover = r.AutoDiscover.ToValid()
}
var repoLocks *valid.RepoLocks
if r.RepoLocks != nil {
repoLocks = r.RepoLocks.ToValid()
}
return valid.Repo{
ID: id,
IDRegex: idRegex,
@@ -346,6 +361,7 @@ OuterGlobalImportReqs:
AllowCustomWorkflows: r.AllowCustomWorkflows,
DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,
RepoLocking: r.RepoLocking,
RepoLocks: repoLocks,
PolicyCheck: r.PolicyCheck,
CustomPolicyCheck: r.CustomPolicyCheck,
AutoDiscover: autoDiscover,

View File

@@ -21,22 +21,23 @@ const (
)
type Project struct {
Name *string `yaml:"name,omitempty"`
Branch *string `yaml:"branch,omitempty"`
Dir *string `yaml:"dir,omitempty"`
Workspace *string `yaml:"workspace,omitempty"`
Workflow *string `yaml:"workflow,omitempty"`
TerraformVersion *string `yaml:"terraform_version,omitempty"`
Autoplan *Autoplan `yaml:"autoplan,omitempty"`
PlanRequirements []string `yaml:"plan_requirements,omitempty"`
ApplyRequirements []string `yaml:"apply_requirements,omitempty"`
ImportRequirements []string `yaml:"import_requirements,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"`
RepoLocking *bool `yaml:"repo_locking,omitempty"`
ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"`
PolicyCheck *bool `yaml:"policy_check,omitempty"`
CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty"`
Name *string `yaml:"name,omitempty"`
Branch *string `yaml:"branch,omitempty"`
Dir *string `yaml:"dir,omitempty"`
Workspace *string `yaml:"workspace,omitempty"`
Workflow *string `yaml:"workflow,omitempty"`
TerraformVersion *string `yaml:"terraform_version,omitempty"`
Autoplan *Autoplan `yaml:"autoplan,omitempty"`
PlanRequirements []string `yaml:"plan_requirements,omitempty"`
ApplyRequirements []string `yaml:"apply_requirements,omitempty"`
ImportRequirements []string `yaml:"import_requirements,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"`
RepoLocking *bool `yaml:"repo_locking,omitempty"`
RepoLocks *RepoLocks `yaml:"repo_locks,omitempty"`
ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"`
PolicyCheck *bool `yaml:"policy_check,omitempty"`
CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty"`
}
func (p Project) Validate() error {
@@ -139,6 +140,10 @@ func (p Project) ToValid() valid.Project {
v.RepoLocking = p.RepoLocking
}
if p.RepoLocks != nil {
v.RepoLocks = p.RepoLocks.ToValid()
}
if p.ExecutionOrderGroup != nil {
v.ExecutionOrderGroup = *p.ExecutionOrderGroup
}

View File

@@ -331,6 +331,7 @@ func TestProject_Validate(t *testing.T) {
func TestProject_ToValid(t *testing.T) {
tfVersionPointEleven, _ := version.NewVersion("v0.11.0")
repoLocksOnApply := valid.RepoLocksOnApplyMode
cases := []struct {
description string
input raw.Project
@@ -366,6 +367,9 @@ func TestProject_ToValid(t *testing.T) {
WhenModified: []string{"hi"},
Enabled: Bool(false),
},
RepoLocks: &raw.RepoLocks{
Mode: &repoLocksOnApply,
},
ApplyRequirements: []string{"approved"},
Name: String("myname"),
ExecutionOrderGroup: Int(10),
@@ -379,6 +383,9 @@ func TestProject_ToValid(t *testing.T) {
WhenModified: []string{"hi"},
Enabled: false,
},
RepoLocks: &valid.RepoLocks{
Mode: repoLocksOnApply,
},
ApplyRequirements: []string{"approved"},
Name: String("myname"),
ExecutionOrderGroup: 10,

View File

@@ -27,6 +27,7 @@ type RepoCfg struct {
EmojiReaction *string `yaml:"emoji_reaction,omitempty"`
AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes,omitempty"`
AbortOnExcecutionOrderFail *bool `yaml:"abort_on_execution_order_fail,omitempty"`
RepoLocks *RepoLocks `yaml:"repo_locks,omitempty"`
}
func (r RepoCfg) Validate() error {
@@ -77,6 +78,10 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
autoDiscover = r.AutoDiscover.ToValid()
}
var repoLocks *valid.RepoLocks
if r.RepoLocks != nil {
repoLocks = r.RepoLocks.ToValid()
}
return valid.RepoCfg{
Version: *r.Version,
Projects: validProjects,
@@ -90,5 +95,6 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
AllowedRegexpPrefixes: r.AllowedRegexpPrefixes,
EmojiReaction: emojiReaction,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
RepoLocks: repoLocks,
}
}

View File

@@ -11,6 +11,8 @@ import (
func TestConfig_UnmarshalYAML(t *testing.T) {
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
repoLocksDisabled := valid.RepoLocksDisabledMode
repoLocksOnApply := valid.RepoLocksOnApplyMode
cases := []struct {
description string
input string
@@ -130,6 +132,8 @@ autodiscover:
mode: enabled
parallel_apply: true
parallel_plan: false
repo_locks:
mode: on_apply
projects:
- dir: mydir
workspace: myworkspace
@@ -139,6 +143,8 @@ projects:
enabled: false
when_modified: []
apply_requirements: [mergeable]
repo_locks:
mode: disabled
workflows:
default:
plan:
@@ -156,6 +162,7 @@ allowed_regexp_prefixes:
Automerge: Bool(true),
ParallelApply: Bool(true),
ParallelPlan: Bool(false),
RepoLocks: &raw.RepoLocks{Mode: &repoLocksOnApply},
Projects: []raw.Project{
{
Dir: String("mydir"),
@@ -167,6 +174,7 @@ allowed_regexp_prefixes:
Enabled: Bool(false),
},
ApplyRequirements: []string{"mergeable"},
RepoLocks: &raw.RepoLocks{Mode: &repoLocksDisabled},
},
},
Workflows: map[string]raw.Workflow{
@@ -236,6 +244,7 @@ func TestConfig_Validate(t *testing.T) {
func TestConfig_ToValid(t *testing.T) {
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
repoLocksOnApply := valid.RepoLocksOnApplyMode
cases := []struct {
description string
input raw.RepoCfg
@@ -256,12 +265,14 @@ func TestConfig_ToValid(t *testing.T) {
AutoDiscover: &raw.AutoDiscover{},
Workflows: map[string]raw.Workflow{},
Projects: []raw.Project{},
RepoLocks: &raw.RepoLocks{},
},
exp: valid.RepoCfg{
Version: 2,
AutoDiscover: raw.DefaultAutoDiscover(),
Workflows: map[string]valid.Workflow{},
Projects: nil,
RepoLocks: &valid.DefaultRepoLocks,
},
},
{
@@ -333,6 +344,30 @@ func TestConfig_ToValid(t *testing.T) {
Workflows: map[string]valid.Workflow{},
},
},
{
description: "repo_locks omitted",
input: raw.RepoCfg{
Version: Int(2),
},
exp: valid.RepoCfg{
Version: 2,
Workflows: map[string]valid.Workflow{},
},
},
{
description: "repo_locks included",
input: raw.RepoCfg{
Version: Int(2),
RepoLocks: &raw.RepoLocks{Mode: &repoLocksOnApply},
},
exp: valid.RepoCfg{
Version: 2,
RepoLocks: &valid.RepoLocks{
Mode: valid.RepoLocksOnApplyMode,
},
Workflows: map[string]valid.Workflow{},
},
},
{
description: "only plan stage set",
input: raw.RepoCfg{
@@ -372,6 +407,9 @@ func TestConfig_ToValid(t *testing.T) {
AutoDiscover: &raw.AutoDiscover{
Mode: &autoDiscoverEnabled,
},
RepoLocks: &raw.RepoLocks{
Mode: &repoLocksOnApply,
},
Workflows: map[string]raw.Workflow{
"myworkflow": {
Apply: &raw.Stage{
@@ -424,6 +462,9 @@ func TestConfig_ToValid(t *testing.T) {
AutoDiscover: &valid.AutoDiscover{
Mode: valid.AutoDiscoverEnabledMode,
},
RepoLocks: &valid.RepoLocks{
Mode: valid.RepoLocksOnApplyMode,
},
Workflows: map[string]valid.Workflow{
"myworkflow": {
Name: "myworkflow",

View File

@@ -0,0 +1,30 @@
package raw
import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/runatlantis/atlantis/server/core/config/valid"
)
type RepoLocks struct {
Mode *valid.RepoLocksMode `yaml:"mode,omitempty"`
}
func (a RepoLocks) ToValid() *valid.RepoLocks {
var v valid.RepoLocks
if a.Mode != nil {
v.Mode = *a.Mode
} else {
v.Mode = valid.DefaultRepoLocksMode
}
return &v
}
func (a RepoLocks) Validate() error {
res := validation.ValidateStruct(&a,
// If a.Mode is nil, this should still pass validation.
validation.Field(&a.Mode, validation.In(valid.RepoLocksDisabledMode, valid.RepoLocksOnPlanMode, valid.RepoLocksOnApplyMode)),
)
return res
}

View File

@@ -0,0 +1,128 @@
package raw_test
import (
"testing"
"github.com/runatlantis/atlantis/server/core/config/raw"
"github.com/runatlantis/atlantis/server/core/config/valid"
. "github.com/runatlantis/atlantis/testing"
)
func TestRepoLocks_UnmarshalYAML(t *testing.T) {
repoLocksOnPlan := valid.RepoLocksOnPlanMode
cases := []struct {
description string
input string
exp raw.RepoLocks
}{
{
description: "omit unset fields",
input: "",
exp: raw.RepoLocks{
Mode: nil,
},
},
{
description: "all fields set",
input: `
mode: on_plan
`,
exp: raw.RepoLocks{
Mode: &repoLocksOnPlan,
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
var a raw.RepoLocks
err := unmarshalString(c.input, &a)
Ok(t, err)
Equals(t, c.exp, a)
})
}
}
func TestRepoLocks_Validate(t *testing.T) {
repoLocksDisabled := valid.RepoLocksDisabledMode
repoLocksOnPlan := valid.RepoLocksOnPlanMode
repoLocksOnApply := valid.RepoLocksOnApplyMode
randomString := valid.RepoLocksMode("random_string")
cases := []struct {
description string
input raw.RepoLocks
errContains *string
}{
{
description: "nothing set",
input: raw.RepoLocks{},
errContains: nil,
},
{
description: "mode set to disabled",
input: raw.RepoLocks{
Mode: &repoLocksDisabled,
},
errContains: nil,
},
{
description: "mode set to on_plan",
input: raw.RepoLocks{
Mode: &repoLocksOnPlan,
},
errContains: nil,
},
{
description: "mode set to on_apply",
input: raw.RepoLocks{
Mode: &repoLocksOnApply,
},
errContains: nil,
},
{
description: "mode set to random string",
input: raw.RepoLocks{
Mode: &randomString,
},
errContains: String("valid value"),
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
if c.errContains == nil {
Ok(t, c.input.Validate())
} else {
ErrContains(t, *c.errContains, c.input.Validate())
}
})
}
}
func TestRepoLocks_ToValid(t *testing.T) {
repoLocksOnApply := valid.RepoLocksOnApplyMode
cases := []struct {
description string
input raw.RepoLocks
exp *valid.RepoLocks
}{
{
description: "nothing set",
input: raw.RepoLocks{},
exp: &valid.DefaultRepoLocks,
},
{
description: "value set",
input: raw.RepoLocks{
Mode: &repoLocksOnApply,
},
exp: &valid.RepoLocks{
Mode: valid.RepoLocksOnApplyMode,
},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
Equals(t, c.exp, c.input.ToValid())
})
}
}

View File

@@ -23,6 +23,7 @@ const AllowCustomWorkflowsKey = "allow_custom_workflows"
const DefaultWorkflowName = "default"
const DeleteSourceBranchOnMergeKey = "delete_source_branch_on_merge"
const RepoLockingKey = "repo_locking"
const RepoLocksKey = "repo_locks"
const PolicyCheckKey = "policy_check"
const CustomPolicyCheckKey = "custom_policy_check"
const AutoDiscoverKey = "autodiscover"
@@ -80,6 +81,7 @@ type Repo struct {
AllowCustomWorkflows *bool
DeleteSourceBranchOnMerge *bool
RepoLocking *bool
RepoLocks *RepoLocks
PolicyCheck *bool
CustomPolicyCheck *bool
AutoDiscover *AutoDiscover
@@ -102,7 +104,7 @@ type MergedProjectCfg struct {
PolicySets PolicySets
DeleteSourceBranchOnMerge bool
ExecutionOrderGroup int
RepoLocking bool
RepoLocks RepoLocks
PolicyCheck bool
CustomPolicyCheck bool
}
@@ -207,11 +209,11 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
allowCustomWorkflows := false
deleteSourceBranchOnMerge := false
repoLockingKey := true
repoLocks := DefaultRepoLocks
customPolicyCheck := false
autoDiscover := AutoDiscover{Mode: AutoDiscoverAutoMode}
if args.AllowAllRepoSettings {
allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey}
allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey}
allowCustomWorkflows = true
}
@@ -231,7 +233,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
AllowedOverrides: allowedOverrides,
AllowCustomWorkflows: &allowCustomWorkflows,
DeleteSourceBranchOnMerge: &deleteSourceBranchOnMerge,
RepoLocking: &repoLockingKey,
RepoLocks: &repoLocks,
PolicyCheck: &policyCheck,
CustomPolicyCheck: &customPolicyCheck,
AutoDiscover: &autoDiscover,
@@ -271,8 +273,7 @@ func (r Repo) IDString() string {
// final config. It assumes that all configs have been validated.
func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg {
log.Debug("MergeProjectCfg started")
planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
// If repos are allowed to override certain keys then override them.
for _, key := range allowedOverrides {
switch key {
@@ -335,8 +336,26 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
case RepoLockingKey:
if proj.RepoLocking != nil {
log.Debug("overriding server-defined %s with repo settings: [%t]", RepoLockingKey, *proj.RepoLocking)
repoLocking = *proj.RepoLocking
if *proj.RepoLocking && repoLocks.Mode == RepoLocksDisabledMode {
repoLocks.Mode = DefaultRepoLocksMode
} else if !*proj.RepoLocking {
repoLocks.Mode = RepoLocksDisabledMode
}
}
case RepoLocksKey:
//We check whether the server configured value and repo-root level
//config is different. If it is then we change to the more granular.
if rCfg.RepoLocks != nil && repoLocks.Mode != rCfg.RepoLocks.Mode {
log.Debug("overriding server-defined %s with repo settings: [%#v]", RepoLocksKey, rCfg.RepoLocks)
repoLocks = *rCfg.RepoLocks
}
//Then we check whether the more granular project based config is
//different. If it is then we set it.
if proj.RepoLocks != nil && repoLocks.Mode != proj.RepoLocks.Mode {
log.Debug("overriding repo-root-defined %s with repo settings: [%#v]", RepoLocksKey, *proj.RepoLocks)
repoLocks = *proj.RepoLocks
}
log.Debug("merged repoLocks: [%#v]", repoLocks)
case PolicyCheckKey:
if proj.PolicyCheck != nil {
log.Debug("overriding server-defined %s with repo settings: [%t]", PolicyCheckKey, *proj.PolicyCheck)
@@ -369,7 +388,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
PolicySets: g.PolicySets,
DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,
ExecutionOrderGroup: proj.ExecutionOrderGroup,
RepoLocking: repoLocking,
RepoLocks: repoLocks,
PolicyCheck: policyCheck,
CustomPolicyCheck: customPolicyCheck,
}
@@ -379,7 +398,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
// repo with id repoID. It is used when there is no repo config.
func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg {
log.Debug("building config based on server-side config")
planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
return MergedProjectCfg{
PlanRequirements: planReqs,
ApplyRequirements: applyReqs,
@@ -392,7 +411,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo
TerraformVersion: nil,
PolicySets: g.PolicySets,
DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,
RepoLocking: repoLocking,
RepoLocks: repoLocks,
PolicyCheck: policyCheck,
CustomPolicyCheck: customPolicyCheck,
}
@@ -412,7 +431,6 @@ func (g GlobalCfg) RepoAutoDiscoverCfg(repoID string) *AutoDiscover {
// ValidateRepoCfg validates that rCfg for repo with id repoID is valid based
// on our global config.
func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {
mapContainsF := func(m map[string]Workflow, key string) bool {
for k := range m {
if k == key {
@@ -450,6 +468,9 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {
if p.RepoLocking != nil && !utils.SlicesContains(allowedOverrides, RepoLockingKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", RepoLockingKey, AllowedOverridesKey, RepoLockingKey)
}
if p.RepoLocks != nil && !utils.SlicesContains(allowedOverrides, RepoLocksKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", RepoLocksKey, AllowedOverridesKey, RepoLocksKey)
}
if p.CustomPolicyCheck != nil && !utils.SlicesContains(allowedOverrides, CustomPolicyCheckKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", CustomPolicyCheckKey, AllowedOverridesKey, CustomPolicyCheckKey)
}
@@ -511,7 +532,7 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {
}
// getMatchingCfg returns the key settings for repoID.
func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover) {
func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocks RepoLocks, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover) {
toLog := make(map[string]string)
traceF := func(repoIdx int, repoID string, key string, val interface{}) string {
from := "default server config"
@@ -535,8 +556,10 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla
// Can't use raw.DefaultAutoDiscoverMode() because of an import cycle. Should refactor to avoid that.
autoDiscover = AutoDiscover{Mode: AutoDiscoverAutoMode}
repoLocking := true
repoLocks = DefaultRepoLocks
for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey, CustomPolicyCheckKey} {
for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, CustomPolicyCheckKey} {
for i, repo := range g.Repos {
if repo.IDMatches(repoID) {
switch key {
@@ -580,6 +603,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla
toLog[RepoLockingKey] = traceF(i, repo.IDString(), RepoLockingKey, *repo.RepoLocking)
repoLocking = *repo.RepoLocking
}
case RepoLocksKey:
if repo.RepoLocks != nil {
toLog[RepoLocksKey] = traceF(i, repo.IDString(), RepoLocksKey, repo.RepoLocks.Mode)
repoLocks = *repo.RepoLocks
}
case PolicyCheckKey:
if repo.PolicyCheck != nil {
toLog[PolicyCheckKey] = traceF(i, repo.IDString(), PolicyCheckKey, *repo.PolicyCheck)
@@ -602,6 +630,10 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla
for _, l := range toLog {
log.Debug(l)
}
// repoLocking is deprecated and enabled by default, disable repo locks if it is explicitly disabled
if !repoLocking {
repoLocks.Mode = RepoLocksDisabledMode
}
return
}

View File

@@ -80,7 +80,7 @@ func TestNewGlobalCfg(t *testing.T) {
AllowedOverrides: []string{},
AllowCustomWorkflows: Bool(false),
DeleteSourceBranchOnMerge: Bool(false),
RepoLocking: Bool(true),
RepoLocks: &valid.DefaultRepoLocks,
PolicyCheck: Bool(false),
CustomPolicyCheck: Bool(false),
AutoDiscover: raw.DefaultAutoDiscover(),
@@ -129,7 +129,7 @@ func TestNewGlobalCfg(t *testing.T) {
if c.allowAllRepoSettings {
exp.Repos[0].AllowCustomWorkflows = Bool(true)
exp.Repos[0].AllowedOverrides = []string{"plan_requirements", "apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking", "policy_check"}
exp.Repos[0].AllowedOverrides = []string{"plan_requirements", "apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking", "repo_locks", "policy_check"}
}
if c.policyCheckEnabled {
exp.Repos[0].PlanRequirements = append(exp.Repos[0].PlanRequirements, "policies_passed")
@@ -569,7 +569,7 @@ policies:
Workspace: "default",
Name: "",
AutoplanEnabled: false,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -618,7 +618,7 @@ policies:
Workspace: "default",
Name: "",
AutoplanEnabled: false,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -707,7 +707,7 @@ workflows:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -737,7 +737,7 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -767,7 +767,7 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -798,7 +798,7 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
PolicyCheck: true,
},
@@ -829,7 +829,7 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
PolicyCheck: false,
},
@@ -860,7 +860,7 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -877,7 +877,6 @@ repos:
PlanRequirements: []string{},
ApplyRequirements: []string{},
ImportRequirements: []string{},
RepoLocking: Bool(true),
CustomPolicyCheck: Bool(false),
},
repoWorkflows: nil,
@@ -891,7 +890,39 @@ repos:
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: false,
RepoLocks: valid.RepoLocks{Mode: valid.RepoLocksDisabledMode},
CustomPolicyCheck: false,
},
},
"repo-side repo_locks win out if allowed": {
gCfg: `
repos:
- id: /.*/
repo_locks:
mode: on_apply
`,
repoID: "github.com/owner/repo",
proj: valid.Project{
Dir: ".",
Workspace: "default",
PlanRequirements: []string{},
ApplyRequirements: []string{},
ImportRequirements: []string{},
RepoLocks: &valid.DefaultRepoLocks,
CustomPolicyCheck: Bool(false),
},
repoWorkflows: nil,
exp: valid.MergedProjectCfg{
PlanRequirements: []string{},
ApplyRequirements: []string{},
ImportRequirements: []string{},
Workflow: defaultWorkflow,
RepoRelDir: ".",
Workspace: "default",
Name: "",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocks: valid.RepoLocks{Mode: valid.RepoLocksOnApplyMode},
CustomPolicyCheck: false,
},
},
@@ -928,7 +959,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -955,7 +986,7 @@ repos:
Name: "myname",
AutoplanEnabled: true,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -984,7 +1015,7 @@ repos:
AutoplanEnabled: true,
PolicySets: emptyPolicySets,
ExecutionOrderGroup: 10,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
CustomPolicyCheck: false,
},
},
@@ -1172,7 +1203,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
PolicyCheck: false,
CustomPolicyCheck: false,
},
@@ -1212,7 +1243,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
PolicyCheck: true,
CustomPolicyCheck: false,
},
@@ -1253,7 +1284,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
PolicyCheck: false,
CustomPolicyCheck: false,
},
@@ -1294,7 +1325,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
PolicyCheck: false,
CustomPolicyCheck: false,
},
@@ -1335,7 +1366,7 @@ repos:
Name: "myname",
AutoplanEnabled: false,
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocks: valid.DefaultRepoLocks,
PolicyCheck: true, // Project will have policy check as true but since it is globally disable it wont actually run
CustomPolicyCheck: false,
},

View File

@@ -24,7 +24,7 @@ type RepoCfg struct {
ParallelPlan *bool
ParallelPolicyCheck *bool
DeleteSourceBranchOnMerge *bool
RepoLocking *bool
RepoLocks *RepoLocks
CustomPolicyCheck *bool
EmojiReaction string
AllowedRegexpPrefixes []string
@@ -154,6 +154,7 @@ type Project struct {
DependsOn []string
DeleteSourceBranchOnMerge *bool
RepoLocking *bool
RepoLocks *RepoLocks
ExecutionOrderGroup int
PolicyCheck *bool
CustomPolicyCheck *bool

View File

@@ -0,0 +1,19 @@
package valid
// RepoLocksMode enum
type RepoLocksMode string
var DefaultRepoLocksMode = RepoLocksOnPlanMode
var DefaultRepoLocks = RepoLocks{
Mode: DefaultRepoLocksMode,
}
const (
RepoLocksDisabledMode RepoLocksMode = "disabled"
RepoLocksOnPlanMode RepoLocksMode = "on_plan"
RepoLocksOnApplyMode RepoLocksMode = "on_apply"
)
type RepoLocks struct {
Mode RepoLocksMode
}

View File

@@ -113,8 +113,8 @@ type ProjectContext struct {
ClearPolicyApproval bool
// DeleteSourceBranchOnMerge will attempt to allow a branch to be deleted when merged (AzureDevOps & GitLab Support Only)
DeleteSourceBranchOnMerge bool
// RepoLocking will get a lock when plan
RepoLocking bool
// Repo locks mode: disabled, on plan or on apply
RepoLocksMode valid.RepoLocksMode
// RepoConfigFile
RepoConfigFile string
// UUID for atlantis logs

View File

@@ -84,7 +84,7 @@ workflows:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"init", "plan"},
expApplySteps: []string{"apply"},
@@ -143,7 +143,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"init", "plan"},
expApplySteps: []string{"apply"},
@@ -204,7 +204,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"init", "plan"},
expApplySteps: []string{"apply"},
@@ -273,7 +273,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"plan"},
expApplySteps: []string{},
@@ -429,7 +429,7 @@ workflows:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"plan"},
expApplySteps: []string{"apply"},
@@ -492,7 +492,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"plan"},
expApplySteps: []string{"apply"},
@@ -558,7 +558,7 @@ workflows:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{},
expApplySteps: []string{},
@@ -609,7 +609,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"plan"},
expApplySteps: []string{"apply"},
@@ -693,9 +693,9 @@ projects:
ErrEquals(t, c.expErr, err)
return
}
ctx := ctxs[0]
Ok(t, err)
ctx := ctxs[0]
// Construct expected steps.
var stepNames []string
@@ -824,7 +824,7 @@ projects:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPlanSteps: []string{"init", "plan"},
expApplySteps: []string{"apply"},
@@ -910,9 +910,9 @@ projects:
ErrEquals(t, c.expErr, err)
return
}
ctx := ctxs[0]
Ok(t, err)
ctx := ctxs[0]
Equals(t, 2, len(ctxs))
// Construct expected steps.
@@ -1005,7 +1005,7 @@ repos:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
},
expPolicyCheckSteps: []string{"show", "policy_check"},
},
@@ -1069,7 +1069,7 @@ workflows:
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
RepoLocking: true,
RepoLocksMode: valid.DefaultRepoLocksMode,
PolicySetTarget: "",
},
expPolicyCheckSteps: []string{"policy_check"},
@@ -1157,9 +1157,8 @@ workflows:
return
}
ctx := ctxs[1]
Ok(t, err)
ctx := ctxs[1]
// Construct expected steps.
var stepNames []string

View File

@@ -273,7 +273,7 @@ func newProjectCommandContext(ctx *command.Context,
EscapedCommentArgs: escapedCommentArgs,
AutomergeEnabled: automergeEnabled,
DeleteSourceBranchOnMerge: projCfg.DeleteSourceBranchOnMerge,
RepoLocking: projCfg.RepoLocking,
RepoLocksMode: projCfg.RepoLocks.Mode,
CustomPolicyCheck: projCfg.CustomPolicyCheck,
ParallelApplyEnabled: parallelApplyEnabled,
ParallelPlanEnabled: parallelPlanEnabled,

View File

@@ -320,7 +320,7 @@ func (p *DefaultProjectCommandRunner) StateRm(ctx command.ProjectContext) comman
func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectContext) (*models.PolicyCheckResults, string, error) {
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocking)
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
}
@@ -417,7 +417,7 @@ func (p *DefaultProjectCommandRunner) doPolicyCheck(ctx command.ProjectContext)
// we will attempt to capture the lock here but fail to get the working directory
// at which point we will unlock again to preserve functionality
// If we fail to capture the lock here (super unlikely) then we error out and the user is forced to replan
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocking)
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
@@ -536,7 +536,7 @@ func (p *DefaultProjectCommandRunner) doPolicyCheck(ctx command.ProjectContext)
func (p *DefaultProjectCommandRunner) doPlan(ctx command.ProjectContext) (*models.PlanSuccess, string, error) {
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocking)
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnPlanMode)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
}
@@ -612,6 +612,16 @@ func (p *DefaultProjectCommandRunner) doApply(ctx command.ProjectContext) (apply
return "", failure, err
}
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode == valid.RepoLocksOnApplyMode)
if err != nil {
return "", "", errors.Wrap(err, "acquiring lock")
}
if !lockAttempt.LockAcquired {
return "", lockAttempt.LockFailureReason, nil
}
ctx.Log.Debug("acquired lock for project")
// Acquire internal lock for the directory we're going to operate in.
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir)
if err != nil {
@@ -682,7 +692,7 @@ func (p *DefaultProjectCommandRunner) doImport(ctx command.ProjectContext) (out
}
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocking)
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode != valid.RepoLocksDisabledMode)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
}
@@ -723,7 +733,7 @@ func (p *DefaultProjectCommandRunner) doStateRm(ctx command.ProjectContext) (out
}
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocking)
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir, ctx.ProjectName), ctx.RepoLocksMode != valid.RepoLocksDisabledMode)
if err != nil {
return nil, "", errors.Wrap(err, "acquiring lock")
}

View File

@@ -424,6 +424,17 @@ func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
Any[models.PullRequest](),
Any[string](),
)).ThenReturn(repoDir, nil)
When(mockLocker.TryLock(
Any[logging.SimpleLogging](),
Any[models.PullRequest](),
Any[models.User](),
Any[string](),
Any[models.Project](),
AnyBool(),
)).ThenReturn(&events.TryLockResponse{
LockAcquired: true,
LockKey: "lock-key",
}, nil)
ctx := command.ProjectContext{
Log: logging.NewNoopLogger(t),
@@ -495,6 +506,17 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {
Any[models.PullRequest](),
Any[string](),
)).ThenReturn(repoDir, nil)
When(mockLocker.TryLock(
Any[logging.SimpleLogging](),
Any[models.PullRequest](),
Any[models.User](),
Any[string](),
Any[models.Project](),
AnyBool(),
)).ThenReturn(&events.TryLockResponse{
LockAcquired: true,
LockKey: "lock-key",
}, nil)
ctx := command.ProjectContext{
Log: logging.NewNoopLogger(t),

View File

@@ -90,7 +90,8 @@ func NewGithubClient(hostname string, credentials GithubCredentials, config Gith
graphqlURL = "https://api.github.com/graphql"
} else {
apiURL := resolveGithubAPIURL(hostname)
client, err = github.NewEnterpriseClient(apiURL.String(), apiURL.String(), transport)
// TODO: Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead
client, err = github.NewEnterpriseClient(apiURL.String(), apiURL.String(), transport) //nolint:staticcheck
if err != nil {
return nil, err
}