mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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.
180 lines
3.4 KiB
Go
180 lines
3.4 KiB
Go
package consul
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
consulapi "github.com/hashicorp/consul/api"
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/terraform/state"
|
|
"github.com/hashicorp/terraform/state/remote"
|
|
)
|
|
|
|
const (
|
|
lockSuffix = "/.lock"
|
|
lockInfoSuffix = "/.lockinfo"
|
|
)
|
|
|
|
// RemoteClient is a remote client that stores data in Consul.
|
|
type RemoteClient struct {
|
|
Client *consulapi.Client
|
|
Path string
|
|
|
|
consulLock *consulapi.Lock
|
|
lockCh <-chan struct{}
|
|
}
|
|
|
|
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
|
pair, _, err := c.Client.KV().Get(c.Path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pair == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
md5 := md5.Sum(pair.Value)
|
|
return &remote.Payload{
|
|
Data: pair.Value,
|
|
MD5: md5[:],
|
|
}, nil
|
|
}
|
|
|
|
func (c *RemoteClient) Put(data []byte) error {
|
|
kv := c.Client.KV()
|
|
_, err := kv.Put(&consulapi.KVPair{
|
|
Key: c.Path,
|
|
Value: data,
|
|
}, nil)
|
|
return err
|
|
}
|
|
|
|
func (c *RemoteClient) Delete() error {
|
|
kv := c.Client.KV()
|
|
_, err := kv.Delete(c.Path, nil)
|
|
return err
|
|
}
|
|
|
|
func (c *RemoteClient) putLockInfo(info *state.LockInfo) error {
|
|
info.Path = c.Path
|
|
info.Created = time.Now().UTC()
|
|
|
|
kv := c.Client.KV()
|
|
_, err := kv.Put(&consulapi.KVPair{
|
|
Key: c.Path + lockInfoSuffix,
|
|
Value: info.Marshal(),
|
|
}, nil)
|
|
|
|
return err
|
|
}
|
|
|
|
func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
|
|
path := c.Path + lockInfoSuffix
|
|
pair, _, err := c.Client.KV().Get(path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pair == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
li := &state.LockInfo{}
|
|
err = json.Unmarshal(pair.Value, li)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshaling lock info: %s", err)
|
|
}
|
|
|
|
return li, nil
|
|
}
|
|
|
|
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
|
select {
|
|
case <-c.lockCh:
|
|
// We had a lock, but lost it.
|
|
// Since we typically only call lock once, we shouldn't ever see this.
|
|
return "", errors.New("lost consul lock")
|
|
default:
|
|
if c.lockCh != nil {
|
|
// we have an active lock already
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
if c.consulLock == nil {
|
|
opts := &consulapi.LockOptions{
|
|
Key: c.Path + lockSuffix,
|
|
// We currently don't procide any options to block terraform and
|
|
// retry lock acquisition, but we can wait briefly in case the
|
|
// lock is about to be freed.
|
|
LockWaitTime: time.Second,
|
|
LockTryOnce: true,
|
|
}
|
|
|
|
lock, err := c.Client.LockOpts(opts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
c.consulLock = lock
|
|
}
|
|
|
|
lockErr := &state.LockError{}
|
|
|
|
lockCh, err := c.consulLock.Lock(make(chan struct{}))
|
|
if err != nil {
|
|
lockErr.Err = err
|
|
return "", lockErr
|
|
}
|
|
|
|
if lockCh == nil {
|
|
lockInfo, e := c.getLockInfo()
|
|
if e != nil {
|
|
lockErr.Err = e
|
|
return "", lockErr
|
|
}
|
|
|
|
lockErr.Info = lockInfo
|
|
return "", lockErr
|
|
}
|
|
|
|
c.lockCh = lockCh
|
|
|
|
err = c.putLockInfo(info)
|
|
if err != nil {
|
|
if unlockErr := c.Unlock(info.ID); unlockErr != nil {
|
|
err = multierror.Append(err, unlockErr)
|
|
}
|
|
|
|
return "", err
|
|
}
|
|
|
|
return info.ID, nil
|
|
}
|
|
|
|
func (c *RemoteClient) Unlock(id string) error {
|
|
// this doesn't use the lock id, because the lock is tied to the consul client.
|
|
if c.consulLock == nil || c.lockCh == nil {
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case <-c.lockCh:
|
|
return errors.New("consul lock was lost")
|
|
default:
|
|
}
|
|
|
|
err := c.consulLock.Unlock()
|
|
c.lockCh = nil
|
|
|
|
kv := c.Client.KV()
|
|
_, delErr := kv.Delete(c.Path+lockInfoSuffix, nil)
|
|
if delErr != nil {
|
|
err = multierror.Append(err, delErr)
|
|
}
|
|
|
|
return err
|
|
}
|