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 {
lockInfo := &state.LockInfo{
Info: op.Type.String(),
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = op.Type.String()
_, err := clistate.Lock(s, lockInfo, b.CLI, b.Colorize())
if err != nil {
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
lockInfo := &state.LockInfo{
Operation: "plan",
Info: "backend from plan",
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from plan"
lockID, err := clistate.Lock(realMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
@ -991,9 +989,8 @@ func (m *Meta) backend_C_r_s(
}
// Lock the state if we can
lockInfo := &state.LockInfo{
Info: "backend from config",
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
@ -1100,9 +1097,9 @@ func (m *Meta) backend_C_r_S_changed(
}
// Lock the state if we can
lockInfo := &state.LockInfo{
Info: "backend from config",
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {
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
lockInfo := &state.LockInfo{
Info: "backend from config",
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = "backend from config"
lockID, err := clistate.Lock(sMgr, lockInfo, m.Ui, m.Colorize())
if err != nil {

View File

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

View File

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

View File

@ -23,7 +23,11 @@ func main() {
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 {
io.WriteString(os.Stderr, err.Error())
return

View File

@ -62,9 +62,8 @@ func (c *UntaintCommand) Run(args []string) int {
}
if c.Meta.stateLock {
lockInfo := &state.LockInfo{
Operation: "untaint",
}
lockInfo := state.NewLockInfo()
lockInfo.Operation = "untaint"
lockID, err := clistate.Lock(st, lockInfo, c.Ui, c.Colorize())
if err != nil {
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)
// lock first
info := &LockInfo{
Operation: "test",
}
info := NewLockInfo()
info.Operation = "test"
lockID, err := s.Lock(info)
if err != nil {
t.Fatal(err)

View File

@ -8,7 +8,6 @@ import (
"log"
"os"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"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)
lockID, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
info.ID = lockID
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{
Item: map[string]*dynamodb.AttributeValue{
@ -228,7 +228,7 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
TableName: aws.String(c.lockTable),
ConditionExpression: aws.String("attribute_not_exists(LockID)"),
}
_, err = c.dynClient.PutItem(putParams)
_, err := c.dynClient.PutItem(putParams)
if err != nil {
getParams := &dynamodb.GetItemInput{
@ -241,7 +241,7 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
resp, err := c.dynClient.GetItem(getParams)
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
@ -252,12 +252,12 @@ func (c *S3Client) Lock(info *state.LockInfo) (string, error) {
lockInfo := &state.LockInfo{}
err = json.Unmarshal([]byte(infoData), lockInfo)
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 {

View File

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

View File

@ -3,12 +3,22 @@ package state
import (
"encoding/json"
"fmt"
"math/rand"
"os"
"os/user"
"strings"
"time"
uuid "github.com/hashicorp/go-uuid"
"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.
type State interface {
StateReader
@ -59,15 +69,56 @@ type Locker interface {
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 {
ID string // unique ID
Path string // Path to the state file
Created time.Time // The time the lock was taken
Version string // Terraform version
Operation string // Terraform operation
Who string // user@hostname when available
Info string // Extra info field
// Unique ID for the lock. NewLockInfo provides a random ID, but this may
// be overridden by the lock implementation. The final value if 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
}
// Err returns the lock info formatted in an error

View File

@ -21,3 +21,24 @@ func TestMain(m *testing.M) {
}
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],
}
_, 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 {
io.WriteString(os.Stderr, "lock failed")