feat: New API endpoint to list locks (#5328)

Signed-off-by: Patrick Vinograd <patrick.vinograd@devoted.com>
This commit is contained in:
Patrick Vinograd
2025-02-16 07:49:05 -08:00
committed by GitHub
parent ca7bf2774f
commit 1f83f13cb5
4 changed files with 141 additions and 0 deletions

View File

@@ -161,6 +161,38 @@ curl --request POST 'https://<ATLANTIS_HOST_NAME>/api/apply' \
The endpoints listed in this section are non-destructive and therefore don't require authentication nor special secret token.
### GET /api/locks
#### Description
List the currently held project locks.
#### Sample Request
```shell
curl --request GET 'https://<ATLANTIS_HOST_NAME>/api/locks'
```
#### Sample Response
```json
{
"Locks": [
{
"Name": "lock-id",
"ProjectName": "terraform",
"ProjectRepo": "owner/repo",
"ProjectRepoPath": "/path",
"PullID": "123",
"PullURL": "url",
"User": "jdoe",
"Workspace": "default",
"Time": "2025-02-13T16:47:42.040856-08:00"
}
]
}
```
### GET /status
#### Description

View File

@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/runatlantis/atlantis/server/core/locking"
@@ -159,6 +160,55 @@ func (a *APIController) Apply(w http.ResponseWriter, r *http.Request) {
a.respond(w, logging.Warn, code, "%s", string(response))
}
type LockDetail struct {
Name string
ProjectName string
ProjectRepo string
ProjectRepoPath string
PullID int `json:",string"`
PullURL string
User string
Workspace string
Time time.Time
}
type ListLocksResult struct {
Locks []LockDetail
}
func (a *APIController) ListLocks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
locks, err := a.Locker.List()
if err != nil {
a.apiReportError(w, http.StatusInternalServerError, err)
return
}
result := ListLocksResult{}
for name, lock := range locks {
lockDetail := LockDetail{
name,
lock.Project.ProjectName,
lock.Project.RepoFullName,
lock.Project.Path,
lock.Pull.Num,
lock.Pull.URL,
lock.User.Username,
lock.Workspace,
lock.Time,
}
result.Locks = append(result.Locks, lockDetail)
}
response, err := json.Marshal(result)
if err != nil {
a.apiReportError(w, http.StatusInternalServerError, err)
return
}
a.respond(w, logging.Warn, http.StatusOK, "%s", string(response))
}
func (a *APIController) apiSetup(ctx *command.Context) error {
pull := ctx.Pull
baseRepo := ctx.Pull.BaseRepo

View File

@@ -3,9 +3,11 @@ package controllers_test
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/controllers"
@@ -198,6 +200,62 @@ func TestAPIController_Apply(t *testing.T) {
projectCommandRunner.VerifyWasCalled(Times(expectedCalls)).Apply(Any[command.ProjectContext]())
}
func TestAPIController_ListLocks(t *testing.T) {
ac, _, _ := setup(t)
time := time.Now()
expected := controllers.ListLocksResult{[]controllers.LockDetail{
{
Name: "lock-id",
ProjectName: "terraform",
ProjectRepo: "owner/repo",
ProjectRepoPath: "/path",
PullID: 123,
PullURL: "url",
User: "jdoe",
Workspace: "default",
Time: time,
},
},
}
mockLock := models.ProjectLock{
Project: models.Project{ProjectName: "terraform", RepoFullName: "owner/repo", Path: "/path"},
Pull: models.PullRequest{Num: 123, URL: "url", Author: "lkysow"},
User: models.User{Username: "jdoe"},
Workspace: "default",
Time: time,
}
mockLocks := map[string]models.ProjectLock{
"lock-id": mockLock,
}
When(ac.Locker.List()).ThenReturn(mockLocks, nil)
req, _ := http.NewRequest("GET", "", nil)
w := httptest.NewRecorder()
ac.ListLocks(w, req)
response, _ := io.ReadAll(w.Result().Body)
var result controllers.ListLocksResult
err := json.Unmarshal(response, &result)
Ok(t, err)
Equals(t, expected, result)
}
func TestAPIController_ListLocksEmpty(t *testing.T) {
ac, _, _ := setup(t)
expected := controllers.ListLocksResult{}
mockLocks := map[string]models.ProjectLock{}
When(ac.Locker.List()).ThenReturn(mockLocks, nil)
req, _ := http.NewRequest("GET", "", nil)
w := httptest.NewRecorder()
ac.ListLocks(w, req)
response, _ := io.ReadAll(w.Result().Body)
var result controllers.ListLocksResult
err := json.Unmarshal(response, &result)
Ok(t, err)
Equals(t, expected, result)
}
func setup(t *testing.T) (controllers.APIController, *MockProjectCommandBuilder, *MockProjectCommandRunner) {
RegisterMockTestingT(t)
locker := NewMockLocker()

View File

@@ -1038,6 +1038,7 @@ func (s *Server) Start() error {
s.Router.HandleFunc("/events", s.VCSEventsController.Post).Methods("POST")
s.Router.HandleFunc("/api/plan", s.APIController.Plan).Methods("POST")
s.Router.HandleFunc("/api/apply", s.APIController.Apply).Methods("POST")
s.Router.HandleFunc("/api/locks", s.APIController.ListLocks).Methods("GET")
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("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}")