github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/backend/remote-state/consul/client.go (about)

     1  package consul
     2  
     3  import (
     4  	"crypto/md5"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"time"
     9  
    10  	consulapi "github.com/hashicorp/consul/api"
    11  	multierror "github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/terraform/state"
    13  	"github.com/hashicorp/terraform/state/remote"
    14  )
    15  
    16  const (
    17  	lockSuffix     = "/.lock"
    18  	lockInfoSuffix = "/.lockinfo"
    19  )
    20  
    21  // RemoteClient is a remote client that stores data in Consul.
    22  type RemoteClient struct {
    23  	Client *consulapi.Client
    24  	Path   string
    25  
    26  	consulLock *consulapi.Lock
    27  	lockCh     <-chan struct{}
    28  }
    29  
    30  func (c *RemoteClient) Get() (*remote.Payload, error) {
    31  	pair, _, err := c.Client.KV().Get(c.Path, nil)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	if pair == nil {
    36  		return nil, nil
    37  	}
    38  
    39  	md5 := md5.Sum(pair.Value)
    40  	return &remote.Payload{
    41  		Data: pair.Value,
    42  		MD5:  md5[:],
    43  	}, nil
    44  }
    45  
    46  func (c *RemoteClient) Put(data []byte) error {
    47  	kv := c.Client.KV()
    48  	_, err := kv.Put(&consulapi.KVPair{
    49  		Key:   c.Path,
    50  		Value: data,
    51  	}, nil)
    52  	return err
    53  }
    54  
    55  func (c *RemoteClient) Delete() error {
    56  	kv := c.Client.KV()
    57  	_, err := kv.Delete(c.Path, nil)
    58  	return err
    59  }
    60  
    61  func (c *RemoteClient) putLockInfo(info *state.LockInfo) error {
    62  	info.Path = c.Path
    63  	info.Created = time.Now().UTC()
    64  
    65  	kv := c.Client.KV()
    66  	_, err := kv.Put(&consulapi.KVPair{
    67  		Key:   c.Path + lockInfoSuffix,
    68  		Value: info.Marshal(),
    69  	}, nil)
    70  
    71  	return err
    72  }
    73  
    74  func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
    75  	path := c.Path + lockInfoSuffix
    76  	pair, _, err := c.Client.KV().Get(path, nil)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	if pair == nil {
    81  		return nil, nil
    82  	}
    83  
    84  	li := &state.LockInfo{}
    85  	err = json.Unmarshal(pair.Value, li)
    86  	if err != nil {
    87  		return nil, fmt.Errorf("error unmarshaling lock info: %s", err)
    88  	}
    89  
    90  	return li, nil
    91  }
    92  
    93  func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
    94  	select {
    95  	case <-c.lockCh:
    96  		// We had a lock, but lost it.
    97  		// Since we typically only call lock once, we shouldn't ever see this.
    98  		return "", errors.New("lost consul lock")
    99  	default:
   100  		if c.lockCh != nil {
   101  			// we have an active lock already
   102  			return "", nil
   103  		}
   104  	}
   105  
   106  	if c.consulLock == nil {
   107  		opts := &consulapi.LockOptions{
   108  			Key: c.Path + lockSuffix,
   109  			// We currently don't procide any options to block terraform and
   110  			// retry lock acquisition, but we can wait briefly in case the
   111  			// lock is about to be freed.
   112  			LockWaitTime: time.Second,
   113  			LockTryOnce:  true,
   114  		}
   115  
   116  		lock, err := c.Client.LockOpts(opts)
   117  		if err != nil {
   118  			return "", err
   119  		}
   120  
   121  		c.consulLock = lock
   122  	}
   123  
   124  	lockErr := &state.LockError{}
   125  
   126  	lockCh, err := c.consulLock.Lock(make(chan struct{}))
   127  	if err != nil {
   128  		lockErr.Err = err
   129  		return "", lockErr
   130  	}
   131  
   132  	if lockCh == nil {
   133  		lockInfo, e := c.getLockInfo()
   134  		if e != nil {
   135  			lockErr.Err = e
   136  			return "", lockErr
   137  		}
   138  
   139  		lockErr.Info = lockInfo
   140  		return "", lockErr
   141  	}
   142  
   143  	c.lockCh = lockCh
   144  
   145  	err = c.putLockInfo(info)
   146  	if err != nil {
   147  		if unlockErr := c.Unlock(info.ID); unlockErr != nil {
   148  			err = multierror.Append(err, unlockErr)
   149  		}
   150  
   151  		return "", err
   152  	}
   153  
   154  	return info.ID, nil
   155  }
   156  
   157  func (c *RemoteClient) Unlock(id string) error {
   158  	// this doesn't use the lock id, because the lock is tied to the consul client.
   159  	if c.consulLock == nil || c.lockCh == nil {
   160  		return nil
   161  	}
   162  
   163  	select {
   164  	case <-c.lockCh:
   165  		return errors.New("consul lock was lost")
   166  	default:
   167  	}
   168  
   169  	err := c.consulLock.Unlock()
   170  	c.lockCh = nil
   171  
   172  	kv := c.Client.KV()
   173  	_, delErr := kv.Delete(c.Path+lockInfoSuffix, nil)
   174  	if delErr != nil {
   175  		err = multierror.Append(err, delErr)
   176  	}
   177  
   178  	return err
   179  }