github.com/bradfeehan/terraform@v0.7.0-rc3.0.20170529055808-34b45c5ad841/builtin/providers/aws/resource_aws_s3_bucket_object.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "log" 8 "net/url" 9 "os" 10 "sort" 11 "strings" 12 13 "github.com/hashicorp/terraform/helper/schema" 14 "github.com/mitchellh/go-homedir" 15 16 "github.com/aws/aws-sdk-go/aws" 17 "github.com/aws/aws-sdk-go/aws/awserr" 18 "github.com/aws/aws-sdk-go/service/kms" 19 "github.com/aws/aws-sdk-go/service/s3" 20 ) 21 22 func resourceAwsS3BucketObject() *schema.Resource { 23 return &schema.Resource{ 24 Create: resourceAwsS3BucketObjectPut, 25 Read: resourceAwsS3BucketObjectRead, 26 Update: resourceAwsS3BucketObjectPut, 27 Delete: resourceAwsS3BucketObjectDelete, 28 29 Schema: map[string]*schema.Schema{ 30 "bucket": { 31 Type: schema.TypeString, 32 Required: true, 33 ForceNew: true, 34 }, 35 36 "acl": { 37 Type: schema.TypeString, 38 Default: "private", 39 Optional: true, 40 ValidateFunc: validateS3BucketObjectAclType, 41 }, 42 43 "cache_control": { 44 Type: schema.TypeString, 45 Optional: true, 46 }, 47 48 "content_disposition": { 49 Type: schema.TypeString, 50 Optional: true, 51 }, 52 53 "content_encoding": { 54 Type: schema.TypeString, 55 Optional: true, 56 }, 57 58 "content_language": { 59 Type: schema.TypeString, 60 Optional: true, 61 }, 62 63 "content_type": { 64 Type: schema.TypeString, 65 Optional: true, 66 Computed: true, 67 }, 68 69 "key": { 70 Type: schema.TypeString, 71 Required: true, 72 ForceNew: true, 73 }, 74 75 "source": { 76 Type: schema.TypeString, 77 Optional: true, 78 ConflictsWith: []string{"content"}, 79 }, 80 81 "content": { 82 Type: schema.TypeString, 83 Optional: true, 84 ConflictsWith: []string{"source"}, 85 }, 86 87 "storage_class": { 88 Type: schema.TypeString, 89 Optional: true, 90 Computed: true, 91 ValidateFunc: validateS3BucketObjectStorageClassType, 92 }, 93 94 "server_side_encryption": { 95 Type: schema.TypeString, 96 Optional: true, 97 ValidateFunc: validateS3BucketObjectServerSideEncryption, 98 Computed: true, 99 }, 100 101 "kms_key_id": { 102 Type: schema.TypeString, 103 Optional: true, 104 ValidateFunc: validateArn, 105 }, 106 107 "etag": { 108 Type: schema.TypeString, 109 // This will conflict with SSE-C and SSE-KMS encryption and multi-part upload 110 // if/when it's actually implemented. The Etag then won't match raw-file MD5. 111 // See http://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html 112 Optional: true, 113 Computed: true, 114 ConflictsWith: []string{"kms_key_id", "server_side_encryption"}, 115 }, 116 117 "version_id": { 118 Type: schema.TypeString, 119 Computed: true, 120 }, 121 122 "tags": tagsSchema(), 123 }, 124 } 125 } 126 127 func resourceAwsS3BucketObjectPut(d *schema.ResourceData, meta interface{}) error { 128 s3conn := meta.(*AWSClient).s3conn 129 130 restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud() 131 132 var body io.ReadSeeker 133 134 if v, ok := d.GetOk("source"); ok { 135 source := v.(string) 136 path, err := homedir.Expand(source) 137 if err != nil { 138 return fmt.Errorf("Error expanding homedir in source (%s): %s", source, err) 139 } 140 file, err := os.Open(path) 141 if err != nil { 142 return fmt.Errorf("Error opening S3 bucket object source (%s): %s", source, err) 143 } 144 145 body = file 146 } else if v, ok := d.GetOk("content"); ok { 147 content := v.(string) 148 body = bytes.NewReader([]byte(content)) 149 } else { 150 return fmt.Errorf("Must specify \"source\" or \"content\" field") 151 } 152 153 bucket := d.Get("bucket").(string) 154 key := d.Get("key").(string) 155 156 putInput := &s3.PutObjectInput{ 157 Bucket: aws.String(bucket), 158 Key: aws.String(key), 159 ACL: aws.String(d.Get("acl").(string)), 160 Body: body, 161 } 162 163 if v, ok := d.GetOk("storage_class"); ok { 164 putInput.StorageClass = aws.String(v.(string)) 165 } 166 167 if v, ok := d.GetOk("cache_control"); ok { 168 putInput.CacheControl = aws.String(v.(string)) 169 } 170 171 if v, ok := d.GetOk("content_type"); ok { 172 putInput.ContentType = aws.String(v.(string)) 173 } 174 175 if v, ok := d.GetOk("content_encoding"); ok { 176 putInput.ContentEncoding = aws.String(v.(string)) 177 } 178 179 if v, ok := d.GetOk("content_language"); ok { 180 putInput.ContentLanguage = aws.String(v.(string)) 181 } 182 183 if v, ok := d.GetOk("content_disposition"); ok { 184 putInput.ContentDisposition = aws.String(v.(string)) 185 } 186 187 if v, ok := d.GetOk("server_side_encryption"); ok { 188 putInput.ServerSideEncryption = aws.String(v.(string)) 189 } 190 191 if v, ok := d.GetOk("kms_key_id"); ok { 192 putInput.SSEKMSKeyId = aws.String(v.(string)) 193 putInput.ServerSideEncryption = aws.String(s3.ServerSideEncryptionAwsKms) 194 } 195 196 if v, ok := d.GetOk("tags"); ok { 197 if restricted { 198 return fmt.Errorf("This region does not allow for tags on S3 objects") 199 } 200 201 // The tag-set must be encoded as URL Query parameters. 202 values := url.Values{} 203 for k, v := range v.(map[string]interface{}) { 204 values.Add(k, v.(string)) 205 } 206 putInput.Tagging = aws.String(values.Encode()) 207 } 208 209 resp, err := s3conn.PutObject(putInput) 210 if err != nil { 211 return fmt.Errorf("Error putting object in S3 bucket (%s): %s", bucket, err) 212 } 213 214 // See https://forums.aws.amazon.com/thread.jspa?threadID=44003 215 d.Set("etag", strings.Trim(*resp.ETag, `"`)) 216 217 d.Set("version_id", resp.VersionId) 218 d.SetId(key) 219 return resourceAwsS3BucketObjectRead(d, meta) 220 } 221 222 func resourceAwsS3BucketObjectRead(d *schema.ResourceData, meta interface{}) error { 223 s3conn := meta.(*AWSClient).s3conn 224 225 restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud() 226 227 bucket := d.Get("bucket").(string) 228 key := d.Get("key").(string) 229 230 resp, err := s3conn.HeadObject( 231 &s3.HeadObjectInput{ 232 Bucket: aws.String(bucket), 233 Key: aws.String(key), 234 }) 235 236 if err != nil { 237 // If S3 returns a 404 Request Failure, mark the object as destroyed 238 if awsErr, ok := err.(awserr.RequestFailure); ok && awsErr.StatusCode() == 404 { 239 d.SetId("") 240 log.Printf("[WARN] Error Reading Object (%s), object not found (HTTP status 404)", key) 241 return nil 242 } 243 return err 244 } 245 log.Printf("[DEBUG] Reading S3 Bucket Object meta: %s", resp) 246 247 d.Set("cache_control", resp.CacheControl) 248 d.Set("content_disposition", resp.ContentDisposition) 249 d.Set("content_encoding", resp.ContentEncoding) 250 d.Set("content_language", resp.ContentLanguage) 251 d.Set("content_type", resp.ContentType) 252 d.Set("version_id", resp.VersionId) 253 d.Set("server_side_encryption", resp.ServerSideEncryption) 254 255 // Only set non-default KMS key ID (one that doesn't match default) 256 if resp.SSEKMSKeyId != nil { 257 // retrieve S3 KMS Default Master Key 258 kmsconn := meta.(*AWSClient).kmsconn 259 kmsresp, err := kmsconn.DescribeKey(&kms.DescribeKeyInput{ 260 KeyId: aws.String("alias/aws/s3"), 261 }) 262 if err != nil { 263 return fmt.Errorf("Failed to describe default S3 KMS key (alias/aws/s3): %s", err) 264 } 265 266 if *resp.SSEKMSKeyId != *kmsresp.KeyMetadata.Arn { 267 log.Printf("[DEBUG] S3 object is encrypted using a non-default KMS Key ID: %s", *resp.SSEKMSKeyId) 268 d.Set("kms_key_id", resp.SSEKMSKeyId) 269 } 270 } 271 d.Set("etag", strings.Trim(*resp.ETag, `"`)) 272 273 // The "STANDARD" (which is also the default) storage 274 // class when set would not be included in the results. 275 d.Set("storage_class", s3.StorageClassStandard) 276 if resp.StorageClass != nil { 277 d.Set("storage_class", resp.StorageClass) 278 } 279 280 if !restricted { 281 tagResp, err := s3conn.GetObjectTagging( 282 &s3.GetObjectTaggingInput{ 283 Bucket: aws.String(bucket), 284 Key: aws.String(key), 285 }) 286 if err != nil { 287 return fmt.Errorf("Failed to get object tags (bucket: %s, key: %s): %s", bucket, key, err) 288 } 289 d.Set("tags", tagsToMapS3(tagResp.TagSet)) 290 } 291 292 return nil 293 } 294 295 func resourceAwsS3BucketObjectDelete(d *schema.ResourceData, meta interface{}) error { 296 s3conn := meta.(*AWSClient).s3conn 297 298 bucket := d.Get("bucket").(string) 299 key := d.Get("key").(string) 300 301 if _, ok := d.GetOk("version_id"); ok { 302 // Bucket is versioned, we need to delete all versions 303 vInput := s3.ListObjectVersionsInput{ 304 Bucket: aws.String(bucket), 305 Prefix: aws.String(key), 306 } 307 out, err := s3conn.ListObjectVersions(&vInput) 308 if err != nil { 309 return fmt.Errorf("Failed listing S3 object versions: %s", err) 310 } 311 312 for _, v := range out.Versions { 313 input := s3.DeleteObjectInput{ 314 Bucket: aws.String(bucket), 315 Key: aws.String(key), 316 VersionId: v.VersionId, 317 } 318 _, err := s3conn.DeleteObject(&input) 319 if err != nil { 320 return fmt.Errorf("Error deleting S3 object version of %s:\n %s:\n %s", 321 key, v, err) 322 } 323 } 324 } else { 325 // Just delete the object 326 input := s3.DeleteObjectInput{ 327 Bucket: aws.String(bucket), 328 Key: aws.String(key), 329 } 330 _, err := s3conn.DeleteObject(&input) 331 if err != nil { 332 return fmt.Errorf("Error deleting S3 bucket object: %s Bucket: %q Object: %q", err, bucket, key) 333 } 334 } 335 336 return nil 337 } 338 339 func validateS3BucketObjectAclType(v interface{}, k string) (ws []string, errors []error) { 340 value := v.(string) 341 342 cannedAcls := map[string]bool{ 343 s3.ObjectCannedACLPrivate: true, 344 s3.ObjectCannedACLPublicRead: true, 345 s3.ObjectCannedACLPublicReadWrite: true, 346 s3.ObjectCannedACLAuthenticatedRead: true, 347 s3.ObjectCannedACLAwsExecRead: true, 348 s3.ObjectCannedACLBucketOwnerRead: true, 349 s3.ObjectCannedACLBucketOwnerFullControl: true, 350 } 351 352 sentenceJoin := func(m map[string]bool) string { 353 keys := make([]string, 0, len(m)) 354 for k := range m { 355 keys = append(keys, fmt.Sprintf("%q", k)) 356 } 357 sort.Strings(keys) 358 359 length := len(keys) 360 words := make([]string, length) 361 copy(words, keys) 362 363 words[length-1] = fmt.Sprintf("or %s", words[length-1]) 364 return strings.Join(words, ", ") 365 } 366 367 if _, ok := cannedAcls[value]; !ok { 368 errors = append(errors, fmt.Errorf( 369 "%q contains an invalid canned ACL type %q. Valid types are either %s", 370 k, value, sentenceJoin(cannedAcls))) 371 } 372 return 373 } 374 375 func validateS3BucketObjectStorageClassType(v interface{}, k string) (ws []string, errors []error) { 376 value := v.(string) 377 378 storageClass := map[string]bool{ 379 s3.StorageClassStandard: true, 380 s3.StorageClassReducedRedundancy: true, 381 s3.StorageClassStandardIa: true, 382 } 383 384 if _, ok := storageClass[value]; !ok { 385 errors = append(errors, fmt.Errorf( 386 "%q contains an invalid Storage Class type %q. Valid types are either %q, %q, or %q", 387 k, value, s3.StorageClassStandard, s3.StorageClassReducedRedundancy, 388 s3.StorageClassStandardIa)) 389 } 390 return 391 } 392 393 func validateS3BucketObjectServerSideEncryption(v interface{}, k string) (ws []string, errors []error) { 394 value := v.(string) 395 396 serverSideEncryption := map[string]bool{ 397 s3.ServerSideEncryptionAes256: true, 398 s3.ServerSideEncryptionAwsKms: true, 399 } 400 401 if _, ok := serverSideEncryption[value]; !ok { 402 errors = append(errors, fmt.Errorf( 403 "%q contains an invalid Server Side Encryption value %q. Valid values are %q and %q", 404 k, value, s3.ServerSideEncryptionAes256, s3.ServerSideEncryptionAwsKms)) 405 } 406 return 407 }