github.com/nevins-b/terraform@v0.3.8-0.20170215184714-bbae22007d5a/backend/remote-state/consul/client.go (about)

     1  package consul
     2  
     3  import (
     4  	"crypto/md5"
     5  	"encoding/json"
     6  	"errors"
     7  	"time"
     8  
     9  	consulapi "github.com/hashicorp/consul/api"
    10  	"github.com/hashicorp/errwrap"
    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 string) error {
    62  	li := &state.LockInfo{
    63  		Path:    c.Path,
    64  		Created: time.Now().UTC(),
    65  		Info:    info,
    66  	}
    67  
    68  	js, err := json.Marshal(li)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	kv := c.Client.KV()
    74  	_, err = kv.Put(&consulapi.KVPair{
    75  		Key:   c.Path + lockInfoSuffix,
    76  		Value: js,
    77  	}, nil)
    78  
    79  	return err
    80  }
    81  
    82  func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
    83  	path := c.Path + lockInfoSuffix
    84  	pair, _, err := c.Client.KV().Get(path, nil)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	if pair == nil {
    89  		return nil, nil
    90  	}
    91  
    92  	li := &state.LockInfo{}
    93  	err = json.Unmarshal(pair.Value, li)
    94  	if err != nil {
    95  		return nil, errwrap.Wrapf("error unmarshaling lock info: {{err}}", err)
    96  	}
    97  
    98  	return li, nil
    99  }
   100  
   101  func (c *RemoteClient) Lock(info string) error {
   102  	select {
   103  	case <-c.lockCh:
   104  		// We had a lock, but lost it.
   105  		// Since we typically only call lock once, we shouldn't ever see this.
   106  		return errors.New("lost consul lock")
   107  	default:
   108  		if c.lockCh != nil {
   109  			// we have an active lock already
   110  			return nil
   111  		}
   112  	}
   113  
   114  	if c.consulLock == nil {
   115  		opts := &consulapi.LockOptions{
   116  			Key: c.Path + lockSuffix,
   117  			// We currently don't procide any options to block terraform and
   118  			// retry lock acquisition, but we can wait briefly in case the
   119  			// lock is about to be freed.
   120  			LockWaitTime: time.Second,
   121  			LockTryOnce:  true,
   122  		}
   123  
   124  		lock, err := c.Client.LockOpts(opts)
   125  		if err != nil {
   126  			return nil
   127  		}
   128  
   129  		c.consulLock = lock
   130  	}
   131  
   132  	lockCh, err := c.consulLock.Lock(make(chan struct{}))
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	if lockCh == nil {
   138  		lockInfo, e := c.getLockInfo()
   139  		if e != nil {
   140  			return e
   141  		}
   142  		return lockInfo.Err()
   143  	}
   144  
   145  	c.lockCh = lockCh
   146  
   147  	err = c.putLockInfo(info)
   148  	if err != nil {
   149  		err = multierror.Append(err, c.Unlock())
   150  		return err
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  func (c *RemoteClient) Unlock() error {
   157  	if c.consulLock == nil || c.lockCh == nil {
   158  		return nil
   159  	}
   160  
   161  	select {
   162  	case <-c.lockCh:
   163  		return errors.New("consul lock was lost")
   164  	default:
   165  	}
   166  
   167  	err := c.consulLock.Unlock()
   168  	c.lockCh = nil
   169  
   170  	kv := c.Client.KV()
   171  	_, delErr := kv.Delete(c.Path+lockInfoSuffix, nil)
   172  	if delErr != nil {
   173  		err = multierror.Append(err, delErr)
   174  	}
   175  
   176  	return err
   177  }