Files
atlantis/server/controllers/locks_controller.go
KevinSnyderCodes 1a8344489f fix: add path to WorkingDir methods (#2180)
* fix: add path to WorkingDir methods

`WorkingDirLocker` and `WorkingDir` are closely related -- the former acquires a lock, which ensures that the latter can safely operate on a given file path.

In #2131 we added `path` to the `WorkingDirLocker` lock key, but neglected to add the same to `WorkingDir`.

This commit adds `path` as an argument to certain `WorkingDir` methods, and includes `path` in the directory that we use to clone the repository for a given project.

Since `path` can include certain special characters such as `/`, we encode `path` as base32 when using it as part of a file path. This should ensure no special characters are used in the filesystem, and that the value can be decoded if desired (unlike hashes such as md5).

Additional changes:

- All calls to changed methods have been updated, including unit tests
- Mocks have been regenerated for `WorkingDir` and `WorkingDirLocker`
- `working_dir_test.go`
  - Commands that operate on filesystem paths have been updated to include the base32 encoded `path` value
  - When running `git init`, we include `-b master` as additional arguments, to ensure that `master` is our default branch (expected by the unit tests)

* Try fixing E2E tests

* Fix DefaultPendingPlanFinder

In addition to iterating over `workspaceDirs`, also iterate over `pathDirs`, and use both `workspace` and `path` to build `repoDir`.

* Fix DefaultPendingPlanFinder unit tests

* Fix DefaultProjectCommandBuilder unit tests

Co-authored-by: Kevin Snyder <kevinsnyder@KevinSnydersMBP.lan>
Co-authored-by: Kevin Snyder <kevinsnyder@ip-192-168-1-188.ec2.internal>
2022-04-08 11:55:35 -07:00

164 lines
5.7 KiB
Go

package controllers
import (
"fmt"
"net/http"
"net/url"
"github.com/runatlantis/atlantis/server/controllers/templates"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/gorilla/mux"
"github.com/runatlantis/atlantis/server/core/locking"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/logging"
)
// LocksController handles all requests relating to Atlantis locks.
type LocksController struct {
AtlantisVersion string
AtlantisURL *url.URL
Locker locking.Locker
Logger logging.SimpleLogging
ApplyLocker locking.ApplyLocker
VCSClient vcs.Client
LockDetailTemplate templates.TemplateWriter
WorkingDir events.WorkingDir
WorkingDirLocker events.WorkingDirLocker
DB *db.BoltDB
DeleteLockCommand events.DeleteLockCommand
}
// LockApply handles creating a global apply lock.
// If Lock already exists it will be a no-op
func (l *LocksController) LockApply(w http.ResponseWriter, r *http.Request) {
lock, err := l.ApplyLocker.LockApply()
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "creating apply lock failed with: %s", err)
return
}
l.respond(w, logging.Info, http.StatusOK, "Apply Lock is acquired on %s", lock.Time.Format("2006-01-02 15:04:05"))
}
// UnlockApply handles releasing a global apply lock.
// If Lock doesn't exists it will be a no-op
func (l *LocksController) UnlockApply(w http.ResponseWriter, r *http.Request) {
err := l.ApplyLocker.UnlockApply()
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "deleting apply lock failed with: %s", err)
return
}
l.respond(w, logging.Info, http.StatusOK, "Deleted apply lock")
}
// GetLock is the GET /locks/{id} route. It renders the lock detail view.
func (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
l.respond(w, logging.Warn, http.StatusBadRequest, "No lock id in request")
return
}
idUnencoded, err := url.QueryUnescape(id)
if err != nil {
l.respond(w, logging.Warn, http.StatusBadRequest, "Invalid lock id: %s", err)
return
}
lock, err := l.Locker.GetLock(idUnencoded)
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "Failed getting lock: %s", err)
return
}
if lock == nil {
l.respond(w, logging.Info, http.StatusNotFound, "No lock found at id %q", idUnencoded)
return
}
owner, repo := models.SplitRepoFullName(lock.Project.RepoFullName)
viewData := templates.LockDetailData{
LockKeyEncoded: id,
LockKey: idUnencoded,
PullRequestLink: lock.Pull.URL,
LockedBy: lock.Pull.Author,
Workspace: lock.Workspace,
AtlantisVersion: l.AtlantisVersion,
CleanedBasePath: l.AtlantisURL.Path,
RepoOwner: owner,
RepoName: repo,
}
err = l.LockDetailTemplate.Execute(w, viewData)
if err != nil {
l.Logger.Err(err.Error())
}
}
// DeleteLock handles deleting the lock at id and commenting back on the
// pull request that the lock has been deleted.
func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok || id == "" {
l.respond(w, logging.Warn, http.StatusBadRequest, "No lock id in request")
return
}
idUnencoded, err := url.PathUnescape(id)
if err != nil {
l.respond(w, logging.Warn, http.StatusBadRequest, "Invalid lock id %q. Failed with error: %s", id, err)
return
}
lock, err := l.DeleteLockCommand.DeleteLock(idUnencoded)
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "deleting lock failed with: %s", err)
return
}
if lock == nil {
l.respond(w, logging.Info, http.StatusNotFound, "No lock found at id %q", idUnencoded)
return
}
// NOTE: Because BaseRepo was added to the PullRequest model later, previous
// installations of Atlantis will have locks in their DB that do not have
// this field on PullRequest. We skip commenting in this case.
if lock.Pull.BaseRepo != (models.Repo{}) {
unlock, err := l.WorkingDirLocker.TryLock(lock.Pull.BaseRepo.FullName, lock.Pull.Num, lock.Workspace, lock.Project.Path)
if err != nil {
l.Logger.Err("unable to obtain working dir lock when trying to delete old plans: %s", err)
} else {
defer unlock()
// nolint: vetshadow
if err := l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace, lock.Project.Path); err != nil {
l.Logger.Err("unable to delete workspace: %s", err)
}
}
if err := l.DB.UpdateProjectStatus(lock.Pull, lock.Workspace, lock.Project.Path, models.DiscardedPlanStatus); err != nil {
l.Logger.Err("unable to update project status: %s", err)
}
// Once the lock has been deleted, comment back on the pull request.
comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+
"To `apply` this plan you must run `plan` again.", lock.Project.Path, lock.Workspace)
if err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment, ""); err != nil {
l.Logger.Warn("failed commenting on pull request: %s", err)
}
} else {
l.Logger.Debug("skipping commenting on pull request and deleting workspace because BaseRepo field is empty")
}
l.respond(w, logging.Info, http.StatusOK, "Deleted lock id %q", id)
}
// respond is a helper function to respond and log the response. lvl is the log
// level to log at, code is the HTTP response code.
func (l *LocksController) respond(w http.ResponseWriter, lvl logging.LogLevel, responseCode int, format string, args ...interface{}) {
response := fmt.Sprintf(format, args...)
l.Logger.Log(lvl, response)
w.WriteHeader(responseCode)
fmt.Fprintln(w, response)
}