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") }