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 }