github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/backend/remote-state/consul/client.go (about)

     1  package consul
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"crypto/md5"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	consulapi "github.com/hashicorp/consul/api"
    13  	multierror "github.com/hashicorp/go-multierror"
    14  	"github.com/hashicorp/terraform/state"
    15  	"github.com/hashicorp/terraform/state/remote"
    16  )
    17  
    18  const (
    19  	lockSuffix     = "/.lock"
    20  	lockInfoSuffix = "/.lockinfo"
    21  )
    22  
    23  // RemoteClient is a remote client that stores data in Consul.
    24  type RemoteClient struct {
    25  	Client *consulapi.Client
    26  	Path   string
    27  	GZip   bool
    28  
    29  	consulLock *consulapi.Lock
    30  	lockCh     <-chan struct{}
    31  }
    32  
    33  func (c *RemoteClient) Get() (*remote.Payload, error) {
    34  	pair, _, err := c.Client.KV().Get(c.Path, nil)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	if pair == nil {
    39  		return nil, nil
    40  	}
    41  
    42  	payload := pair.Value
    43  	// If the payload starts with 0x1f, it's gzip, not json
    44  	if len(pair.Value) >= 1 && pair.Value[0] == '\x1f' {
    45  		if data, err := uncompressState(pair.Value); err == nil {
    46  			payload = data
    47  		} else {
    48  			return nil, err
    49  		}
    50  	}
    51  
    52  	md5 := md5.Sum(pair.Value)
    53  	return &remote.Payload{
    54  		Data: payload,
    55  		MD5:  md5[:],
    56  	}, nil
    57  }
    58  
    59  func (c *RemoteClient) Put(data []byte) error {
    60  	payload := data
    61  	if c.GZip {
    62  		if compressedState, err := compressState(data); err == nil {
    63  			payload = compressedState
    64  		} else {
    65  			return err
    66  		}
    67  	}
    68  
    69  	kv := c.Client.KV()
    70  	_, err := kv.Put(&consulapi.KVPair{
    71  		Key:   c.Path,
    72  		Value: payload,
    73  	}, nil)
    74  	return err
    75  }
    76  
    77  func (c *RemoteClient) Delete() error {
    78  	kv := c.Client.KV()
    79  	_, err := kv.Delete(c.Path, nil)
    80  	return err
    81  }
    82  
    83  func (c *RemoteClient) putLockInfo(info *state.LockInfo) error {
    84  	info.Path = c.Path
    85  	info.Created = time.Now().UTC()
    86  
    87  	kv := c.Client.KV()
    88  	_, err := kv.Put(&consulapi.KVPair{
    89  		Key:   c.Path + lockInfoSuffix,
    90  		Value: info.Marshal(),
    91  	}, nil)
    92  
    93  	return err
    94  }
    95  
    96  func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
    97  	path := c.Path + lockInfoSuffix
    98  	pair, _, err := c.Client.KV().Get(path, nil)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	if pair == nil {
   103  		return nil, nil
   104  	}
   105  
   106  	li := &state.LockInfo{}
   107  	err = json.Unmarshal(pair.Value, li)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("error unmarshaling lock info: %s", err)
   110  	}
   111  
   112  	return li, nil
   113  }
   114  
   115  func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
   116  	select {
   117  	case <-c.lockCh:
   118  		// We had a lock, but lost it.
   119  		// Since we typically only call lock once, we shouldn't ever see this.
   120  		return "", errors.New("lost consul lock")
   121  	default:
   122  		if c.lockCh != nil {
   123  			// we have an active lock already
   124  			return "", fmt.Errorf("state %q already locked", c.Path)
   125  		}
   126  	}
   127  
   128  	if c.consulLock == nil {
   129  		opts := &consulapi.LockOptions{
   130  			Key: c.Path + lockSuffix,
   131  			// only wait briefly, so terraform has the choice to fail fast or
   132  			// retry as needed.
   133  			LockWaitTime: time.Second,
   134  			LockTryOnce:  true,
   135  		}
   136  
   137  		lock, err := c.Client.LockOpts(opts)
   138  		if err != nil {
   139  			return "", err
   140  		}
   141  
   142  		c.consulLock = lock
   143  	}
   144  
   145  	lockErr := &state.LockError{}
   146  
   147  	lockCh, err := c.consulLock.Lock(make(chan struct{}))
   148  	if err != nil {
   149  		lockErr.Err = err
   150  		return "", lockErr
   151  	}
   152  
   153  	if lockCh == nil {
   154  		lockInfo, e := c.getLockInfo()
   155  		if e != nil {
   156  			lockErr.Err = e
   157  			return "", lockErr
   158  		}
   159  
   160  		lockErr.Info = lockInfo
   161  		return "", lockErr
   162  	}
   163  
   164  	c.lockCh = lockCh
   165  
   166  	err = c.putLockInfo(info)
   167  	if err != nil {
   168  		if unlockErr := c.Unlock(info.ID); unlockErr != nil {
   169  			err = multierror.Append(err, unlockErr)
   170  		}
   171  
   172  		return "", err
   173  	}
   174  
   175  	return info.ID, nil
   176  }
   177  
   178  func (c *RemoteClient) Unlock(id string) error {
   179  	// this doesn't use the lock id, because the lock is tied to the consul client.
   180  	if c.consulLock == nil || c.lockCh == nil {
   181  		return nil
   182  	}
   183  
   184  	select {
   185  	case <-c.lockCh:
   186  		return errors.New("consul lock was lost")
   187  	default:
   188  	}
   189  
   190  	err := c.consulLock.Unlock()
   191  	c.lockCh = nil
   192  
   193  	// This is only cleanup, and will fail if the lock was immediately taken by
   194  	// another client, so we don't report an error to the user here.
   195  	c.consulLock.Destroy()
   196  
   197  	kv := c.Client.KV()
   198  	_, delErr := kv.Delete(c.Path+lockInfoSuffix, nil)
   199  	if delErr != nil {
   200  		err = multierror.Append(err, delErr)
   201  	}
   202  
   203  	return err
   204  }
   205  
   206  func compressState(data []byte) ([]byte, error) {
   207  	b := new(bytes.Buffer)
   208  	gz := gzip.NewWriter(b)
   209  	if _, err := gz.Write(data); err != nil {
   210  		return nil, err
   211  	}
   212  	if err := gz.Flush(); err != nil {
   213  		return nil, err
   214  	}
   215  	if err := gz.Close(); err != nil {
   216  		return nil, err
   217  	}
   218  	return b.Bytes(), nil
   219  }
   220  
   221  func uncompressState(data []byte) ([]byte, error) {
   222  	b := new(bytes.Buffer)
   223  	gz, err := gzip.NewReader(bytes.NewReader(data))
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	b.ReadFrom(gz)
   228  	if err := gz.Close(); err != nil {
   229  		return nil, err
   230  	}
   231  	return b.Bytes(), nil
   232  }