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