tfstated/pkg/database/locks.go
Julien Dessaux 20bc9fe17a
All checks were successful
main / main (push) Successful in 2m49s
main / deploy (push) Has been skipped
main / publish (push) Has been skipped
feat(webui): add state force unlock
Closes #13
2025-05-02 00:43:18 +02:00

98 lines
2.6 KiB
Go

package database
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"git.adyxax.org/adyxax/tfstated/pkg/model"
"go.n16f.net/uuid"
)
// Atomically check the lock status of a state and lock it if unlocked. Returns
// true if the function locked the state, otherwise returns false and the lock
// parameter is updated to the value of the existing lock
func (db *DB) SetLockOrGetExistingLock(path string, lock any) (bool, error) {
ret := false
return ret, db.WithTransaction(func(tx *sql.Tx) error {
var lockData []byte
err := tx.QueryRowContext(db.ctx,
`SELECT json_extract(lock, '$')
FROM states
WHERE path = ?;`,
path).Scan(&lockData)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if lockData, err = json.Marshal(lock); err != nil {
return fmt.Errorf("failed to marshall lock data: %w", err)
}
var stateId uuid.UUID
if err := stateId.Generate(uuid.V7); err != nil {
return fmt.Errorf("failed to generate state id: %w", err)
}
_, err := tx.ExecContext(db.ctx,
`INSERT INTO states(id, path, lock)
VALUES (?, ?, jsonb(?))`,
stateId, path, lockData)
if err != nil {
return fmt.Errorf("failed to create new state: %w", err)
}
ret = true
return nil
}
return fmt.Errorf("failed to select lock data from state: %w", err)
}
if lockData != nil {
if err := json.Unmarshal(lockData, lock); err != nil {
return fmt.Errorf("failed to unmarshal lock data: %w", err)
}
return nil
}
if lockData, err = json.Marshal(lock); err != nil {
return fmt.Errorf("failed to marshal lock data: %w", err)
}
_, err = tx.ExecContext(db.ctx,
`UPDATE states
SET lock = jsonb(?)
WHERE path = ?;`,
lockData, path)
if err != nil {
return fmt.Errorf("failed to set lock data: %w", err)
}
ret = true
return nil
})
}
func (db *DB) Unlock(path string, lock any) (bool, error) {
data, err := json.Marshal(lock)
if err != nil {
return false, fmt.Errorf("failed to marshal lock data: %w", err)
}
result, err := db.Exec(
`UPDATE states
SET lock = NULL
WHERE path = ? and lock = jsonb(?);`,
path, data)
if err != nil {
return false, fmt.Errorf("failed to update state: %w", err)
}
n, err := result.RowsAffected()
if err != nil {
return false, fmt.Errorf("failed to get affected rows: %w", err)
}
return n == 1, nil
}
func (db *DB) ForceUnlock(state *model.State) error {
_, err := db.Exec(
`UPDATE states
SET lock = NULL
WHERE id = ?;`,
state.Id)
if err != nil {
return fmt.Errorf("failed to update state: %w", err)
}
return nil
}