From fe32f8645c2d745402a7d3f00c5f31e633ebb64e Mon Sep 17 00:00:00 2001 From: Isaac Gaskin Date: Tue, 23 Jan 2024 21:10:44 -0800 Subject: [PATCH] feat(4157): adding option to hide UI button to disable apply commands (#4158) --- cmd/server.go | 4 +++ .../events/events_controller_e2e_test.go | 3 ++- server/controllers/templates/web_templates.go | 9 ++++--- server/core/locking/apply_locking.go | 26 ++++++++++++------- server/core/locking/locking_test.go | 18 ++++++------- server/server.go | 21 ++++++++++----- server/user_config.go | 1 + 7 files changed, 54 insertions(+), 28 deletions(-) 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"`