diff --git a/cmd/server.go b/cmd/server.go
index f5088bd757..b13c7d576e 100644
--- a/cmd/server.go
+++ b/cmd/server.go
@@ -72,6 +72,7 @@ const (
DisableAutoplanLabelFlag = "disable-autoplan-label"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
+ DisableGlobalApplyLockFlag = "disable-global-apply-lock"
DisableUnlockLabelFlag = "disable-unlock-label"
DiscardApprovalOnPlanFlag = "discard-approval-on-plan"
EmojiReaction = "emoji-reaction"
@@ -437,6 +438,9 @@ var boolFlags = map[string]boolFlag{
DisableRepoLockingFlag: {
description: "Disable atlantis locking repos",
},
+ DisableGlobalApplyLockFlag: {
+ description: "Disable atlantis global apply lock in UI",
+ },
DiscardApprovalOnPlanFlag: {
description: "Enables the discarding of approval if a new plan has been executed. Currently only Github is supported",
defaultValue: false,
diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go
index 1d7ff18f94..aaf7f8d8ab 100644
--- a/server/controllers/events/events_controller_e2e_test.go
+++ b/server/controllers/events/events_controller_e2e_test.go
@@ -1295,6 +1295,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
allowCommands = opt.allowCommands
}
disableApply := true
+ disableGlobalApplyLock := false
for _, allowCommand := range allowCommands {
if allowCommand == command.Apply {
disableApply = false
@@ -1314,7 +1315,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
backend := boltdb
lockingClient := locking.NewClient(boltdb)
noOpLocker := locking.NewNoOpLocker()
- applyLocker = locking.NewApplyClient(boltdb, disableApply)
+ applyLocker = locking.NewApplyClient(boltdb, disableApply, disableGlobalApplyLock)
projectLocker := &events.DefaultProjectLocker{
Locker: lockingClient,
NoOpLocker: noOpLocker,
diff --git a/server/controllers/templates/web_templates.go b/server/controllers/templates/web_templates.go
index 177a6428b4..6b030505a0 100644
--- a/server/controllers/templates/web_templates.go
+++ b/server/controllers/templates/web_templates.go
@@ -45,9 +45,10 @@ type LockIndexData struct {
// ApplyLockData holds the fields to display in the index view
type ApplyLockData struct {
- Locked bool
- Time time.Time
- TimeFormatted string
+ Locked bool
+ GlobalApplyLockEnabled bool
+ Time time.Time
+ TimeFormatted string
}
// IndexData holds the data for rendering the index page
@@ -98,6 +99,7 @@ var IndexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
Plan discarded and unlocked!
+ {{ if .ApplyLock.GlobalApplyLockEnabled }}
{{ if .ApplyLock.Locked }}
Apply commands are disabled globally
@@ -111,6 +113,7 @@ var IndexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
Disable Apply Commands
{{ end }}
+ {{ end }}
diff --git a/server/core/locking/apply_locking.go b/server/core/locking/apply_locking.go
index 921b4ae7e2..bdb8546308 100644
--- a/server/core/locking/apply_locking.go
+++ b/server/core/locking/apply_locking.go
@@ -33,20 +33,23 @@ type ApplyCommandLock struct {
// Locked is true is when apply commands are locked
// Either by using omitting apply from AllowCommands or creating a global ApplyCommandLock
// DisableApply lock take precedence when set
- Locked bool
- Time time.Time
- Failure string
+ Locked bool
+ GlobalApplyLockEnabled bool
+ Time time.Time
+ Failure string
}
type ApplyClient struct {
- backend Backend
- disableApply bool
+ backend Backend
+ disableApply bool
+ disableGlobalApplyLock bool
}
-func NewApplyClient(backend Backend, disableApply bool) ApplyLocker {
+func NewApplyClient(backend Backend, disableApply bool, disableGlobalApplyLock bool) ApplyLocker {
return &ApplyClient{
- backend: backend,
- disableApply: disableApply,
+ backend: backend,
+ disableApply: disableApply,
+ disableGlobalApplyLock: disableGlobalApplyLock,
}
}
@@ -91,7 +94,9 @@ func (c *ApplyClient) UnlockApply() error {
// CheckApplyLock retrieves an apply command lock if present.
// If DisableApply is set it will always return a lock.
func (c *ApplyClient) CheckApplyLock() (ApplyCommandLock, error) {
- response := ApplyCommandLock{}
+ response := ApplyCommandLock{
+ GlobalApplyLockEnabled: true,
+ }
if c.disableApply {
return ApplyCommandLock{
@@ -108,6 +113,9 @@ func (c *ApplyClient) CheckApplyLock() (ApplyCommandLock, error) {
response.Locked = true
response.Time = applyCmdLock.LockTime()
}
+ if c.disableGlobalApplyLock {
+ response.GlobalApplyLockEnabled = false
+ }
return response, nil
}
diff --git a/server/core/locking/locking_test.go b/server/core/locking/locking_test.go
index edf90d1a23..57a6701455 100644
--- a/server/core/locking/locking_test.go
+++ b/server/core/locking/locking_test.go
@@ -196,7 +196,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.LockCommand(Any[command.Name](), Any[time.Time]())).ThenReturn(nil, errExpected)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
lock, err := l.LockApply()
Equals(t, errExpected, err)
Assert(t, !lock.Locked, "exp false")
@@ -205,7 +205,7 @@ func TestApplyLocker(t *testing.T) {
t.Run("can't lock if apply is omitted from userConfig.AllowCommands", func(t *testing.T) {
backend := mocks.NewMockBackend()
- l := locking.NewApplyClient(backend, true)
+ l := locking.NewApplyClient(backend, true, false)
_, err := l.LockApply()
ErrEquals(t, "apply is omitted from AllowCommands; Apply commands are locked globally until flag is updated", err)
@@ -216,7 +216,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.LockCommand(Any[command.Name](), Any[time.Time]())).ThenReturn(applyLock, nil)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
lock, _ := l.LockApply()
Assert(t, lock.Locked, "exp lock present")
})
@@ -227,7 +227,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.UnlockCommand(Any[command.Name]())).ThenReturn(errExpected)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
err := l.UnlockApply()
Equals(t, errExpected, err)
})
@@ -235,7 +235,7 @@ func TestApplyLocker(t *testing.T) {
t.Run("can't lock if apply is omitted from userConfig.AllowCommands", func(t *testing.T) {
backend := mocks.NewMockBackend()
- l := locking.NewApplyClient(backend, true)
+ l := locking.NewApplyClient(backend, true, false)
err := l.UnlockApply()
ErrEquals(t, "apply commands are disabled until AllowCommands flag is updated", err)
@@ -246,7 +246,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.UnlockCommand(Any[command.Name]())).ThenReturn(nil)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
err := l.UnlockApply()
Equals(t, nil, err)
})
@@ -258,7 +258,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.CheckCommandLock(Any[command.Name]())).ThenReturn(nil, errExpected)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
lock, err := l.CheckApplyLock()
Equals(t, errExpected, err)
Equals(t, lock.Locked, false)
@@ -267,7 +267,7 @@ func TestApplyLocker(t *testing.T) {
t.Run("when apply is not in AllowCommands always return a lock", func(t *testing.T) {
backend := mocks.NewMockBackend()
- l := locking.NewApplyClient(backend, true)
+ l := locking.NewApplyClient(backend, true, false)
lock, err := l.CheckApplyLock()
Ok(t, err)
Equals(t, lock.Locked, true)
@@ -278,7 +278,7 @@ func TestApplyLocker(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.CheckCommandLock(Any[command.Name]())).ThenReturn(applyLock, nil)
- l := locking.NewApplyClient(backend, false)
+ l := locking.NewApplyClient(backend, false, false)
lock, err := l.CheckApplyLock()
Equals(t, nil, err)
Assert(t, lock.Locked, "exp lock present")
diff --git a/server/server.go b/server/server.go
index 4b95e16947..eeab9d732e 100644
--- a/server/server.go
+++ b/server/server.go
@@ -121,6 +121,7 @@ type Server struct {
WebPassword string
ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler
ScheduledExecutorService *scheduled.ExecutorService
+ DisableGlobalApplyLock bool
}
// Config holds config for server that isn't passed in by the user.
@@ -455,8 +456,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
} else {
lockingClient = locking.NewClient(backend)
}
+ disableGlobalApplyLock := false
+ if userConfig.DisableGlobalApplyLock {
+ disableGlobalApplyLock = true
+ }
- applyLockingClient = locking.NewApplyClient(backend, disableApply)
+ applyLockingClient = locking.NewApplyClient(backend, disableApply, disableGlobalApplyLock)
workingDirLocker := events.NewDefaultWorkingDirLocker()
var workingDir events.WorkingDir = &events.FileWorkspace{
@@ -919,6 +924,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
ProjectJobsErrorTemplate: templates.ProjectJobsErrorTemplate,
SSLKeyFile: userConfig.SSLKeyFile,
SSLCertFile: userConfig.SSLCertFile,
+ DisableGlobalApplyLock: userConfig.DisableGlobalApplyLock,
Drainer: drainer,
ProjectCmdOutputHandler: projectCmdOutputHandler,
WebAuthentication: userConfig.WebBasicAuth,
@@ -941,8 +947,6 @@ func (s *Server) Start() error {
s.Router.HandleFunc("/api/apply", s.APIController.Apply).Methods("POST")
s.Router.HandleFunc("/github-app/exchange-code", s.GithubAppController.ExchangeCode).Methods("GET")
s.Router.HandleFunc("/github-app/setup", s.GithubAppController.New).Methods("GET")
- s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries()
- s.Router.HandleFunc("/apply/unlock", s.LocksController.UnlockApply).Methods("DELETE").Queries()
s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}")
s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET").
Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName)
@@ -953,6 +957,10 @@ func (s *Server) Start() error {
if ok {
s.Router.Handle(s.CommandRunner.GlobalCfg.Metrics.Prometheus.Endpoint, r.HTTPHandler())
}
+ if !s.DisableGlobalApplyLock {
+ s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries()
+ s.Router.HandleFunc("/apply/unlock", s.LocksController.UnlockApply).Methods("DELETE").Queries()
+ }
n := negroni.New(&negroni.Recovery{
Logger: log.New(os.Stdout, "", log.LstdFlags),
@@ -1064,9 +1072,10 @@ func (s *Server) Index(w http.ResponseWriter, _ *http.Request) {
}
applyLockData := templates.ApplyLockData{
- Time: applyCmdLock.Time,
- Locked: applyCmdLock.Locked,
- TimeFormatted: applyCmdLock.Time.Format("02-01-2006 15:04:05"),
+ Time: applyCmdLock.Time,
+ Locked: applyCmdLock.Locked,
+ GlobalApplyLockEnabled: applyCmdLock.GlobalApplyLockEnabled,
+ TimeFormatted: applyCmdLock.Time.Format("02-01-2006 15:04:05"),
}
//Sort by date - newest to oldest.
sort.SliceStable(lockResults, func(i, j int) bool { return lockResults[i].Time.After(lockResults[j].Time) })
diff --git a/server/user_config.go b/server/user_config.go
index b81a117055..cf2366fb76 100644
--- a/server/user_config.go
+++ b/server/user_config.go
@@ -36,6 +36,7 @@ type UserConfig struct {
DisableAutoplanLabel string `mapstructure:"disable-autoplan-label"`
DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"`
DisableRepoLocking bool `mapstructure:"disable-repo-locking"`
+ DisableGlobalApplyLock bool `mapstructure:"disable-global-apply-lock"`
DisableUnlockLabel string `mapstructure:"disable-unlock-label"`
DiscardApprovalOnPlanFlag bool `mapstructure:"discard-approval-on-plan"`
EmojiReaction string `mapstructure:"emoji-reaction"`