"http" backend to return existing lock metadata on lock conflict, rather than new lock metadata (#2090)

Signed-off-by: Bari, Haider <haider.bari@fmr.com>
Co-authored-by: Bari, Haider <haider.bari@fmr.com>
This commit is contained in:
Haider Bari 2024-11-19 12:57:19 +00:00 committed by GitHub
parent e4f685d12b
commit eb15415a2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 1 deletions

View File

@ -121,7 +121,7 @@ func (c *httpClient) Lock(info *statemgr.LockInfo) (string, error) {
}
}
return "", &statemgr.LockError{
Info: info,
Info: &existing,
Err: fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID),
}
default:

View File

@ -14,9 +14,11 @@ import (
"net/url"
"reflect"
"testing"
"time"
"github.com/hashicorp/go-retryablehttp"
"github.com/opentofu/opentofu/internal/states/remote"
"github.com/opentofu/opentofu/internal/states/statemgr"
)
func TestHTTPClient_impl(t *testing.T) {
@ -251,3 +253,107 @@ func TestHttpClient_IsLockingEnabled(t *testing.T) {
})
}
}
// Tests the Lock method for the HTTP client.
// Test to see correct lock info is returned
func TestHttpClient_lock(t *testing.T) {
stateLockInfoA := statemgr.LockInfo{
ID: "ada-lovelace-state-lock-id",
Who: "AdaLovelace",
Operation: "TestTypePlan",
Created: time.Date(2023, time.August, 16, 15, 9, 26, 0, time.UTC),
}
stateLockInfoRemoteB := statemgr.LockInfo{
ID: "linus-torvalds-http-remote-state-lock-id",
Who: "LinusTorvalds",
Operation: "TestTypePlan",
Created: time.Date(2024, time.August, 15, 9, 0, 26, 0, time.UTC),
}
testCases := []struct {
name string
lockInfo *statemgr.LockInfo
lockResponseStatus int
lockResponseBody []byte
expectedStateLockID string
expectedErrorMsg error
}{
{
// Successful locking HTTP remote state
name: "Successfully locked",
lockInfo: &stateLockInfoA,
lockResponseStatus: http.StatusOK,
lockResponseBody: nil,
expectedStateLockID: stateLockInfoA.ID,
expectedErrorMsg: nil,
},
{
// Failed to lock state, HTTP remote state already locked
name: "Locked remote state",
lockInfo: &stateLockInfoA,
lockResponseStatus: http.StatusLocked,
lockResponseBody: stateLockInfoRemoteB.Marshal(),
expectedStateLockID: "",
expectedErrorMsg: &statemgr.LockError{
Info: &stateLockInfoRemoteB,
Err: fmt.Errorf("HTTP remote state already locked: ID=%s", stateLockInfoRemoteB.ID),
},
},
{
// Failed to lock state HTTP remote state already locked. No remote lock details returned
name: "Locked remote state failed to unmarshal body",
lockInfo: &stateLockInfoA,
lockResponseStatus: http.StatusLocked,
lockResponseBody: nil,
expectedStateLockID: "",
expectedErrorMsg: &statemgr.LockError{
Info: &stateLockInfoA,
Err: fmt.Errorf("HTTP remote state already locked, failed to unmarshal body"),
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
handler := func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(tt.lockResponseStatus)
_, err := w.Write(tt.lockResponseBody)
if err != nil {
t.Fatalf("Failed to write response body: %v", err)
}
}
ts := httptest.NewServer(http.HandlerFunc(handler))
defer ts.Close()
lockURL, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("Failed to parse lockURL: %v", err)
}
client := &httpClient{
LockURL: lockURL,
LockMethod: "LOCK",
Client: retryablehttp.NewClient(),
}
lockID, err := client.Lock(tt.lockInfo)
if tt.expectedErrorMsg != nil && err == nil {
// no expected error
t.Errorf("Lock() no expected error = %v", tt.expectedErrorMsg)
}
if tt.expectedErrorMsg == nil && err != nil {
// unexpected error
t.Errorf("Lock() unexpected error = %v", err)
}
if tt.expectedErrorMsg != nil && err.Error() != tt.expectedErrorMsg.Error() {
// mismatched errors
t.Errorf("Lock() error = %v, want %v", err, tt.expectedErrorMsg)
}
if lockID != tt.expectedStateLockID {
t.Errorf("Lock() = %v, want %v", lockID, tt.expectedStateLockID)
}
})
}
}