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 }