github.com/simonswine/terraform@v0.9.0-beta2/state/remote/s3.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "log" 9 "os" 10 "strconv" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/aws/awserr" 14 "github.com/aws/aws-sdk-go/aws/session" 15 "github.com/aws/aws-sdk-go/service/dynamodb" 16 "github.com/aws/aws-sdk-go/service/s3" 17 "github.com/hashicorp/go-cleanhttp" 18 "github.com/hashicorp/go-multierror" 19 uuid "github.com/hashicorp/go-uuid" 20 terraformAws "github.com/hashicorp/terraform/builtin/providers/aws" 21 "github.com/hashicorp/terraform/state" 22 ) 23 24 func s3Factory(conf map[string]string) (Client, error) { 25 bucketName, ok := conf["bucket"] 26 if !ok { 27 return nil, fmt.Errorf("missing 'bucket' configuration") 28 } 29 30 keyName, ok := conf["key"] 31 if !ok { 32 return nil, fmt.Errorf("missing 'key' configuration") 33 } 34 35 endpoint, ok := conf["endpoint"] 36 if !ok { 37 endpoint = os.Getenv("AWS_S3_ENDPOINT") 38 } 39 40 regionName, ok := conf["region"] 41 if !ok { 42 regionName = os.Getenv("AWS_DEFAULT_REGION") 43 if regionName == "" { 44 return nil, fmt.Errorf( 45 "missing 'region' configuration or AWS_DEFAULT_REGION environment variable") 46 } 47 } 48 49 serverSideEncryption := false 50 if raw, ok := conf["encrypt"]; ok { 51 v, err := strconv.ParseBool(raw) 52 if err != nil { 53 return nil, fmt.Errorf( 54 "'encrypt' field couldn't be parsed as bool: %s", err) 55 } 56 57 serverSideEncryption = v 58 } 59 60 acl := "" 61 if raw, ok := conf["acl"]; ok { 62 acl = raw 63 } 64 kmsKeyID := conf["kms_key_id"] 65 66 var errs []error 67 creds, err := terraformAws.GetCredentials(&terraformAws.Config{ 68 AccessKey: conf["access_key"], 69 SecretKey: conf["secret_key"], 70 Token: conf["token"], 71 Profile: conf["profile"], 72 CredsFilename: conf["shared_credentials_file"], 73 AssumeRoleARN: conf["role_arn"], 74 }) 75 if err != nil { 76 return nil, err 77 } 78 79 // Call Get to check for credential provider. If nothing found, we'll get an 80 // error, and we can present it nicely to the user 81 _, err = creds.Get() 82 if err != nil { 83 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { 84 errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS S3 remote. 85 Please see https://www.terraform.io/docs/state/remote/s3.html for more information on 86 providing credentials for the AWS S3 remote`)) 87 } else { 88 errs = append(errs, fmt.Errorf("Error loading credentials for AWS S3 remote: %s", err)) 89 } 90 return nil, &multierror.Error{Errors: errs} 91 } 92 93 awsConfig := &aws.Config{ 94 Credentials: creds, 95 Endpoint: aws.String(endpoint), 96 Region: aws.String(regionName), 97 HTTPClient: cleanhttp.DefaultClient(), 98 } 99 sess := session.New(awsConfig) 100 nativeClient := s3.New(sess) 101 dynClient := dynamodb.New(sess) 102 103 return &S3Client{ 104 nativeClient: nativeClient, 105 bucketName: bucketName, 106 keyName: keyName, 107 serverSideEncryption: serverSideEncryption, 108 acl: acl, 109 kmsKeyID: kmsKeyID, 110 dynClient: dynClient, 111 lockTable: conf["lock_table"], 112 }, nil 113 } 114 115 type S3Client struct { 116 nativeClient *s3.S3 117 bucketName string 118 keyName string 119 serverSideEncryption bool 120 acl string 121 kmsKeyID string 122 dynClient *dynamodb.DynamoDB 123 lockTable string 124 } 125 126 func (c *S3Client) Get() (*Payload, error) { 127 output, err := c.nativeClient.GetObject(&s3.GetObjectInput{ 128 Bucket: &c.bucketName, 129 Key: &c.keyName, 130 }) 131 132 if err != nil { 133 if awserr := err.(awserr.Error); awserr != nil { 134 if awserr.Code() == "NoSuchKey" { 135 return nil, nil 136 } else { 137 return nil, err 138 } 139 } else { 140 return nil, err 141 } 142 } 143 144 defer output.Body.Close() 145 146 buf := bytes.NewBuffer(nil) 147 if _, err := io.Copy(buf, output.Body); err != nil { 148 return nil, fmt.Errorf("Failed to read remote state: %s", err) 149 } 150 151 payload := &Payload{ 152 Data: buf.Bytes(), 153 } 154 155 // If there was no data, then return nil 156 if len(payload.Data) == 0 { 157 return nil, nil 158 } 159 160 return payload, nil 161 } 162 163 func (c *S3Client) Put(data []byte) error { 164 contentType := "application/json" 165 contentLength := int64(len(data)) 166 167 i := &s3.PutObjectInput{ 168 ContentType: &contentType, 169 ContentLength: &contentLength, 170 Body: bytes.NewReader(data), 171 Bucket: &c.bucketName, 172 Key: &c.keyName, 173 } 174 175 if c.serverSideEncryption { 176 if c.kmsKeyID != "" { 177 i.SSEKMSKeyId = &c.kmsKeyID 178 i.ServerSideEncryption = aws.String("aws:kms") 179 } else { 180 i.ServerSideEncryption = aws.String("AES256") 181 } 182 } 183 184 if c.acl != "" { 185 i.ACL = aws.String(c.acl) 186 } 187 188 log.Printf("[DEBUG] Uploading remote state to S3: %#v", i) 189 190 if _, err := c.nativeClient.PutObject(i); err == nil { 191 return nil 192 } else { 193 return fmt.Errorf("Failed to upload state: %v", err) 194 } 195 } 196 197 func (c *S3Client) Delete() error { 198 _, err := c.nativeClient.DeleteObject(&s3.DeleteObjectInput{ 199 Bucket: &c.bucketName, 200 Key: &c.keyName, 201 }) 202 203 return err 204 } 205 206 func (c *S3Client) Lock(info *state.LockInfo) (string, error) { 207 if c.lockTable == "" { 208 return "", nil 209 } 210 211 stateName := fmt.Sprintf("%s/%s", c.bucketName, c.keyName) 212 info.Path = stateName 213 214 if info.ID == "" { 215 lockID, err := uuid.GenerateUUID() 216 if err != nil { 217 return "", err 218 } 219 220 info.ID = lockID 221 } 222 223 putParams := &dynamodb.PutItemInput{ 224 Item: map[string]*dynamodb.AttributeValue{ 225 "LockID": {S: aws.String(stateName)}, 226 "Info": {S: aws.String(string(info.Marshal()))}, 227 }, 228 TableName: aws.String(c.lockTable), 229 ConditionExpression: aws.String("attribute_not_exists(LockID)"), 230 } 231 _, err := c.dynClient.PutItem(putParams) 232 233 if err != nil { 234 lockInfo, infoErr := c.getLockInfo() 235 if infoErr != nil { 236 err = multierror.Append(err, infoErr) 237 } 238 239 lockErr := &state.LockError{ 240 Err: err, 241 Info: lockInfo, 242 } 243 return "", lockErr 244 } 245 return info.ID, nil 246 } 247 248 func (c *S3Client) getLockInfo() (*state.LockInfo, error) { 249 getParams := &dynamodb.GetItemInput{ 250 Key: map[string]*dynamodb.AttributeValue{ 251 "LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))}, 252 }, 253 ProjectionExpression: aws.String("LockID, Info"), 254 TableName: aws.String(c.lockTable), 255 } 256 257 resp, err := c.dynClient.GetItem(getParams) 258 if err != nil { 259 return nil, err 260 } 261 262 var infoData string 263 if v, ok := resp.Item["Info"]; ok && v.S != nil { 264 infoData = *v.S 265 } 266 267 lockInfo := &state.LockInfo{} 268 err = json.Unmarshal([]byte(infoData), lockInfo) 269 if err != nil { 270 return nil, err 271 } 272 273 return lockInfo, nil 274 } 275 276 func (c *S3Client) Unlock(id string) error { 277 if c.lockTable == "" { 278 return nil 279 } 280 281 lockErr := &state.LockError{} 282 283 // TODO: store the path and lock ID in separate fields, and have proper 284 // projection expression only delete the lock if both match, rather than 285 // checking the ID from the info field first. 286 lockInfo, err := c.getLockInfo() 287 if err != nil { 288 lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) 289 return lockErr 290 } 291 lockErr.Info = lockInfo 292 293 if lockInfo.ID != id { 294 lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) 295 return lockErr 296 } 297 298 params := &dynamodb.DeleteItemInput{ 299 Key: map[string]*dynamodb.AttributeValue{ 300 "LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))}, 301 }, 302 TableName: aws.String(c.lockTable), 303 } 304 _, err = c.dynClient.DeleteItem(params) 305 306 if err != nil { 307 lockErr.Err = err 308 return lockErr 309 } 310 return nil 311 }