github.com/dougneal/terraform@v0.6.15-0.20170330092735-b6a3840768a4/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 stateName := fmt.Sprintf("%s/%s", c.bucketName, c.path) 117 info.Path = stateName 118 119 if info.ID == "" { 120 lockID, err := uuid.GenerateUUID() 121 if err != nil { 122 return "", err 123 } 124 125 info.ID = lockID 126 } 127 128 putParams := &dynamodb.PutItemInput{ 129 Item: map[string]*dynamodb.AttributeValue{ 130 "LockID": {S: aws.String(stateName)}, 131 "Info": {S: aws.String(string(info.Marshal()))}, 132 }, 133 TableName: aws.String(c.lockTable), 134 ConditionExpression: aws.String("attribute_not_exists(LockID)"), 135 } 136 _, err := c.dynClient.PutItem(putParams) 137 138 if err != nil { 139 lockInfo, infoErr := c.getLockInfo() 140 if infoErr != nil { 141 err = multierror.Append(err, infoErr) 142 } 143 144 lockErr := &state.LockError{ 145 Err: err, 146 Info: lockInfo, 147 } 148 return "", lockErr 149 } 150 return info.ID, nil 151 } 152 153 func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) { 154 getParams := &dynamodb.GetItemInput{ 155 Key: map[string]*dynamodb.AttributeValue{ 156 "LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.path))}, 157 }, 158 ProjectionExpression: aws.String("LockID, Info"), 159 TableName: aws.String(c.lockTable), 160 } 161 162 resp, err := c.dynClient.GetItem(getParams) 163 if err != nil { 164 return nil, err 165 } 166 167 var infoData string 168 if v, ok := resp.Item["Info"]; ok && v.S != nil { 169 infoData = *v.S 170 } 171 172 lockInfo := &state.LockInfo{} 173 err = json.Unmarshal([]byte(infoData), lockInfo) 174 if err != nil { 175 return nil, err 176 } 177 178 return lockInfo, nil 179 } 180 181 func (c *RemoteClient) Unlock(id string) error { 182 if c.lockTable == "" { 183 return nil 184 } 185 186 lockErr := &state.LockError{} 187 188 // TODO: store the path and lock ID in separate fields, and have proper 189 // projection expression only delete the lock if both match, rather than 190 // checking the ID from the info field first. 191 lockInfo, err := c.getLockInfo() 192 if err != nil { 193 lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) 194 return lockErr 195 } 196 lockErr.Info = lockInfo 197 198 if lockInfo.ID != id { 199 lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) 200 return lockErr 201 } 202 203 params := &dynamodb.DeleteItemInput{ 204 Key: map[string]*dynamodb.AttributeValue{ 205 "LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.path))}, 206 }, 207 TableName: aws.String(c.lockTable), 208 } 209 _, err = c.dynClient.DeleteItem(params) 210 211 if err != nil { 212 lockErr.Err = err 213 return lockErr 214 } 215 return nil 216 }