mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-02 12:17:39 -06:00
f40800b3a4
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
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")
|
|
}
|