github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 }