github.com/dougneal/terraform@v0.6.15-0.20170330092735-b6a3840768a4/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 "", nil 125 } 126 } 127 128 if c.consulLock == nil { 129 opts := &consulapi.LockOptions{ 130 Key: c.Path + lockSuffix, 131 // We currently don't procide any options to block terraform and 132 // retry lock acquisition, but we can wait briefly in case the 133 // lock is about to be freed. 134 LockWaitTime: time.Second, 135 LockTryOnce: true, 136 } 137 138 lock, err := c.Client.LockOpts(opts) 139 if err != nil { 140 return "", err 141 } 142 143 c.consulLock = lock 144 } 145 146 lockErr := &state.LockError{} 147 148 lockCh, err := c.consulLock.Lock(make(chan struct{})) 149 if err != nil { 150 lockErr.Err = err 151 return "", lockErr 152 } 153 154 if lockCh == nil { 155 lockInfo, e := c.getLockInfo() 156 if e != nil { 157 lockErr.Err = e 158 return "", lockErr 159 } 160 161 lockErr.Info = lockInfo 162 return "", lockErr 163 } 164 165 c.lockCh = lockCh 166 167 err = c.putLockInfo(info) 168 if err != nil { 169 if unlockErr := c.Unlock(info.ID); unlockErr != nil { 170 err = multierror.Append(err, unlockErr) 171 } 172 173 return "", err 174 } 175 176 return info.ID, nil 177 } 178 179 func (c *RemoteClient) Unlock(id string) error { 180 // this doesn't use the lock id, because the lock is tied to the consul client. 181 if c.consulLock == nil || c.lockCh == nil { 182 return nil 183 } 184 185 select { 186 case <-c.lockCh: 187 return errors.New("consul lock was lost") 188 default: 189 } 190 191 err := c.consulLock.Unlock() 192 c.lockCh = nil 193 194 kv := c.Client.KV() 195 _, delErr := kv.Delete(c.Path+lockInfoSuffix, nil) 196 if delErr != nil { 197 err = multierror.Append(err, delErr) 198 } 199 200 return err 201 } 202 203 func compressState(data []byte) ([]byte, error) { 204 b := new(bytes.Buffer) 205 gz := gzip.NewWriter(b) 206 if _, err := gz.Write(data); err != nil { 207 return nil, err 208 } 209 if err := gz.Flush(); err != nil { 210 return nil, err 211 } 212 if err := gz.Close(); err != nil { 213 return nil, err 214 } 215 return b.Bytes(), nil 216 } 217 218 func uncompressState(data []byte) ([]byte, error) { 219 b := new(bytes.Buffer) 220 gz, err := gzip.NewReader(bytes.NewReader(data)) 221 if err != nil { 222 return nil, err 223 } 224 b.ReadFrom(gz) 225 if err := gz.Close(); err != nil { 226 return nil, err 227 } 228 return b.Bytes(), nil 229 }