1package statemgr 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "math/rand" 10 "os" 11 "os/user" 12 "strings" 13 "text/template" 14 "time" 15 16 uuid "github.com/hashicorp/go-uuid" 17 "github.com/hashicorp/terraform/version" 18) 19 20var rngSource = rand.New(rand.NewSource(time.Now().UnixNano())) 21 22// Locker is the interface for state managers that are able to manage 23// mutual-exclusion locks for state. 24// 25// Implementing Locker alongside Persistent relaxes some of the usual 26// implementation constraints for implementations of Refresher and Persister, 27// under the assumption that the locking mechanism effectively prevents 28// multiple Terraform processes from reading and writing state concurrently. 29// In particular, a type that implements both Locker and Persistent is only 30// required to that the Persistent implementation is concurrency-safe within 31// a single Terraform process. 32// 33// A Locker implementation must ensure that another processes with a 34// similarly-configured state manager cannot successfully obtain a lock while 35// the current process is holding it, or vice-versa, assuming that both 36// processes agree on the locking mechanism. 37// 38// A Locker is not required to prevent non-cooperating processes from 39// concurrently modifying the state, but is free to do so as an extra 40// protection. If a mandatory locking mechanism of this sort is implemented, 41// the state manager must ensure that RefreshState and PersistState calls 42// can succeed if made through the same manager instance that is holding the 43// lock, such has by retaining some sort of lock token that the Persistent 44// methods can then use. 45type Locker interface { 46 // Lock attempts to obtain a lock, using the given lock information. 47 // 48 // The result is an opaque id that can be passed to Unlock to release 49 // the lock, or an error if the lock cannot be acquired. Lock returns 50 // an instance of LockError immediately if the lock is already held, 51 // and the helper function LockWithContext uses this to automatically 52 // retry lock acquisition periodically until a timeout is reached. 53 Lock(info *LockInfo) (string, error) 54 55 // Unlock releases a lock previously acquired by Lock. 56 // 57 // If the lock cannot be released -- for example, if it was stolen by 58 // another user with some sort of administrative override privilege -- 59 // then an error is returned explaining the situation in a way that 60 // is suitable for returning to an end-user. 61 Unlock(id string) error 62} 63 64// test hook to verify that LockWithContext has attempted a lock 65var postLockHook func() 66 67// LockWithContext locks the given state manager using the provided context 68// for both timeout and cancellation. 69// 70// This method has a built-in retry/backoff behavior up to the context's 71// timeout. 72func LockWithContext(ctx context.Context, s Locker, info *LockInfo) (string, error) { 73 delay := time.Second 74 maxDelay := 16 * time.Second 75 for { 76 id, err := s.Lock(info) 77 if err == nil { 78 return id, nil 79 } 80 81 le, ok := err.(*LockError) 82 if !ok { 83 // not a lock error, so we can't retry 84 return "", err 85 } 86 87 if le == nil || le.Info == nil || le.Info.ID == "" { 88 // If we don't have a complete LockError then there's something 89 // wrong with the lock. 90 return "", err 91 } 92 93 if postLockHook != nil { 94 postLockHook() 95 } 96 97 // there's an existing lock, wait and try again 98 select { 99 case <-ctx.Done(): 100 // return the last lock error with the info 101 return "", err 102 case <-time.After(delay): 103 if delay < maxDelay { 104 delay *= 2 105 } 106 } 107 } 108} 109 110// LockInfo stores lock metadata. 111// 112// Only Operation and Info are required to be set by the caller of Lock. 113// Most callers should use NewLockInfo to create a LockInfo value with many 114// of the fields populated with suitable default values. 115type LockInfo struct { 116 // Unique ID for the lock. NewLockInfo provides a random ID, but this may 117 // be overridden by the lock implementation. The final value of ID will be 118 // returned by the call to Lock. 119 ID string 120 121 // Terraform operation, provided by the caller. 122 Operation string 123 124 // Extra information to store with the lock, provided by the caller. 125 Info string 126 127 // user@hostname when available 128 Who string 129 130 // Terraform version 131 Version string 132 133 // Time that the lock was taken. 134 Created time.Time 135 136 // Path to the state file when applicable. Set by the Lock implementation. 137 Path string 138} 139 140// NewLockInfo creates a LockInfo object and populates many of its fields 141// with suitable default values. 142func NewLockInfo() *LockInfo { 143 // this doesn't need to be cryptographically secure, just unique. 144 // Using math/rand alleviates the need to check handle the read error. 145 // Use a uuid format to match other IDs used throughout Terraform. 146 buf := make([]byte, 16) 147 rngSource.Read(buf) 148 149 id, err := uuid.FormatUUID(buf) 150 if err != nil { 151 // this of course shouldn't happen 152 panic(err) 153 } 154 155 // don't error out on user and hostname, as we don't require them 156 userName := "" 157 if userInfo, err := user.Current(); err == nil { 158 userName = userInfo.Username 159 } 160 host, _ := os.Hostname() 161 162 info := &LockInfo{ 163 ID: id, 164 Who: fmt.Sprintf("%s@%s", userName, host), 165 Version: version.Version, 166 Created: time.Now().UTC(), 167 } 168 return info 169} 170 171// Err returns the lock info formatted in an error 172func (l *LockInfo) Err() error { 173 return errors.New(l.String()) 174} 175 176// Marshal returns a string json representation of the LockInfo 177func (l *LockInfo) Marshal() []byte { 178 js, err := json.Marshal(l) 179 if err != nil { 180 panic(err) 181 } 182 return js 183} 184 185// String return a multi-line string representation of LockInfo 186func (l *LockInfo) String() string { 187 tmpl := `Lock Info: 188 ID: {{.ID}} 189 Path: {{.Path}} 190 Operation: {{.Operation}} 191 Who: {{.Who}} 192 Version: {{.Version}} 193 Created: {{.Created}} 194 Info: {{.Info}} 195` 196 197 t := template.Must(template.New("LockInfo").Parse(tmpl)) 198 var out bytes.Buffer 199 if err := t.Execute(&out, l); err != nil { 200 panic(err) 201 } 202 return out.String() 203} 204 205// LockError is a specialization of type error that is returned by Locker.Lock 206// to indicate that the lock is already held by another process and that 207// retrying may be productive to take the lock once the other process releases 208// it. 209type LockError struct { 210 Info *LockInfo 211 Err error 212} 213 214func (e *LockError) Error() string { 215 var out []string 216 if e.Err != nil { 217 out = append(out, e.Err.Error()) 218 } 219 220 if e.Info != nil { 221 out = append(out, e.Info.String()) 222 } 223 return strings.Join(out, "\n") 224} 225