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