diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md
index 018794a0a3..0c9af490d6 100644
--- a/runatlantis.io/docs/repo-level-atlantis-yaml.md
+++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md
@@ -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
*(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
*(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
*(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
*(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
*(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
*(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
*(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
*(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`. |
diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md
index 031df79fce..f7cd73595c 100644
--- a/runatlantis.io/docs/server-side-repo-config.md
+++ b/runatlantis.io/docs/server-side-repo-config.md
@@ -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 /<regex>/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
-| branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched |
-| repo_config_file | string | none | no | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names. |
-| workflow | string | none | no | A custom workflow. |
-| 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 /<regex>/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
+| branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched |
+| repo_config_file | string | none | no | Repo config file path in this repo. By default, use `atlantis.yaml` which is located on repository root. When multiple atlantis servers work with the same repo, please set different file names. |
+| workflow | string | none | no | A custom workflow. |
+| 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 |
diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go
index 08be7173b8..24b66e4db7 100644
--- a/server/core/config/parser_validator_test.go
+++ b/server/core/config/parser_validator_test.go
@@ -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{
diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go
index b795294239..445332ec39 100644
--- a/server/core/config/raw/global_cfg.go
+++ b/server/core/config/raw/global_cfg.go
@@ -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,
diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go
index d73062cef3..a20aad10af 100644
--- a/server/core/config/raw/project.go
+++ b/server/core/config/raw/project.go
@@ -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
}
diff --git a/server/core/config/raw/project_test.go b/server/core/config/raw/project_test.go
index 72a8dd78d0..3c69177f96 100644
--- a/server/core/config/raw/project_test.go
+++ b/server/core/config/raw/project_test.go
@@ -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,
diff --git a/server/core/config/raw/repo_cfg.go b/server/core/config/raw/repo_cfg.go
index f3a688725d..e7b4b273ab 100644
--- a/server/core/config/raw/repo_cfg.go
+++ b/server/core/config/raw/repo_cfg.go
@@ -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,
}
}
diff --git a/server/core/config/raw/repo_cfg_test.go b/server/core/config/raw/repo_cfg_test.go
index 31d01101dd..b3844ee68c 100644
--- a/server/core/config/raw/repo_cfg_test.go
+++ b/server/core/config/raw/repo_cfg_test.go
@@ -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",
diff --git a/server/core/config/raw/repo_locks.go b/server/core/config/raw/repo_locks.go
new file mode 100644
index 0000000000..60ab8461fa
--- /dev/null
+++ b/server/core/config/raw/repo_locks.go
@@ -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
+}
diff --git a/server/core/config/raw/repo_locks_test.go b/server/core/config/raw/repo_locks_test.go
new file mode 100644
index 0000000000..8a8d45a0fe
--- /dev/null
+++ b/server/core/config/raw/repo_locks_test.go
@@ -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())
+ })
+ }
+}
diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go
index a2c84669e4..72bfa60432 100644
--- a/server/core/config/valid/global_cfg.go
+++ b/server/core/config/valid/global_cfg.go
@@ -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
}
diff --git a/server/core/config/valid/global_cfg_test.go b/server/core/config/valid/global_cfg_test.go
index 5c9cfc919a..6c1b94ded2 100644
--- a/server/core/config/valid/global_cfg_test.go
+++ b/server/core/config/valid/global_cfg_test.go
@@ -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,
},
diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go
index 95e36b1f27..7010d1c95d 100644
--- a/server/core/config/valid/repo_cfg.go
+++ b/server/core/config/valid/repo_cfg.go
@@ -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
diff --git a/server/core/config/valid/repo_locks.go b/server/core/config/valid/repo_locks.go
new file mode 100644
index 0000000000..7a4a77a873
--- /dev/null
+++ b/server/core/config/valid/repo_locks.go
@@ -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
+}
diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go
index c06681ef82..dc4c6dffa4 100644
--- a/server/events/command/project_context.go
+++ b/server/events/command/project_context.go
@@ -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
diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go
index 2d45006959..d020871b31 100644
--- a/server/events/project_command_builder_internal_test.go
+++ b/server/events/project_command_builder_internal_test.go
@@ -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
diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go
index 5ed6dad94c..ea17e02e07 100644
--- a/server/events/project_command_context_builder.go
+++ b/server/events/project_command_context_builder.go
@@ -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,
diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go
index 2ad783efd1..cd1b2e0d15 100644
--- a/server/events/project_command_runner.go
+++ b/server/events/project_command_runner.go
@@ -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")
}
diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go
index 103966ef52..cf40a2dbcb 100644
--- a/server/events/project_command_runner_test.go
+++ b/server/events/project_command_runner_test.go
@@ -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),
diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go
index e29f887a38..8ee5c34385 100644
--- a/server/events/vcs/github_client.go
+++ b/server/events/vcs/github_client.go
@@ -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
}