Clean up LockInfo and LockError and use them

Gove LockInfo a Marshal method for easy serialization, and a String
method for more readable output.

Have the state.Locker implementations use LockError when possible to
return LockInfo and an error.
This commit is contained in:
James Bardin 2017-02-15 14:01:18 -05:00
parent 888af93356
commit ec00564be6
11 changed files with 95 additions and 46 deletions

View File

@ -4,10 +4,10 @@ import (
"crypto/md5" "crypto/md5"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"time" "time"
consulapi "github.com/hashicorp/consul/api" consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/errwrap"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/state/remote"
@ -62,15 +62,10 @@ func (c *RemoteClient) putLockInfo(info *state.LockInfo) error {
info.Path = c.Path info.Path = c.Path
info.Created = time.Now().UTC() info.Created = time.Now().UTC()
js, err := json.Marshal(info)
if err != nil {
return err
}
kv := c.Client.KV() kv := c.Client.KV()
_, err = kv.Put(&consulapi.KVPair{ _, err := kv.Put(&consulapi.KVPair{
Key: c.Path + lockInfoSuffix, Key: c.Path + lockInfoSuffix,
Value: js, Value: info.Marshal(),
}, nil) }, nil)
return err return err
@ -89,7 +84,7 @@ func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
li := &state.LockInfo{} li := &state.LockInfo{}
err = json.Unmarshal(pair.Value, li) err = json.Unmarshal(pair.Value, li)
if err != nil { if err != nil {
return nil, errwrap.Wrapf("error unmarshaling lock info: {{err}}", err) return nil, fmt.Errorf("error unmarshaling lock info: %s", err)
} }
return li, nil return li, nil
@ -126,24 +121,33 @@ func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
c.consulLock = lock c.consulLock = lock
} }
lockErr := &state.LockError{}
lockCh, err := c.consulLock.Lock(make(chan struct{})) lockCh, err := c.consulLock.Lock(make(chan struct{}))
if err != nil { if err != nil {
return "", err lockErr.Err = err
return "", lockErr
} }
if lockCh == nil { if lockCh == nil {
lockInfo, e := c.getLockInfo() lockInfo, e := c.getLockInfo()
if e != nil { if e != nil {
return "", e lockErr.Err = e
return "", lockErr
} }
return "", lockInfo.Err()
lockErr.Info = lockInfo
return "", lockErr
} }
c.lockCh = lockCh c.lockCh = lockCh
err = c.putLockInfo(info) err = c.putLockInfo(info)
if err != nil { if err != nil {
err = multierror.Append(err, c.Unlock(info.ID)) if unlockErr := c.Unlock(info.ID); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
return "", err return "", err
} }

View File

@ -140,7 +140,7 @@ func TestApply_destroyLockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -87,7 +87,7 @@ func TestApply_lockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -72,7 +72,7 @@ func TestPlan_lockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -91,7 +91,7 @@ func TestRefresh_lockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -84,7 +84,7 @@ func TestTaint_lockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -90,7 +90,7 @@ func TestUntaint_lockedState(t *testing.T) {
} }
output := ui.ErrorWriter.String() output := ui.ErrorWriter.String()
if !strings.Contains(output, "locked") { if !strings.Contains(output, "lock") {
t.Fatal("command output does not look like a lock error:", output) t.Fatal("command output does not look like a lock error:", output)
} }
} }

View File

@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/hashicorp/errwrap" multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -148,11 +148,17 @@ func (s *LocalState) Lock(info *LockInfo) (string, error) {
} }
if err := s.lock(); err != nil { if err := s.lock(); err != nil {
if info, err := s.lockInfo(); err == nil { info, infoErr := s.lockInfo()
return "", info.Err() if infoErr != nil {
err = multierror.Append(err, infoErr)
} }
return "", fmt.Errorf("state file %q locked: %s", s.Path, err) lockErr := &LockError{
Info: info,
Err: err,
}
return "", lockErr
} }
s.lockID = info.ID s.lockID = info.ID
@ -167,8 +173,8 @@ func (s *LocalState) Unlock(id string) error {
if id != s.lockID { if id != s.lockID {
idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
info, err := s.lockInfo() info, err := s.lockInfo()
if err == nil { if err != nil {
return errwrap.Wrap(idErr, err) err = multierror.Append(idErr, err)
} }
return &LockError{ return &LockError{
@ -257,12 +263,7 @@ func (s *LocalState) writeLockInfo(info *LockInfo) error {
info.Path = s.Path info.Path = s.Path
info.Created = time.Now().UTC() info.Created = time.Now().UTC()
infoData, err := json.Marshal(info) err := ioutil.WriteFile(path, info.Marshal(), 0600)
if err != nil {
panic(fmt.Sprintf("could not marshal lock info: %#v", info))
}
err = ioutil.WriteFile(path, infoData, 0600)
if err != nil { if err != nil {
return fmt.Errorf("could not write lock info for %q: %s", s.Path, err) return fmt.Errorf("could not write lock info for %q: %s", s.Path, err)
} }

View File

@ -223,7 +223,7 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
putParams := &dynamodb.PutItemInput{ putParams := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{ Item: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(stateName)}, "LockID": {S: aws.String(stateName)},
"Info": {S: aws.String(info.String())}, "Info": {S: aws.String(string(info.Marshal()))},
}, },
TableName: aws.String(c.lockTable), TableName: aws.String(c.lockTable),
ConditionExpression: aws.String("attribute_not_exists(LockID)"), ConditionExpression: aws.String("attribute_not_exists(LockID)"),
@ -231,12 +231,16 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
_, err := c.dynClient.PutItem(putParams) _, err := c.dynClient.PutItem(putParams)
if err != nil { if err != nil {
lockInfo, err := c.getLockInfo() lockInfo, infoErr := c.getLockInfo()
if err != nil { if infoErr != nil {
return "", fmt.Errorf("s3 state file %q locked, failed to retrieve info: %s", stateName, err) err = multierror.Append(err, infoErr)
} }
return info.ID, lockInfo.Err() lockErr := &state.LockError{
Err: err,
Info: lockInfo,
}
return "", lockErr
} }
return info.ID, nil return info.ID, nil
} }
@ -274,16 +278,21 @@ func (c *S3Client) Unlock(id string) error {
return nil return nil
} }
lockErr := &state.LockError{}
// TODO: store the path and lock ID in separate fields, and have proper // TODO: store the path and lock ID in separate fields, and have proper
// projection expression only delete the lock if both match, rather than // projection expression only delete the lock if both match, rather than
// checking the ID from the info field first. // checking the ID from the info field first.
lockInfo, err := c.getLockInfo() lockInfo, err := c.getLockInfo()
if err != nil { if err != nil {
return fmt.Errorf("failed to retrieve lock info: %s", err) lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
return lockErr
} }
lockErr.Info = lockInfo
if lockInfo.ID != id { if lockInfo.ID != id {
return fmt.Errorf("lock id %q does not match existing lock", id) lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
return lockErr
} }
params := &dynamodb.DeleteItemInput{ params := &dynamodb.DeleteItemInput{
@ -295,7 +304,8 @@ func (c *S3Client) Unlock(id string) error {
_, err = c.dynClient.DeleteItem(params) _, err = c.dynClient.DeleteItem(params)
if err != nil { if err != nil {
return err lockErr.Err = err
return lockErr
} }
return nil return nil
} }

View File

@ -1,12 +1,15 @@
package state package state
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"os" "os"
"os/user" "os/user"
"strings" "strings"
"text/template"
"time" "time"
uuid "github.com/hashicorp/go-uuid" uuid "github.com/hashicorp/go-uuid"
@ -84,12 +87,15 @@ func NewLockInfo() *LockInfo {
} }
// don't error out on user and hostname, as we don't require them // don't error out on user and hostname, as we don't require them
username, _ := user.Current() userName := ""
if userInfo, err := user.Current(); err == nil {
userName = userInfo.Username
}
host, _ := os.Hostname() host, _ := os.Hostname()
info := &LockInfo{ info := &LockInfo{
ID: id, ID: id,
Who: fmt.Sprintf("%s@%s", username, host), Who: fmt.Sprintf("%s@%s", userName, host),
Version: terraform.Version, Version: terraform.Version,
Created: time.Now().UTC(), Created: time.Now().UTC(),
} }
@ -123,16 +129,36 @@ type LockInfo struct {
// Err returns the lock info formatted in an error // Err returns the lock info formatted in an error
func (l *LockInfo) Err() error { func (l *LockInfo) Err() error {
return fmt.Errorf("state locked. path:%q, created:%s, info:%q", return errors.New(l.String())
l.Path, l.Created, l.Info)
} }
func (l *LockInfo) String() string { // Marshal returns a string json representation of the LockInfo
func (l *LockInfo) Marshal() []byte {
js, err := json.Marshal(l) js, err := json.Marshal(l)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return string(js) 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()
} }
type LockError struct { type LockError struct {
@ -147,7 +173,7 @@ func (e *LockError) Error() string {
} }
if e.Info != nil { if e.Info != nil {
out = append(out, e.Info.Err().Error()) out = append(out, e.Info.String())
} }
return strings.Join(out, "\n") return strings.Join(out, "\n")
} }

View File

@ -1,6 +1,7 @@
package state package state
import ( import (
"encoding/json"
"flag" "flag"
"io/ioutil" "io/ioutil"
"log" "log"
@ -41,4 +42,11 @@ func TestNewLockInfo(t *testing.T) {
if info1.ID == info2.ID { if info1.ID == info2.ID {
t.Fatal("multiple LockInfo with identical IDs") t.Fatal("multiple LockInfo with identical IDs")
} }
// test the JSON output is valid
newInfo := &LockInfo{}
err := json.Unmarshal(info1.Marshal(), newInfo)
if err != nil {
t.Fatal(err)
}
} }