From 4d957836add10230b4d094b1b7d997d31b93bbb1 Mon Sep 17 00:00:00 2001 From: Ken Kaizu Date: Sat, 10 Dec 2022 07:13:31 +0900 Subject: [PATCH] Enable or disable `repo_locking` per repo in `repos.yaml` and `atlantis.yaml` (#2700) * disable repo locking repos.yaml and atlantis.yaml with allow override * rename disable_repo_locking into repo_locking * add both enable/disable repo_locking test Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- .../docs/repo-level-atlantis-yaml.md | 6 +- .../docs/server-side-repo-config.md | 25 +++--- .../events/events_controller_e2e_test.go | 6 +- server/core/config/parser_validator_test.go | 3 +- server/core/config/raw/global_cfg.go | 6 +- server/core/config/raw/project.go | 5 ++ server/core/config/valid/global_cfg.go | 30 ++++++-- server/core/config/valid/global_cfg_test.go | 40 +++++++++- server/core/config/valid/repo_cfg.go | 2 + server/events/command/project_context.go | 2 + server/events/mocks/mock_project_lock.go | 20 +++-- server/events/project_command_builder.go | 4 - .../project_command_builder_internal_test.go | 11 +++ .../events/project_command_context_builder.go | 17 ++-- .../project_command_context_builder_test.go | 6 +- server/events/project_command_runner.go | 4 +- server/events/project_command_runner_test.go | 2 + server/events/project_locker.go | 16 ++-- server/events/project_locker_test.go | 77 ++++++++++++++++++- server/server.go | 8 +- 21 files changed, 231 insertions(+), 61 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 615946e805..b406a821f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -130,7 +130,7 @@ We use [pegomock](https://github.com/petergtz/pegomock) for mocking. If you're modifying any interfaces that are mocked, you'll need to regen the mocks for that interface. -Install using `go get github.com/petergtz/pegomock/pegomock` +Install using `go install github.com/petergtz/pegomock/pegomock` If you see errors like: ``` diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 6ae9606bc6..482d5b8129 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -56,6 +56,7 @@ projects: workspace: default terraform_version: v0.11.0 delete_source_branch_on_merge: true + repo_locking: true autoplan: when_modified: ["*.tf", "../modules/**/*.tf"] enabled: true @@ -264,6 +265,7 @@ dir: mydir workspace: myworkspace execution_order_group: 0 delete_source_branch_on_merge: false +repo_locking: true autoplan: terraform_version: 0.11.0 apply_requirements: ["approved"] @@ -273,10 +275,12 @@ 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. | +| 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://www.terraform.io/docs/state/workspaces.html) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | | 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. | | autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | | terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | | apply_requirements
*(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 [Apply Requirements](apply-requirements.html) for more details. | diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index a54c8bd2ec..bc3ee64594 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -44,7 +44,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] + allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking] # allowed_workflows specifies which workflows the repos that match # are allowed to select. @@ -58,7 +58,11 @@ repos: # delete_source_branch_on_merge defines whether the source branch would be deleted on merge # If false (default), the source branch won't be deleted on merge delete_source_branch_on_merge: true - + + # repo_locking defines whether lock repository when planning. + # If true (default), atlantis try to get a lock. + repo_locking: true + # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution. pre_workflow_hooks: - run: my-pre-workflow-hook-command arg1 @@ -392,16 +396,17 @@ If you set a workflow with the key `default`, it will override this. ::: ### Repo -| Key | Type | Default | Required | Description | -|-------------------------------|----------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 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 | -| workflow | string | none | no | A custom workflow. | -| 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 [Apply Requirements](apply-requirements.html) 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` and `delete_source_branch_on_merge` | -| 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.html). | -| delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge (only AzureDevOps and GitLab support) | +| workflow | string | none | no | A custom workflow. | +| 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 [Apply Requirements](apply-requirements.html) 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` and `repo_locking` | +| 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.html). | +| delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge (only AzureDevOps and GitLab support) | +| repo_locking | bool | false | no | Whether or not to get a lock | :::tip Notes diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 1f9b0176d7..a5236d337c 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -899,10 +899,12 @@ func setupE2E(t *testing.T, repoDir string) (events_controllers.VCSEventsControl Ok(t, err) backend := boltdb lockingClient := locking.NewClient(boltdb) + noOpLocker := locking.NewNoOpLocker() applyLocker = locking.NewApplyClient(boltdb, userConfig.DisableApply) projectLocker := &events.DefaultProjectLocker{ - Locker: lockingClient, - VCSClient: e2eVCSClient, + Locker: lockingClient, + NoOpLocker: noOpLocker, + VCSClient: e2eVCSClient, } workingDir := &events.FileWorkspace{ DataDir: dataDir, diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index c9cdec788c..08e07987fc 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -1222,7 +1222,7 @@ func TestParseGlobalCfg(t *testing.T) { input: `repos: - id: /.*/ allowed_overrides: [invalid]`, - expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\", \"workflow\" and \"delete_source_branch_on_merge\" are supported.).).", + expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"apply_requirements\", \"workflow\", \"delete_source_branch_on_merge\" and \"repo_locking\" are supported.).).", }, "invalid apply_requirement": { input: `repos: @@ -1450,6 +1450,7 @@ workflows: AllowedOverrides: []string{}, AllowCustomWorkflows: Bool(false), DeleteSourceBranchOnMerge: Bool(false), + RepoLocking: Bool(true), }, }, Workflows: map[string]valid.Workflow{ diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go index 980596daf1..b998342133 100644 --- a/server/core/config/raw/global_cfg.go +++ b/server/core/config/raw/global_cfg.go @@ -30,6 +30,7 @@ type Repo struct { AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"` 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"` } func (g GlobalCfg) Validate() error { @@ -173,8 +174,8 @@ func (r Repo) Validate() error { overridesValid := func(value interface{}) error { overrides := value.([]string) for _, o := range overrides { - if o != valid.ApplyRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey { - return fmt.Errorf("%q is not a valid override, only %q, %q and %q are supported", o, valid.ApplyRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey) + if o != valid.ApplyRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey { + return fmt.Errorf("%q is not a valid override, only %q, %q, %q and %q are supported", o, valid.ApplyRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey) } } return nil @@ -268,5 +269,6 @@ OUTER: AllowedOverrides: r.AllowedOverrides, AllowCustomWorkflows: r.AllowCustomWorkflows, DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge, + RepoLocking: r.RepoLocking, } } diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index cf2422f046..081eb87903 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -30,6 +30,7 @@ type Project struct { Autoplan *Autoplan `yaml:"autoplan,omitempty"` ApplyRequirements []string `yaml:"apply_requirements,omitempty"` DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"` + RepoLocking *bool `yaml:"repo_locking,omitempty"` ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"` } @@ -118,6 +119,10 @@ func (p Project) ToValid() valid.Project { v.DeleteSourceBranchOnMerge = p.DeleteSourceBranchOnMerge } + if p.RepoLocking != nil { + v.RepoLocking = p.RepoLocking + } + if p.ExecutionOrderGroup != nil { v.ExecutionOrderGroup = *p.ExecutionOrderGroup } diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index 6fcad2cc6c..ecf4ec5cf8 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -22,6 +22,7 @@ const AllowedOverridesKey = "allowed_overrides" const AllowCustomWorkflowsKey = "allow_custom_workflows" const DefaultWorkflowName = "default" const DeleteSourceBranchOnMergeKey = "delete_source_branch_on_merge" +const RepoLockingKey = "repo_locking" // NonOverrideableApplyReqs will get applied across all "repos" in the server side config. // If repo config is allowed overrides, they can override this. @@ -69,6 +70,7 @@ type Repo struct { AllowedOverrides []string AllowCustomWorkflows *bool DeleteSourceBranchOnMerge *bool + RepoLocking *bool } type MergedProjectCfg struct { @@ -85,6 +87,7 @@ type MergedProjectCfg struct { PolicySets PolicySets DeleteSourceBranchOnMerge bool ExecutionOrderGroup int + RepoLocking bool } // WorkflowHook is a map of custom run commands to run before or after workflows. @@ -191,8 +194,9 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { allowCustomWorkflows := false deleteSourceBranchOnMerge := false + repoLockingKey := true if args.AllowRepoCfg { - allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey} + allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} allowCustomWorkflows = true } @@ -209,6 +213,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { AllowedOverrides: allowedOverrides, AllowCustomWorkflows: &allowCustomWorkflows, DeleteSourceBranchOnMerge: &deleteSourceBranchOnMerge, + RepoLocking: &repoLockingKey, }, }, Workflows: map[string]Workflow{ @@ -245,7 +250,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") - applyReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID) + applyReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) // If repos are allowed to override certain keys then override them. for _, key := range allowedOverrides { @@ -291,6 +296,11 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro deleteSourceBranchOnMerge = *proj.DeleteSourceBranchOnMerge } log.Debug("merged deleteSourceBranchOnMerge: [%t]", deleteSourceBranchOnMerge) + case RepoLockingKey: + if proj.RepoLocking != nil { + log.Debug("overriding server-defined %s with repo settings: [%t]", RepoLockingKey, *proj.RepoLocking) + repoLocking = *proj.RepoLocking + } } log.Debug("MergeProjectCfg completed") } @@ -310,6 +320,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro PolicySets: g.PolicySets, DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, ExecutionOrderGroup: proj.ExecutionOrderGroup, + RepoLocking: repoLocking, } } @@ -317,7 +328,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") - applyReqs, workflow, _, _, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID) + applyReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking := g.getMatchingCfg(log, repoID) return MergedProjectCfg{ ApplyRequirements: applyReqs, Workflow: workflow, @@ -328,6 +339,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo TerraformVersion: nil, PolicySets: g.PolicySets, DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, + RepoLocking: repoLocking, } } @@ -371,6 +383,9 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { if p.DeleteSourceBranchOnMerge != nil && !sliceContainsF(allowedOverrides, DeleteSourceBranchOnMergeKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", DeleteSourceBranchOnMergeKey, AllowedOverridesKey, DeleteSourceBranchOnMergeKey) } + if p.RepoLocking != nil && !sliceContainsF(allowedOverrides, RepoLockingKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", RepoLockingKey, AllowedOverridesKey, RepoLockingKey) + } } // Check custom workflows. @@ -429,7 +444,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) (applyReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool) { +func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (applyReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool) { toLog := make(map[string]string) traceF := func(repoIdx int, repoID string, key string, val interface{}) string { from := "default server config" @@ -451,7 +466,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey} { + for _, key := range []string{ApplyRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { @@ -480,6 +495,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (app toLog[DeleteSourceBranchOnMergeKey] = traceF(i, repo.IDString(), DeleteSourceBranchOnMergeKey, *repo.DeleteSourceBranchOnMerge) deleteSourceBranchOnMerge = *repo.DeleteSourceBranchOnMerge } + case RepoLockingKey: + if repo.RepoLocking != nil { + toLog[RepoLockingKey] = traceF(i, repo.IDString(), RepoLockingKey, *repo.RepoLocking) + repoLocking = *repo.RepoLocking + } } } } diff --git a/server/core/config/valid/global_cfg_test.go b/server/core/config/valid/global_cfg_test.go index e9bce44fb6..c161a03ecc 100644 --- a/server/core/config/valid/global_cfg_test.go +++ b/server/core/config/valid/global_cfg_test.go @@ -57,6 +57,7 @@ func TestNewGlobalCfg(t *testing.T) { AllowedOverrides: []string{}, AllowCustomWorkflows: Bool(false), DeleteSourceBranchOnMerge: Bool(false), + RepoLocking: Bool(true), }, }, Workflows: map[string]valid.Workflow{ @@ -163,7 +164,7 @@ func TestNewGlobalCfg(t *testing.T) { if c.allowRepoCfg { exp.Repos[0].AllowCustomWorkflows = Bool(true) - exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"} + exp.Repos[0].AllowedOverrides = []string{"apply_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking"} } if c.mergeableReq { exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "mergeable") @@ -610,6 +611,7 @@ policies: Workspace: "default", Name: "", AutoplanEnabled: false, + RepoLocking: true, }, }, "policies set correct version if specified": { @@ -651,6 +653,7 @@ policies: Workspace: "default", Name: "", AutoplanEnabled: false, + RepoLocking: true, }, }, } @@ -732,6 +735,7 @@ workflows: Name: "", AutoplanEnabled: false, PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "repo-side apply reqs win out if allowed": { @@ -761,6 +765,37 @@ repos: Name: "", AutoplanEnabled: false, PolicySets: emptyPolicySets, + RepoLocking: true, + }, + }, + "repo-side repo_locking win out if allowed": { + gCfg: ` +repos: +- id: /.*/ + repo_locking: false +`, + repoID: "github.com/owner/repo", + proj: valid.Project{ + Dir: ".", + Workspace: "default", + ApplyRequirements: []string{}, + RepoLocking: Bool(true), + }, + repoWorkflows: nil, + exp: valid.MergedProjectCfg{ + ApplyRequirements: []string{}, + Workflow: valid.Workflow{ + Name: "default", + Apply: valid.DefaultApplyStage, + PolicyCheck: valid.DefaultPolicyCheckStage, + Plan: valid.DefaultPlanStage, + }, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: false, }, }, "last server-side match wins": { @@ -793,6 +828,7 @@ repos: Name: "myname", AutoplanEnabled: false, PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "autoplan is set properly": { @@ -821,6 +857,7 @@ repos: Name: "myname", AutoplanEnabled: true, PolicySets: emptyPolicySets, + RepoLocking: true, }, }, "execution order group is set": { @@ -851,6 +888,7 @@ repos: AutoplanEnabled: true, PolicySets: emptyPolicySets, ExecutionOrderGroup: 10, + RepoLocking: true, }, }, } diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index f645fa9fbd..331b4c7ba0 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -23,6 +23,7 @@ type RepoCfg struct { ParallelPlan bool ParallelPolicyCheck bool DeleteSourceBranchOnMerge *bool + RepoLocking *bool AllowedRegexpPrefixes []string } @@ -127,6 +128,7 @@ type Project struct { Autoplan Autoplan ApplyRequirements []string DeleteSourceBranchOnMerge *bool + RepoLocking *bool ExecutionOrderGroup int } diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go index cbd7cae9a1..4a772b7158 100644 --- a/server/events/command/project_context.go +++ b/server/events/command/project_context.go @@ -88,6 +88,8 @@ type ProjectContext struct { PolicySets valid.PolicySets // 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 // UUID for atlantis logs JobID string // The index of order group. Before planning/applying it will use to sort projects. Default is 0. diff --git a/server/events/mocks/mock_project_lock.go b/server/events/mocks/mock_project_lock.go index c1e6109c46..8964983413 100644 --- a/server/events/mocks/mock_project_lock.go +++ b/server/events/mocks/mock_project_lock.go @@ -28,11 +28,11 @@ func NewMockProjectLocker(options ...pegomock.Option) *MockProjectLocker { func (mock *MockProjectLocker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockProjectLocker) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project) (*events.TryLockResponse, error) { +func (mock *MockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*events.TryLockResponse, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockProjectLocker().") } - params := []pegomock.Param{log, pull, user, workspace, project} + params := []pegomock.Param{log, pull, user, workspace, project, repoLocking} result := pegomock.GetGenericMockFrom(mock).Invoke("TryLock", params, []reflect.Type{reflect.TypeOf((**events.TryLockResponse)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 *events.TryLockResponse var ret1 error @@ -84,8 +84,8 @@ type VerifierMockProjectLocker struct { timeout time.Duration } -func (verifier *VerifierMockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project) *MockProjectLocker_TryLock_OngoingVerification { - params := []pegomock.Param{log, pull, user, workspace, project} +func (verifier *VerifierMockProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) *MockProjectLocker_TryLock_OngoingVerification { + params := []pegomock.Param{log, pull, user, workspace, project, repoLocking} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "TryLock", params, verifier.timeout) return &MockProjectLocker_TryLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -95,12 +95,12 @@ type MockProjectLocker_TryLock_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockProjectLocker_TryLock_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.User, string, models.Project) { - log, pull, user, workspace, project := c.GetAllCapturedArguments() - return log[len(log)-1], pull[len(pull)-1], user[len(user)-1], workspace[len(workspace)-1], project[len(project)-1] +func (c *MockProjectLocker_TryLock_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.PullRequest, models.User, string, models.Project, bool) { + log, pull, user, workspace, project, repoLocking := c.GetAllCapturedArguments() + return log[len(log)-1], pull[len(pull)-1], user[len(user)-1], workspace[len(workspace)-1], project[len(project)-1], repoLocking[len(repoLocking)-1] } -func (c *MockProjectLocker_TryLock_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.User, _param3 []string, _param4 []models.Project) { +func (c *MockProjectLocker_TryLock_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.PullRequest, _param2 []models.User, _param3 []string, _param4 []models.Project, _param5 []bool) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) @@ -123,6 +123,10 @@ func (c *MockProjectLocker_TryLock_OngoingVerification) GetAllCapturedArguments( for u, param := range params[4] { _param4[u] = param.(models.Project) } + _param5 = make([]bool, len(c.methodInvocations)) + for u, param := range params[5] { + _param5[u] = param.(bool) + } } return } diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 63c4c44049..d616f25a9a 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -317,7 +317,6 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context commentFlags, repoDir, repoCfg.Automerge, - mergedCfg.DeleteSourceBranchOnMerge, repoCfg.ParallelApply, repoCfg.ParallelPlan, verbose, @@ -355,7 +354,6 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context commentFlags, repoDir, DefaultAutomergeEnabled, - pCfg.DeleteSourceBranchOnMerge, DefaultParallelApplyEnabled, DefaultParallelPlanEnabled, verbose, @@ -689,7 +687,6 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte commentFlags, repoDir, automerge, - projCfg.DeleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -705,7 +702,6 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte commentFlags, repoDir, automerge, - projCfg.DeleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index 0ffb961c9e..3a9adbb9a3 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -83,6 +83,7 @@ workflows: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -138,6 +139,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -193,6 +195,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -256,6 +259,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{}, @@ -406,6 +410,7 @@ workflows: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -465,6 +470,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -527,6 +533,7 @@ workflows: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{}, expApplySteps: []string{}, @@ -572,6 +579,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"plan"}, expApplySteps: []string{"apply"}, @@ -778,6 +786,7 @@ projects: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPlanSteps: []string{"init", "plan"}, expApplySteps: []string{"apply"}, @@ -945,6 +954,7 @@ repos: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPolicyCheckSteps: []string{"show", "policy_check"}, }, @@ -1005,6 +1015,7 @@ workflows: Verbose: true, Workspace: "myworkspace", PolicySets: emptyPolicySets, + RepoLocking: true, }, expPolicyCheckSteps: []string{"policy_check"}, }, diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index 956d7f539f..62b9adbf27 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -44,7 +44,7 @@ type ProjectCommandContextBuilder interface { prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, + automerge, parallelApply, parallelPlan, verbose bool, ) []command.ProjectContext } @@ -63,12 +63,12 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext( prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, + automerge, parallelApply, parallelPlan, verbose bool, ) (projectCmds []command.ProjectContext) { cb.ProjectCounter.Inc(1) cmds := cb.ProjectCommandContextBuilder.BuildProjectContext( - ctx, cmdName, prjCfg, commentFlags, repoDir, automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, + ctx, cmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, ) projectCmds = []command.ProjectContext{} @@ -95,7 +95,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, + automerge, parallelApply, parallelPlan, verbose bool, ) (projectCmds []command.ProjectContext) { ctx.Log.Debug("Building project command context for %s", cmdName) @@ -129,7 +129,6 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( prjCfg.PolicySets, escapeArgs(commentFlags), automerge, - deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -153,7 +152,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose bool, + automerge, parallelApply, parallelPlan, verbose bool, ) (projectCmds []command.ProjectContext) { ctx.Log.Debug("PolicyChecks are enabled") @@ -170,7 +169,6 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( commentFlags, repoDir, automerge, - deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -191,7 +189,6 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( prjCfg.PolicySets, escapeArgs(commentFlags), automerge, - deleteSourceBranchOnMerge, parallelApply, parallelPlan, verbose, @@ -215,7 +212,6 @@ func newProjectCommandContext(ctx *command.Context, policySets valid.PolicySets, escapedCommentArgs []string, automergeEnabled bool, - deleteSourceBranchOnMerge, parallelApplyEnabled bool, parallelPlanEnabled bool, verbose bool, @@ -247,7 +243,8 @@ func newProjectCommandContext(ctx *command.Context, BaseRepo: ctx.Pull.BaseRepo, EscapedCommentArgs: escapedCommentArgs, AutomergeEnabled: automergeEnabled, - DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, + DeleteSourceBranchOnMerge: projCfg.DeleteSourceBranchOnMerge, + RepoLocking: projCfg.RepoLocking, ParallelApplyEnabled: parallelApplyEnabled, ParallelPlanEnabled: parallelPlanEnabled, ParallelPolicyCheckEnabled: parallelPlanEnabled, diff --git a/server/events/project_command_context_builder_test.go b/server/events/project_command_context_builder_test.go index 8f3c787276..a9039e24ba 100644 --- a/server/events/project_command_context_builder_test.go +++ b/server/events/project_command_context_builder_test.go @@ -58,7 +58,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { }, } - result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false, false) + result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) }) @@ -77,7 +77,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { }, } - result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false, false) + result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, false, false) assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus) }) @@ -97,7 +97,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { }, } - result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, false, true, false, false) + result := subject.BuildProjectContext(commandCtx, command.Plan, projCfg, []string{}, "some/dir", false, true, false, false) assert.True(t, result[0].ParallelApplyEnabled) assert.False(t, result[0].ParallelPlanEnabled) diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 5b5c7976f0..246fdae701 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -288,7 +288,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)) + lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir), ctx.RepoLocking) if err != nil { return nil, "", errors.Wrap(err, "acquiring lock") @@ -354,7 +354,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)) + lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.Pull.BaseRepo.FullName, ctx.RepoRelDir), ctx.RepoLocking) 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 96f4b17c6e..5b54005f88 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -74,6 +74,7 @@ func TestDefaultProjectCommandRunner_Plan(t *testing.T) { matchers.AnyModelsUser(), AnyString(), matchers.AnyModelsProject(), + AnyBool(), )).ThenReturn(&events.TryLockResponse{ LockAcquired: true, LockKey: "lock-key", @@ -566,6 +567,7 @@ func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { matchers.AnyModelsUser(), AnyString(), matchers.AnyModelsProject(), + AnyBool(), )).ThenReturn(&events.TryLockResponse{ LockAcquired: true, LockKey: "lock-key", diff --git a/server/events/project_locker.go b/server/events/project_locker.go index 4109724bd2..c44e7271a3 100644 --- a/server/events/project_locker.go +++ b/server/events/project_locker.go @@ -33,13 +33,14 @@ type ProjectLocker interface { // The third return value is a function that can be called to unlock the // lock. It will only be set if the lock was acquired. Any errors will set // error. - TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project) (*TryLockResponse, error) + TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*TryLockResponse, error) } // DefaultProjectLocker implements ProjectLocker. type DefaultProjectLocker struct { - Locker locking.Locker - VCSClient vcs.Client + Locker locking.Locker + NoOpLocker locking.Locker + VCSClient vcs.Client } // TryLockResponse is the result of trying to lock a project. @@ -58,8 +59,13 @@ type TryLockResponse struct { } // TryLock implements ProjectLocker.TryLock. -func (p *DefaultProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project) (*TryLockResponse, error) { - lockAttempt, err := p.Locker.TryLock(project, workspace, pull, user) +func (p *DefaultProjectLocker) TryLock(log logging.SimpleLogging, pull models.PullRequest, user models.User, workspace string, project models.Project, repoLocking bool) (*TryLockResponse, error) { + locker := p.Locker + if !repoLocking { + locker = p.NoOpLocker + } + + lockAttempt, err := locker.TryLock(project, workspace, pull, user) if err != nil { return nil, err } diff --git a/server/events/project_locker_test.go b/server/events/project_locker_test.go index 0da01b4e48..666bd98298 100644 --- a/server/events/project_locker_test.go +++ b/server/events/project_locker_test.go @@ -53,7 +53,7 @@ func TestDefaultProjectLocker_TryLockWhenLocked(t *testing.T) { }, nil, ) - res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject) + res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true) link, _ := mockClient.MarkdownPullLink(lockingPull) Ok(t, err) Equals(t, &events.TryLockResponse{ @@ -90,7 +90,7 @@ func TestDefaultProjectLocker_TryLockWhenLockedSamePull(t *testing.T) { }, nil, ) - res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject) + res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true) Ok(t, err) Equals(t, true, res.LockAcquired) @@ -129,7 +129,7 @@ func TestDefaultProjectLocker_TryLockUnlocked(t *testing.T) { }, nil, ) - res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject) + res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, true) Ok(t, err) Equals(t, true, res.LockAcquired) @@ -139,3 +139,74 @@ func TestDefaultProjectLocker_TryLockUnlocked(t *testing.T) { Ok(t, err) mockLocker.VerifyWasCalledOnce().Unlock(lockKey) } + +func TestDefaultProjectLocker_RepoLocking(t *testing.T) { + var githubClient *vcs.GithubClient + mockClient := vcs.NewClientProxy(githubClient, nil, nil, nil, nil) + expProject := models.Project{} + expWorkspace := "default" + expPull := models.PullRequest{Num: 2} + expUser := models.User{} + lockKey := "key" + + tests := []struct { + name string + repoLocking bool + setup func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) + verify func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) + }{ + { + "enable repo locking", + true, + func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) { + When(locker.TryLock(expProject, expWorkspace, expPull, expUser)).ThenReturn( + locking.TryLockResponse{ + LockAcquired: true, + CurrLock: models.ProjectLock{}, + LockKey: lockKey, + }, + nil, + ) + }, + func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) { + locker.VerifyWasCalledOnce().TryLock(expProject, expWorkspace, expPull, expUser) + noOpLocker.VerifyWasCalled(Never()).TryLock(expProject, expWorkspace, expPull, expUser) + }, + }, + { + "disable repo locking", + false, + func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) { + When(noOpLocker.TryLock(expProject, expWorkspace, expPull, expUser)).ThenReturn( + locking.TryLockResponse{ + LockAcquired: true, + CurrLock: models.ProjectLock{}, + LockKey: lockKey, + }, + nil, + ) + }, + func(locker *mocks.MockLocker, noOpLocker *mocks.MockLocker) { + locker.VerifyWasCalled(Never()).TryLock(expProject, expWorkspace, expPull, expUser) + noOpLocker.VerifyWasCalledOnce().TryLock(expProject, expWorkspace, expPull, expUser) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterMockTestingT(t) + mockLocker := mocks.NewMockLocker() + mockNoOpLocker := mocks.NewMockLocker() + locker := events.DefaultProjectLocker{ + Locker: mockLocker, + NoOpLocker: mockNoOpLocker, + VCSClient: mockClient, + } + tt.setup(mockLocker, mockNoOpLocker) + res, err := locker.TryLock(logging.NewNoopLogger(t), expPull, expUser, expWorkspace, expProject, tt.repoLocking) + Ok(t, err) + Equals(t, true, res.LockAcquired) + tt.verify(mockLocker, mockNoOpLocker) + }) + } +} diff --git a/server/server.go b/server/server.go index ef91327a75..579a921597 100644 --- a/server/server.go +++ b/server/server.go @@ -430,9 +430,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } + noOpLocker := locking.NewNoOpLocker() if userConfig.DisableRepoLocking { logger.Info("Repo Locking is disabled") - lockingClient = locking.NewNoOpLocker() + lockingClient = noOpLocker } else { lockingClient = locking.NewClient(backend) } @@ -458,8 +459,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } projectLocker := &events.DefaultProjectLocker{ - Locker: lockingClient, - VCSClient: vcsClient, + Locker: lockingClient, + NoOpLocker: noOpLocker, + VCSClient: vcsClient, } deleteLockCommand := &events.DefaultDeleteLockCommand{ Locker: lockingClient,