Use NewLockInfo to get a pre-populated value

Using NewLockInfo ensure we start with all required fields filled.
This commit is contained in:
James Bardin 2017-02-15 10:25:04 -05:00
parent 52b2343672
commit f5ed8cd288
12 changed files with 132 additions and 64 deletions

View File

@ -30,10 +30,8 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
} }
if op.LockState { if op.LockState {
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Info: op.Type.String(), lockInfo.Operation = op.Type.String()
}
_, err := clistate.Lock(s, lockInfo, b.CLI, b.Colorize()) _, err := clistate.Lock(s, lockInfo, b.CLI, b.Colorize())
if err != nil { if err != nil {
return nil, nil, errwrap.Wrapf("Error locking state: {{err}}", err) return nil, nil, errwrap.Wrapf("Error locking state: {{err}}", err)

View File

@ -533,10 +533,8 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) {
} }
// Lock the state if we can // Lock the state if we can
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Operation: "plan", lockInfo.Operation = "backend from plan"
Info: "backend from plan",
}
lockID, err := clistate.Lock(realMgr, lockInfo, m.Ui, m.Colorize()) lockID, err := clistate.Lock(realMgr, lockInfo, m.Ui, m.Colorize())
if err != nil { if err != nil {
@ -991,9 +989,8 @@ func (m *Meta) backend_C_r_s(
} }
// Lock the state if we can // Lock the state if we can
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Info: "backend from config", lockInfo.Operation = "backend from config"
}
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize()) lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil { if err != nil {
@ -1100,9 +1097,9 @@ func (m *Meta) backend_C_r_S_changed(
} }
// Lock the state if we can // Lock the state if we can
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Info: "backend from config", lockInfo.Operation = "backend from config"
}
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize()) lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil { if err != nil {
return nil, fmt.Errorf("Error locking state: %s", err) return nil, fmt.Errorf("Error locking state: %s", err)
@ -1261,9 +1258,8 @@ func (m *Meta) backend_C_R_S_unchanged(
} }
// Lock the state if we can // Lock the state if we can
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Info: "backend from config", lockInfo.Operation = "backend from config"
}
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize()) lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil { if err != nil {

View File

@ -24,9 +24,8 @@ import (
// //
// This will attempt to lock both states for the migration. // This will attempt to lock both states for the migration.
func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
lockInfoOne := &state.LockInfo{ lockInfoOne := state.NewLockInfo()
Info: "migration source state", lockInfoOne.Operation = "migration source state"
}
lockIDOne, err := clistate.Lock(opts.One, lockInfoOne, m.Ui, m.Colorize()) lockIDOne, err := clistate.Lock(opts.One, lockInfoOne, m.Ui, m.Colorize())
if err != nil { if err != nil {
@ -34,9 +33,8 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error {
} }
defer clistate.Unlock(opts.One, lockIDOne, m.Ui, m.Colorize()) defer clistate.Unlock(opts.One, lockIDOne, m.Ui, m.Colorize())
lockInfoTwo := &state.LockInfo{ lockInfoTwo := state.NewLockInfo()
Info: "migration source state", lockInfoTwo.Operation = "migration source state"
}
lockIDTwo, err := clistate.Lock(opts.Two, lockInfoTwo, m.Ui, m.Colorize()) lockIDTwo, err := clistate.Lock(opts.Two, lockInfoTwo, m.Ui, m.Colorize())
if err != nil { if err != nil {

View File

@ -74,9 +74,8 @@ func (c *TaintCommand) Run(args []string) int {
} }
if c.Meta.stateLock { if c.Meta.stateLock {
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Operation: "taint", lockInfo.Operation = "taint"
}
lockID, err := clistate.Lock(st, lockInfo, c.Ui, c.Colorize()) lockID, err := clistate.Lock(st, lockInfo, c.Ui, c.Colorize())
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))

View File

@ -23,7 +23,11 @@ func main() {
Path: os.Args[1], Path: os.Args[1],
} }
lockID, err := s.Lock(&state.LockInfo{Operation: "test", Info: "state locker"}) info := state.NewLockInfo()
info.Operation = "test"
info.Info = "state locker"
lockID, err := s.Lock(info)
if err != nil { if err != nil {
io.WriteString(os.Stderr, err.Error()) io.WriteString(os.Stderr, err.Error())
return return

View File

@ -62,9 +62,8 @@ func (c *UntaintCommand) Run(args []string) int {
} }
if c.Meta.stateLock { if c.Meta.stateLock {
lockInfo := &state.LockInfo{ lockInfo := state.NewLockInfo()
Operation: "untaint", lockInfo.Operation = "untaint"
}
lockID, err := clistate.Lock(st, lockInfo, c.Ui, c.Colorize()) lockID, err := clistate.Lock(st, lockInfo, c.Ui, c.Colorize())
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error locking state: %s", err)) c.Ui.Error(fmt.Sprintf("Error locking state: %s", err))

View File

@ -20,9 +20,8 @@ func TestLocalStateLocks(t *testing.T) {
defer os.Remove(s.Path) defer os.Remove(s.Path)
// lock first // lock first
info := &LockInfo{ info := NewLockInfo()
Operation: "test", info.Operation = "test"
}
lockID, err := s.Lock(info) lockID, err := s.Lock(info)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -8,7 +8,6 @@ import (
"log" "log"
"os" "os"
"strconv" "strconv"
"time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
@ -210,15 +209,16 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
} }
stateName := fmt.Sprintf("%s/%s", c.bucketName, c.keyName) stateName := fmt.Sprintf("%s/%s", c.bucketName, c.keyName)
lockID, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
info.ID = lockID
info.Path = stateName info.Path = stateName
info.Created = time.Now().UTC()
if info.ID == "" {
lockID, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
info.ID = lockID
}
putParams := &dynamodb.PutItemInput{ putParams := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{ Item: map[string]*dynamodb.AttributeValue{
@ -228,7 +228,7 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
TableName: aws.String(c.lockTable), TableName: aws.String(c.lockTable),
ConditionExpression: aws.String("attribute_not_exists(LockID)"), ConditionExpression: aws.String("attribute_not_exists(LockID)"),
} }
_, err = c.dynClient.PutItem(putParams) _, err := c.dynClient.PutItem(putParams)
if err != nil { if err != nil {
getParams := &dynamodb.GetItemInput{ getParams := &dynamodb.GetItemInput{
@ -241,7 +241,7 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
resp, err := c.dynClient.GetItem(getParams) resp, err := c.dynClient.GetItem(getParams)
if err != nil { if err != nil {
return "", fmt.Errorf("s3 state file %q locked, failed to retrieve info: %s", stateName, err) return info.ID, fmt.Errorf("s3 state file %q locked, failed to retrieve info: %s", stateName, err)
} }
var infoData string var infoData string
@ -252,12 +252,12 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
lockInfo := &state.LockInfo{} lockInfo := &state.LockInfo{}
err = json.Unmarshal([]byte(infoData), lockInfo) err = json.Unmarshal([]byte(infoData), lockInfo)
if err != nil { if err != nil {
return "", fmt.Errorf("s3 state file %q locked, failed get lock info: %s", stateName, err) return info.ID, fmt.Errorf("s3 state file %q locked, failed get lock info: %s", stateName, err)
} }
return "", lockInfo.Err() return info.ID, lockInfo.Err()
} }
return "", nil return info.ID, nil
} }
func (c *S3Client) Unlock(string) error { func (c *S3Client) Unlock(string) error {

View File

@ -57,14 +57,13 @@ func TestRemoteLocks(t *testing.T, a, b Client) {
t.Fatal("client B not a state.Locker") t.Fatal("client B not a state.Locker")
} }
infoA := &state.LockInfo{ infoA := state.NewLockInfo()
Operation: "test", infoA.Operation = "test"
Who: "client A", infoA.Who = "clientA"
}
infoB := &state.LockInfo{ infoB := state.NewLockInfo()
Operation: "test", infoB.Operation = "test"
Who: "client B", infoB.Who = "clientB"
}
if _, err := lockerA.Lock(infoA); err != nil { if _, err := lockerA.Lock(infoA); err != nil {
t.Fatal("unable to get initial lock:", err) t.Fatal("unable to get initial lock:", err)

View File

@ -3,12 +3,22 @@ package state
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand"
"os"
"os/user"
"strings" "strings"
"time" "time"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
var rngSource *rand.Rand
func init() {
rngSource = rand.New(rand.NewSource(time.Now().UnixNano()))
}
// State is the collection of all state interfaces. // State is the collection of all state interfaces.
type State interface { type State interface {
StateReader StateReader
@ -59,15 +69,56 @@ type Locker interface {
Unlock(id string) error Unlock(id string) error
} }
// LockInfo stores metadata for locks taken. // Generate a LockInfo structure, populating the required fields.
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, _ := user.Current()
host, _ := os.Hostname()
info := &LockInfo{
ID: id,
Who: fmt.Sprintf("%s@%s", username, host),
Version: terraform.Version,
Created: time.Now().UTC(),
}
return info
}
// LockInfo stores lock metadata.
//
// Only Operation and Info are required to be set by the caller of Lock.
type LockInfo struct { type LockInfo struct {
ID string // unique ID // Unique ID for the lock. NewLockInfo provides a random ID, but this may
Path string // Path to the state file // be overridden by the lock implementation. The final value if ID will be
Created time.Time // The time the lock was taken // returned by the call to Lock.
Version string // Terraform version ID string
Operation string // Terraform operation
Who string // user@hostname when available // Terraform operation, provided by the caller.
Info string // Extra info field 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
} }
// Err returns the lock info formatted in an error // Err returns the lock info formatted in an error

View File

@ -21,3 +21,24 @@ func TestMain(m *testing.M) {
} }
os.Exit(m.Run()) os.Exit(m.Run())
} }
func TestNewLockInfo(t *testing.T) {
info1 := NewLockInfo()
info2 := NewLockInfo()
if info1.ID == "" {
t.Fatal("LockInfo missing ID")
}
if info1.Version == "" {
t.Fatal("LockInfo missing version")
}
if info1.Created.IsZero() {
t.Fatal("LockInfo missing Created")
}
if info1.ID == info2.ID {
t.Fatal("multiple LockInfo with identical IDs")
}
}

View File

@ -19,7 +19,11 @@ func main() {
Path: os.Args[1], Path: os.Args[1],
} }
_, err := s.Lock(&state.LockInfo{Operation: "test", Info: "state locker"}) info := state.NewLockInfo()
info.Operation = "test"
info.Info = "state locker"
_, err := s.Lock(info)
if err != nil { if err != nil {
io.WriteString(os.Stderr, "lock failed") io.WriteString(os.Stderr, "lock failed")