github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/backend/remote-state/s3/client.go (about) 1 package s3 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/dynamodb" 13 "github.com/aws/aws-sdk-go/service/s3" 14 multierror "github.com/hashicorp/go-multierror" 15 uuid "github.com/hashicorp/go-uuid" 16 "github.com/hashicorp/terraform/state" 17 "github.com/hashicorp/terraform/state/remote" 18 ) 19 20 type RemoteClient struct { 21 s3Client *s3.S3 22 dynClient *dynamodb.DynamoDB 23 bucketName string 24 path string 25 serverSideEncryption bool 26 acl string 27 kmsKeyID string 28 lockTable string 29 } 30 31 func (c *RemoteClient) Get() (*remote.Payload, error) { 32 output, err := c.s3Client.GetObject(&s3.GetObjectInput{ 33 Bucket: &c.bucketName, 34 Key: &c.path, 35 }) 36 37 if err != nil { 38 if awserr := err.(awserr.Error); awserr != nil { 39 if awserr.Code() == "NoSuchKey" { 40 return nil, nil 41 } else { 42 return nil, err 43 } 44 } else { 45 return nil, err 46 } 47 } 48 49 defer output.Body.Close() 50 51 buf := bytes.NewBuffer(nil) 52 if _, err := io.Copy(buf, output.Body); err != nil { 53 return nil, fmt.Errorf("Failed to read remote state: %s", err) 54 } 55 56 payload := &remote.Payload{ 57 Data: buf.Bytes(), 58 } 59 60 // If there was no data, then return nil 61 if len(payload.Data) == 0 { 62 return nil, nil 63 } 64 65 return payload, nil 66 } 67 68 func (c *RemoteClient) Put(data []byte) error { 69 contentType := "application/json" 70 contentLength := int64(len(data)) 71 72 i := &s3.PutObjectInput{ 73 ContentType: &contentType, 74 ContentLength: &contentLength, 75 Body: bytes.NewReader(data), 76 Bucket: &c.bucketName, 77 Key: &c.path, 78 } 79 80 if c.serverSideEncryption { 81 if c.kmsKeyID != "" { 82 i.SSEKMSKeyId = &c.kmsKeyID 83 i.ServerSideEncryption = aws.String("aws:kms") 84 } else { 85 i.ServerSideEncryption = aws.String("AES256") 86 } 87 } 88 89 if c.acl != "" { 90 i.ACL = aws.String(c.acl) 91 } 92 93 log.Printf("[DEBUG] Uploading remote state to S3: %#v", i) 94 95 if _, err := c.s3Client.PutObject(i); err == nil { 96 return nil 97 } else { 98 return fmt.Errorf("Failed to upload state: %v", err) 99 } 100 } 101 102 func (c *RemoteClient) Delete() error { 103 _, err := c.s3Client.DeleteObject(&s3.DeleteObjectInput{ 104 Bucket: &c.bucketName, 105 Key: &c.path, 106 }) 107 108 return err 109 } 110 111 func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) { 112 if c.lockTable == "" { 113 return "", nil 114 } 115 116 info.Path = c.lockPath() 117 118 if info.ID == "" { 119 lockID, err := uuid.GenerateUUID() 120 if err != nil { 121 return "", err 122 } 123 124 info.ID = lockID 125 } 126 127 putParams := &dynamodb.PutItemInput{ 128 Item: map[string]*dynamodb.AttributeValue{ 129 "LockID": {S: aws.String(c.lockPath())}, 130 "Info": {S: aws.String(string(info.Marshal()))}, 131 }, 132 TableName: aws.String(c.lockTable), 133 ConditionExpression: aws.String("attribute_not_exists(LockID)"), 134 } 135 _, err := c.dynClient.PutItem(putParams) 136 137 if err != nil { 138 lockInfo, infoErr := c.getLockInfo() 139 if infoErr != nil { 140 err = multierror.Append(err, infoErr) 141 } 142 143 lockErr := &state.LockError{ 144 Err: err, 145 Info: lockInfo, 146 } 147 return "", lockErr 148 } 149 return info.ID, nil 150 } 151 152 func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) { 153 getParams := &dynamodb.GetItemInput{ 154 Key: map[string]*dynamodb.AttributeValue{ 155 "LockID": {S: aws.String(c.lockPath())}, 156 }, 157 ProjectionExpression: aws.String("LockID, Info"), 158 TableName: aws.String(c.lockTable), 159 } 160 161 resp, err := c.dynClient.GetItem(getParams) 162 if err != nil { 163 return nil, err 164 } 165 166 var infoData string 167 if v, ok := resp.Item["Info"]; ok && v.S != nil { 168 infoData = *v.S 169 } 170 171 lockInfo := &state.LockInfo{} 172 err = json.Unmarshal([]byte(infoData), lockInfo) 173 if err != nil { 174 return nil, err 175 } 176 177 return lockInfo, nil 178 } 179 180 func (c *RemoteClient) Unlock(id string) error { 181 if c.lockTable == "" { 182 return nil 183 } 184 185 lockErr := &state.LockError{} 186 187 // TODO: store the path and lock ID in separate fields, and have proper 188 // projection expression only delete the lock if both match, rather than 189 // checking the ID from the info field first. 190 lockInfo, err := c.getLockInfo() 191 if err != nil { 192 lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) 193 return lockErr 194 } 195 lockErr.Info = lockInfo 196 197 if lockInfo.ID != id { 198 lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) 199 return lockErr 200 } 201 202 params := &dynamodb.DeleteItemInput{ 203 Key: map[string]*dynamodb.AttributeValue{ 204 "LockID": {S: aws.String(c.lockPath())}, 205 }, 206 TableName: aws.String(c.lockTable), 207 } 208 _, err = c.dynClient.DeleteItem(params) 209 210 if err != nil { 211 lockErr.Err = err 212 return lockErr 213 } 214 return nil 215 } 216 217 func (c *RemoteClient) lockPath() string { 218 return fmt.Sprintf("%s/%s", c.bucketName, c.path) 219 }