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>
This commit is contained in:
Ken Kaizu
2022-12-10 07:13:31 +09:00
committed by GitHub
parent 66dc30e755
commit 4d957836ad
21 changed files with 231 additions and 61 deletions

View File

@@ -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:
```

View File

@@ -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<br />*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Apply Requirements](apply-requirements.html) for more details. |

View File

@@ -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 /&lt;regex&gt;/ or an exact string match. Repo IDs are of the form `{vcs hostname}/{org}/{name}`, ex. `github.com/owner/repo`. Hostname is specified without scheme or port. For Bitbucket Server, {org} is the **name** of the project, not the key. |
| branch | string | none | no | An regex matching pull requests by base branch (the branch the pull request is getting merged into). By default, all branches are matched |
| 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

View File

@@ -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,

View File

@@ -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{

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}
}

View File

@@ -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,
},
},
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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"},
},

View File

@@ -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,

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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,