mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
"force-unlock" support for the HTTP backend (#2381)
Signed-off-by: Bari, Haider <haider.bari@fmr.com> Co-authored-by: Bari, Haider <haider.bari@fmr.com>
This commit is contained in:
parent
ebc900bec9
commit
be72380cdb
@ -144,7 +144,25 @@ func (c *httpClient) Unlock(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, c.jsonLockInfo, "unlock")
|
||||
var lockInfo statemgr.LockInfo
|
||||
|
||||
// force unlock command does not instantiate statemgr.LockInfo
|
||||
// which means that c.jsonLockInfo will be nil
|
||||
if c.jsonLockInfo != nil {
|
||||
if err := json.Unmarshal(c.jsonLockInfo, &lockInfo); err != nil { //nolint:musttag // for now add musttag until we fully adopt the linting rules
|
||||
return fmt.Errorf("failed to unmarshal jsonLockInfo: %w", err)
|
||||
}
|
||||
if lockInfo.ID != id {
|
||||
return &statemgr.LockError{
|
||||
Info: &lockInfo,
|
||||
Err: fmt.Errorf("lock id %q does not match existing lock", id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lockInfo.ID = id
|
||||
|
||||
resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, lockInfo.Marshal(), "unlock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -254,6 +254,130 @@ func TestHttpClient_IsLockingEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the UnLock method for the HTTP client.
|
||||
func TestHttpClient_Unlock(t *testing.T) {
|
||||
stateLockInfoA := statemgr.LockInfo{
|
||||
ID: "bjarne-stroustrup-state-lock-id",
|
||||
Who: "BjarneStroustrup",
|
||||
Operation: "TestTypePlan",
|
||||
Created: time.Date(2023, time.August, 21, 15, 9, 26, 0, time.UTC),
|
||||
}
|
||||
|
||||
stateLockInfoB := statemgr.LockInfo{
|
||||
ID: "edsger-dijkstra-state-lock-id",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
lockID string
|
||||
jsonLockInfo []byte
|
||||
lockResponseStatus int
|
||||
lockResponseBody []byte
|
||||
expectedErrorMsg error
|
||||
expectedPayload []byte
|
||||
}{
|
||||
{
|
||||
// Successful unlocking HTTP remote state
|
||||
name: "Successfully unlocked",
|
||||
lockID: stateLockInfoA.ID,
|
||||
jsonLockInfo: stateLockInfoA.Marshal(),
|
||||
lockResponseStatus: http.StatusOK,
|
||||
lockResponseBody: nil,
|
||||
expectedErrorMsg: nil,
|
||||
expectedPayload: stateLockInfoA.Marshal(),
|
||||
},
|
||||
{
|
||||
// Lock ID parameter does not match with LockInfo object Lock ID
|
||||
name: "Lock ID's don't match",
|
||||
lockID: stateLockInfoB.ID,
|
||||
jsonLockInfo: stateLockInfoA.Marshal(),
|
||||
lockResponseStatus: 0,
|
||||
lockResponseBody: nil,
|
||||
expectedErrorMsg: &statemgr.LockError{
|
||||
Info: &stateLockInfoA,
|
||||
Err: fmt.Errorf("lock id %q does not match existing lock", stateLockInfoB.ID),
|
||||
},
|
||||
expectedPayload: nil,
|
||||
},
|
||||
{
|
||||
// Failed unmarshal jsonLockInfo into LockInfo object
|
||||
name: "Failed to unmarshal jsonLockInfo",
|
||||
lockID: stateLockInfoA.ID,
|
||||
jsonLockInfo: []byte("Simplicity is prerequisite for reliability."),
|
||||
lockResponseStatus: 0,
|
||||
lockResponseBody: nil,
|
||||
expectedErrorMsg: fmt.Errorf("failed to unmarshal jsonLockInfo: invalid character 'S' looking for beginning of value"),
|
||||
expectedPayload: nil,
|
||||
},
|
||||
{
|
||||
// Force unlock command being executed
|
||||
name: "Successful force unlock",
|
||||
lockID: stateLockInfoB.ID,
|
||||
jsonLockInfo: nil,
|
||||
lockResponseStatus: http.StatusOK,
|
||||
lockResponseBody: nil,
|
||||
expectedErrorMsg: nil,
|
||||
expectedPayload: stateLockInfoB.Marshal(),
|
||||
},
|
||||
{
|
||||
// Force unlock command being executed
|
||||
name: "Unsuccessful force unlock",
|
||||
lockID: stateLockInfoB.ID,
|
||||
jsonLockInfo: nil,
|
||||
lockResponseStatus: http.StatusNotFound,
|
||||
lockResponseBody: nil,
|
||||
expectedErrorMsg: fmt.Errorf("Unexpected HTTP response code %d", http.StatusNotFound),
|
||||
expectedPayload: stateLockInfoB.Marshal(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var receivedPayload []byte
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedPayload, _ = io.ReadAll(r.Body)
|
||||
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()
|
||||
|
||||
unlockURL, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse lockURL: %v", err)
|
||||
}
|
||||
|
||||
client := &httpClient{
|
||||
UnlockURL: unlockURL,
|
||||
LockMethod: "UNLOCK",
|
||||
Client: retryablehttp.NewClient(),
|
||||
jsonLockInfo: tt.jsonLockInfo,
|
||||
}
|
||||
|
||||
err = client.Unlock(tt.lockID)
|
||||
if tt.expectedErrorMsg != nil && err == nil {
|
||||
// no expected error
|
||||
t.Errorf("UnLock() no expected error = %v", tt.expectedErrorMsg)
|
||||
}
|
||||
if tt.expectedErrorMsg == nil && err != nil {
|
||||
// unexpected error
|
||||
t.Errorf("UnLock() unexpected error = %v", err)
|
||||
}
|
||||
if tt.expectedErrorMsg != nil && err.Error() != tt.expectedErrorMsg.Error() {
|
||||
// mismatched errors
|
||||
t.Errorf("UnLock() error = %v, want %v", err, tt.expectedErrorMsg)
|
||||
}
|
||||
if !bytes.Equal(receivedPayload, tt.expectedPayload) {
|
||||
t.Errorf("UnLock() payload = %v, want %v", receivedPayload, tt.expectedPayload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the Lock method for the HTTP client.
|
||||
// Test to see correct lock info is returned
|
||||
func TestHttpClient_lock(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user