mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
225 lines
6.3 KiB
Go
225 lines
6.3 KiB
Go
package statemgr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"os/user"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
uuid "github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
var rngSource = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
// Locker is the interface for state managers that are able to manage
|
|
// mutual-exclusion locks for state.
|
|
//
|
|
// Implementing Locker alongside Persistent relaxes some of the usual
|
|
// implementation constraints for implementations of Refresher and Persister,
|
|
// under the assumption that the locking mechanism effectively prevents
|
|
// multiple Terraform processes from reading and writing state concurrently.
|
|
// In particular, a type that implements both Locker and Persistent is only
|
|
// required to that the Persistent implementation is concurrency-safe within
|
|
// a single Terraform process.
|
|
//
|
|
// A Locker implementation must ensure that another processes with a
|
|
// similarly-configured state manager cannot successfully obtain a lock while
|
|
// the current process is holding it, or vice-versa, assuming that both
|
|
// processes agree on the locking mechanism.
|
|
//
|
|
// A Locker is not required to prevent non-cooperating processes from
|
|
// concurrently modifying the state, but is free to do so as an extra
|
|
// protection. If a mandatory locking mechanism of this sort is implemented,
|
|
// the state manager must ensure that RefreshState and PersistState calls
|
|
// can succeed if made through the same manager instance that is holding the
|
|
// lock, such has by retaining some sort of lock token that the Persistent
|
|
// methods can then use.
|
|
type Locker interface {
|
|
// Lock attempts to obtain a lock, using the given lock information.
|
|
//
|
|
// The result is an opaque id that can be passed to Unlock to release
|
|
// the lock, or an error if the lock cannot be acquired. Lock returns
|
|
// an instance of LockError immediately if the lock is already held,
|
|
// and the helper function LockWithContext uses this to automatically
|
|
// retry lock acquisition periodically until a timeout is reached.
|
|
Lock(info *LockInfo) (string, error)
|
|
|
|
// Unlock releases a lock previously acquired by Lock.
|
|
//
|
|
// If the lock cannot be released -- for example, if it was stolen by
|
|
// another user with some sort of administrative override privilege --
|
|
// then an error is returned explaining the situation in a way that
|
|
// is suitable for returning to an end-user.
|
|
Unlock(id string) error
|
|
}
|
|
|
|
// test hook to verify that LockWithContext has attempted a lock
|
|
var postLockHook func()
|
|
|
|
// LockWithContext locks the given state manager using the provided context
|
|
// for both timeout and cancellation.
|
|
//
|
|
// This method has a built-in retry/backoff behavior up to the context's
|
|
// timeout.
|
|
func LockWithContext(ctx context.Context, s Locker, info *LockInfo) (string, error) {
|
|
delay := time.Second
|
|
maxDelay := 16 * time.Second
|
|
for {
|
|
id, err := s.Lock(info)
|
|
if err == nil {
|
|
return id, nil
|
|
}
|
|
|
|
le, ok := err.(*LockError)
|
|
if !ok {
|
|
// not a lock error, so we can't retry
|
|
return "", err
|
|
}
|
|
|
|
if le == nil || le.Info == nil || le.Info.ID == "" {
|
|
// If we don't have a complete LockError then there's something
|
|
// wrong with the lock.
|
|
return "", err
|
|
}
|
|
|
|
if postLockHook != nil {
|
|
postLockHook()
|
|
}
|
|
|
|
// there's an existing lock, wait and try again
|
|
select {
|
|
case <-ctx.Done():
|
|
// return the last lock error with the info
|
|
return "", err
|
|
case <-time.After(delay):
|
|
if delay < maxDelay {
|
|
delay *= 2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// LockInfo stores lock metadata.
|
|
//
|
|
// Only Operation and Info are required to be set by the caller of Lock.
|
|
// Most callers should use NewLockInfo to create a LockInfo value with many
|
|
// of the fields populated with suitable default values.
|
|
type LockInfo struct {
|
|
// Unique ID for the lock. NewLockInfo provides a random ID, but this may
|
|
// be overridden by the lock implementation. The final value of ID will be
|
|
// returned by the call to Lock.
|
|
ID string
|
|
|
|
// Terraform operation, provided by the caller.
|
|
Operation string
|
|
|
|
// Extra information to store with the lock, provided by the caller.
|
|
Info string
|
|
|
|
// user@hostname when available
|
|
Who string
|
|
|
|
// Terraform version
|
|
Version string
|
|
|
|
// Time that the lock was taken.
|
|
Created time.Time
|
|
|
|
// Path to the state file when applicable. Set by the Lock implementation.
|
|
Path string
|
|
}
|
|
|
|
// NewLockInfo creates a LockInfo object and populates many of its fields
|
|
// with suitable default values.
|
|
func NewLockInfo() *LockInfo {
|
|
// this doesn't need to be cryptographically secure, just unique.
|
|
// Using math/rand alleviates the need to check handle the read error.
|
|
// Use a uuid format to match other IDs used throughout Terraform.
|
|
buf := make([]byte, 16)
|
|
rngSource.Read(buf)
|
|
|
|
id, err := uuid.FormatUUID(buf)
|
|
if err != nil {
|
|
// this of course shouldn't happen
|
|
panic(err)
|
|
}
|
|
|
|
// don't error out on user and hostname, as we don't require them
|
|
userName := ""
|
|
if userInfo, err := user.Current(); err == nil {
|
|
userName = userInfo.Username
|
|
}
|
|
host, _ := os.Hostname()
|
|
|
|
info := &LockInfo{
|
|
ID: id,
|
|
Who: fmt.Sprintf("%s@%s", userName, host),
|
|
Version: version.Version,
|
|
Created: time.Now().UTC(),
|
|
}
|
|
return info
|
|
}
|
|
|
|
// Err returns the lock info formatted in an error
|
|
func (l *LockInfo) Err() error {
|
|
return errors.New(l.String())
|
|
}
|
|
|
|
// Marshal returns a string json representation of the LockInfo
|
|
func (l *LockInfo) Marshal() []byte {
|
|
js, err := json.Marshal(l)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return js
|
|
}
|
|
|
|
// String return a multi-line string representation of LockInfo
|
|
func (l *LockInfo) String() string {
|
|
tmpl := `Lock Info:
|
|
ID: {{.ID}}
|
|
Path: {{.Path}}
|
|
Operation: {{.Operation}}
|
|
Who: {{.Who}}
|
|
Version: {{.Version}}
|
|
Created: {{.Created}}
|
|
Info: {{.Info}}
|
|
`
|
|
|
|
t := template.Must(template.New("LockInfo").Parse(tmpl))
|
|
var out bytes.Buffer
|
|
if err := t.Execute(&out, l); err != nil {
|
|
panic(err)
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// LockError is a specialization of type error that is returned by Locker.Lock
|
|
// to indicate that the lock is already held by another process and that
|
|
// retrying may be productive to take the lock once the other process releases
|
|
// it.
|
|
type LockError struct {
|
|
Info *LockInfo
|
|
Err error
|
|
}
|
|
|
|
func (e *LockError) Error() string {
|
|
var out []string
|
|
if e.Err != nil {
|
|
out = append(out, e.Err.Error())
|
|
}
|
|
|
|
if e.Info != nil {
|
|
out = append(out, e.Info.String())
|
|
}
|
|
return strings.Join(out, "\n")
|
|
}
|