github.com/bradfeehan/terraform@v0.7.0-rc3.0.20170529055808-34b45c5ad841/backend/remote-state/s3/client.go (about) 1 package s3 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "log" 12 "time" 13 14 "github.com/aws/aws-sdk-go/aws" 15 "github.com/aws/aws-sdk-go/aws/awserr" 16 "github.com/aws/aws-sdk-go/service/dynamodb" 17 "github.com/aws/aws-sdk-go/service/s3" 18 multierror "github.com/hashicorp/go-multierror" 19 uuid "github.com/hashicorp/go-uuid" 20 "github.com/hashicorp/terraform/state" 21 "github.com/hashicorp/terraform/state/remote" 22 ) 23 24 // Store the last saved serial in dynamo with this suffix for consistency checks. 25 const stateIDSuffix = "-md5" 26 27 type RemoteClient struct { 28 s3Client *s3.S3 29 dynClient *dynamodb.DynamoDB 30 bucketName string 31 path string 32 serverSideEncryption bool 33 acl string 34 kmsKeyID string 35 lockTable string 36 } 37 38 var ( 39 // The amount of time we will retry a state waiting for it to match the 40 // expected checksum. 41 consistencyRetryTimeout = 10 * time.Second 42 43 // delay when polling the state 44 consistencyRetryPollInterval = 2 * time.Second 45 ) 46 47 // test hook called when checksums don't match 48 var testChecksumHook func() 49 50 func (c *RemoteClient) Get() (payload *remote.Payload, err error) { 51 deadline := time.Now().Add(consistencyRetryTimeout) 52 53 // If we have a checksum, and the returned payload doesn't match, we retry 54 // up until deadline. 55 for { 56 payload, err = c.get() 57 if err != nil { 58 return nil, err 59 } 60 61 // verify that this state is what we expect 62 if expected, err := c.getMD5(); err != nil { 63 log.Printf("[WARNING] failed to fetch state md5: %s", err) 64 } else if len(expected) > 0 && !bytes.Equal(expected, payload.MD5) { 65 log.Printf("[WARNING] state md5 mismatch: expected '%x', got '%x'", expected, payload.MD5) 66 67 if testChecksumHook != nil { 68 testChecksumHook() 69 } 70 71 if time.Now().Before(deadline) { 72 time.Sleep(consistencyRetryPollInterval) 73 log.Println("[INFO] retrying S3 RemoteClient.Get...") 74 continue 75 } 76 77 return nil, fmt.Errorf(errBadChecksumFmt, payload.MD5) 78 } 79 80 break 81 } 82 83 return payload, err 84 } 85 86 func (c *RemoteClient) get() (*remote.Payload, error) { 87 output, err := c.s3Client.GetObject(&s3.GetObjectInput{ 88 Bucket: &c.bucketName, 89 Key: &c.path, 90 }) 91 92 if err != nil { 93 if awserr := err.(awserr.Error); awserr != nil { 94 if awserr.Code() == "NoSuchKey" { 95 return nil, nil 96 } else { 97 return nil, err 98 } 99 } else { 100 return nil, err 101 } 102 } 103 104 defer output.Body.Close() 105 106 buf := bytes.NewBuffer(nil) 107 if _, err := io.Copy(buf, output.Body); err != nil { 108 return nil, fmt.Errorf("Failed to read remote state: %s", err) 109 } 110 111 sum := md5.Sum(buf.Bytes()) 112 payload := &remote.Payload{ 113 Data: buf.Bytes(), 114 MD5: sum[:], 115 } 116 117 // If there was no data, then return nil 118 if len(payload.Data) == 0 { 119 return nil, nil 120 } 121 122 return payload, nil 123 } 124 125 func (c *RemoteClient) Put(data []byte) error { 126 contentType := "application/json" 127 contentLength := int64(len(data)) 128 129 i := &s3.PutObjectInput{ 130 ContentType: &contentType, 131 ContentLength: &contentLength, 132 Body: bytes.NewReader(data), 133 Bucket: &c.bucketName, 134 Key: &c.path, 135 } 136 137 if c.serverSideEncryption { 138 if c.kmsKeyID != "" { 139 i.SSEKMSKeyId = &c.kmsKeyID 140 i.ServerSideEncryption = aws.String("aws:kms") 141 } else { 142 i.ServerSideEncryption = aws.String("AES256") 143 } 144 } 145 146 if c.acl != "" { 147 i.ACL = aws.String(c.acl) 148 } 149 150 log.Printf("[DEBUG] Uploading remote state to S3: %#v", i) 151 152 _, err := c.s3Client.PutObject(i) 153 if err != nil { 154 return fmt.Errorf("Failed to upload state: %v", err) 155 } 156 157 sum := md5.Sum(data) 158 if err := c.putMD5(sum[:]); err != nil { 159 // if this errors out, we unfortunately have to error out altogether, 160 // since the next Get will inevitably fail. 161 return fmt.Errorf("failed to store state MD5: %s", err) 162 163 } 164 165 return nil 166 } 167 168 func (c *RemoteClient) Delete() error { 169 _, err := c.s3Client.DeleteObject(&s3.DeleteObjectInput{ 170 Bucket: &c.bucketName, 171 Key: &c.path, 172 }) 173 174 if err != nil { 175 return err 176 } 177 178 if err := c.deleteMD5(); err != nil { 179 log.Printf("error deleting state md5: %s", err) 180 } 181 182 return nil 183 } 184 185 func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { 186 if c.lockTable == "" { 187 return "", nil 188 } 189 190 info.Path = c.lockPath() 191 192 if info.ID == "" { 193 lockID, err := uuid.GenerateUUID() 194 if err != nil { 195 return "", err 196 } 197 198 info.ID = lockID 199 } 200 201 putParams := &dynamodb.PutItemInput{ 202 Item: map[string]*dynamodb.AttributeValue{ 203 "LockID": {S: aws.String(c.lockPath())}, 204 "Info": {S: aws.String(string(info.Marshal()))}, 205 }, 206 TableName: aws.String(c.lockTable), 207 ConditionExpression: aws.String("attribute_not_exists(LockID)"), 208 } 209 _, err := c.dynClient.PutItem(putParams) 210 211 if err != nil { 212 lockInfo, infoErr := c.getLockInfo() 213 if infoErr != nil { 214 err = multierror.Append(err, infoErr) 215 } 216 217 lockErr := &state.LockError{ 218 Err: err, 219 Info: lockInfo, 220 } 221 return "", lockErr 222 } 223 224 return info.ID, nil 225 } 226 227 func (c *RemoteClient) getMD5() ([]byte, error) { 228 if c.lockTable == "" { 229 return nil, nil 230 } 231 232 getParams := &dynamodb.GetItemInput{ 233 Key: map[string]*dynamodb.AttributeValue{ 234 "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, 235 }, 236 ProjectionExpression: aws.String("LockID, Digest"), 237 TableName: aws.String(c.lockTable), 238 } 239 240 resp, err := c.dynClient.GetItem(getParams) 241 if err != nil { 242 return nil, err 243 } 244 245 var val string 246 if v, ok := resp.Item["Digest"]; ok && v.S != nil { 247 val = *v.S 248 } 249 250 sum, err := hex.DecodeString(val) 251 if err != nil || len(sum) != md5.Size { 252 return nil, errors.New("invalid md5") 253 } 254 255 return sum, nil 256 } 257 258 // store the hash of the state to that clients can check for stale state files. 259 func (c *RemoteClient) putMD5(sum []byte) error { 260 if c.lockTable == "" { 261 return nil 262 } 263 264 if len(sum) != md5.Size { 265 return errors.New("invalid payload md5") 266 } 267 268 putParams := &dynamodb.PutItemInput{ 269 Item: map[string]*dynamodb.AttributeValue{ 270 "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, 271 "Digest": {S: aws.String(hex.EncodeToString(sum))}, 272 }, 273 TableName: aws.String(c.lockTable), 274 } 275 _, err := c.dynClient.PutItem(putParams) 276 if err != nil { 277 log.Printf("[WARNING] failed to record state serial in dynamodb: %s", err) 278 } 279 280 return nil 281 } 282 283 // remove the hash value for a deleted state 284 func (c *RemoteClient) deleteMD5() error { 285 if c.lockTable == "" { 286 return nil 287 } 288 289 params := &dynamodb.DeleteItemInput{ 290 Key: map[string]*dynamodb.AttributeValue{ 291 "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, 292 }, 293 TableName: aws.String(c.lockTable), 294 } 295 if _, err := c.dynClient.DeleteItem(params); err != nil { 296 return err 297 } 298 return nil 299 } 300 301 func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) { 302 getParams := &dynamodb.GetItemInput{ 303 Key: map[string]*dynamodb.AttributeValue{ 304 "LockID": {S: aws.String(c.lockPath())}, 305 }, 306 ProjectionExpression: aws.String("LockID, Info"), 307 TableName: aws.String(c.lockTable), 308 } 309 310 resp, err := c.dynClient.GetItem(getParams) 311 if err != nil { 312 return nil, err 313 } 314 315 var infoData string 316 if v, ok := resp.Item["Info"]; ok && v.S != nil { 317 infoData = *v.S 318 } 319 320 lockInfo := &state.LockInfo{} 321 err = json.Unmarshal([]byte(infoData), lockInfo) 322 if err != nil { 323 return nil, err 324 } 325 326 return lockInfo, nil 327 } 328 329 func (c *RemoteClient) Unlock(id string) error { 330 if c.lockTable == "" { 331 return nil 332 } 333 334 lockErr := &state.LockError{} 335 336 // TODO: store the path and lock ID in separate fields, and have proper 337 // projection expression only delete the lock if both match, rather than 338 // checking the ID from the info field first. 339 lockInfo, err := c.getLockInfo() 340 if err != nil { 341 lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) 342 return lockErr 343 } 344 lockErr.Info = lockInfo 345 346 if lockInfo.ID != id { 347 lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) 348 return lockErr 349 } 350 351 params := &dynamodb.DeleteItemInput{ 352 Key: map[string]*dynamodb.AttributeValue{ 353 "LockID": {S: aws.String(c.lockPath())}, 354 }, 355 TableName: aws.String(c.lockTable), 356 } 357 _, err = c.dynClient.DeleteItem(params) 358 359 if err != nil { 360 lockErr.Err = err 361 return lockErr 362 } 363 return nil 364 } 365 366 func (c *RemoteClient) lockPath() string { 367 return fmt.Sprintf("%s/%s", c.bucketName, c.path) 368 } 369 370 const errBadChecksumFmt = `state data in S3 does not have the expected content. 371 372 This may be caused by unusually long delays in S3 processing a previous state 373 update. Please wait for a minute or two and try again. If this problem 374 persists, and neither S3 nor DynamoDB are experiencing an outage, you may need 375 to manually verify the remote state and update the Digest value stored in the 376 DynamoDB table to the following value: %x 377 `