github.com/jsoriano/terraform@v0.6.7-0.20151026070445-8b70867fdd95/builtin/providers/aws/resource_aws_s3_bucket.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "log" 8 9 "github.com/hashicorp/terraform/helper/schema" 10 11 "github.com/aws/aws-sdk-go/aws" 12 "github.com/aws/aws-sdk-go/aws/awserr" 13 "github.com/aws/aws-sdk-go/service/s3" 14 "github.com/hashicorp/terraform/helper/hashcode" 15 ) 16 17 func resourceAwsS3Bucket() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsS3BucketCreate, 20 Read: resourceAwsS3BucketRead, 21 Update: resourceAwsS3BucketUpdate, 22 Delete: resourceAwsS3BucketDelete, 23 24 Schema: map[string]*schema.Schema{ 25 "bucket": &schema.Schema{ 26 Type: schema.TypeString, 27 Required: true, 28 ForceNew: true, 29 }, 30 31 "acl": &schema.Schema{ 32 Type: schema.TypeString, 33 Default: "private", 34 Optional: true, 35 ForceNew: true, 36 }, 37 38 "policy": &schema.Schema{ 39 Type: schema.TypeString, 40 Optional: true, 41 StateFunc: normalizeJson, 42 }, 43 44 "website": &schema.Schema{ 45 Type: schema.TypeList, 46 Optional: true, 47 Elem: &schema.Resource{ 48 Schema: map[string]*schema.Schema{ 49 "index_document": &schema.Schema{ 50 Type: schema.TypeString, 51 Optional: true, 52 }, 53 54 "error_document": &schema.Schema{ 55 Type: schema.TypeString, 56 Optional: true, 57 }, 58 59 "redirect_all_requests_to": &schema.Schema{ 60 Type: schema.TypeString, 61 ConflictsWith: []string{ 62 "website.0.index_document", 63 "website.0.error_document", 64 }, 65 Optional: true, 66 }, 67 }, 68 }, 69 }, 70 71 "hosted_zone_id": &schema.Schema{ 72 Type: schema.TypeString, 73 Optional: true, 74 Computed: true, 75 }, 76 77 "region": &schema.Schema{ 78 Type: schema.TypeString, 79 Optional: true, 80 Computed: true, 81 }, 82 "website_endpoint": &schema.Schema{ 83 Type: schema.TypeString, 84 Optional: true, 85 Computed: true, 86 }, 87 "website_domain": &schema.Schema{ 88 Type: schema.TypeString, 89 Optional: true, 90 Computed: true, 91 }, 92 93 "versioning": &schema.Schema{ 94 Type: schema.TypeSet, 95 Optional: true, 96 Elem: &schema.Resource{ 97 Schema: map[string]*schema.Schema{ 98 "enabled": &schema.Schema{ 99 Type: schema.TypeBool, 100 Optional: true, 101 Default: false, 102 }, 103 }, 104 }, 105 Set: func(v interface{}) int { 106 var buf bytes.Buffer 107 m := v.(map[string]interface{}) 108 buf.WriteString(fmt.Sprintf("%t-", m["enabled"].(bool))) 109 110 return hashcode.String(buf.String()) 111 }, 112 }, 113 114 "tags": tagsSchema(), 115 116 "force_destroy": &schema.Schema{ 117 Type: schema.TypeBool, 118 Optional: true, 119 Default: false, 120 }, 121 }, 122 } 123 } 124 125 func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error { 126 s3conn := meta.(*AWSClient).s3conn 127 awsRegion := meta.(*AWSClient).region 128 129 // Get the bucket and acl 130 bucket := d.Get("bucket").(string) 131 acl := d.Get("acl").(string) 132 133 log.Printf("[DEBUG] S3 bucket create: %s, ACL: %s", bucket, acl) 134 135 req := &s3.CreateBucketInput{ 136 Bucket: aws.String(bucket), 137 ACL: aws.String(acl), 138 } 139 140 // Special case us-east-1 region and do not set the LocationConstraint. 141 // See "Request Elements: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html 142 if awsRegion != "us-east-1" { 143 req.CreateBucketConfiguration = &s3.CreateBucketConfiguration{ 144 LocationConstraint: aws.String(awsRegion), 145 } 146 } 147 148 _, err := s3conn.CreateBucket(req) 149 if err != nil { 150 return fmt.Errorf("Error creating S3 bucket: %s", err) 151 } 152 153 // Assign the bucket name as the resource ID 154 d.SetId(bucket) 155 156 return resourceAwsS3BucketUpdate(d, meta) 157 } 158 159 func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { 160 s3conn := meta.(*AWSClient).s3conn 161 if err := setTagsS3(s3conn, d); err != nil { 162 return err 163 } 164 165 if d.HasChange("policy") { 166 if err := resourceAwsS3BucketPolicyUpdate(s3conn, d); err != nil { 167 return err 168 } 169 } 170 171 if d.HasChange("website") { 172 if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { 173 return err 174 } 175 } 176 177 if d.HasChange("versioning") { 178 if err := resourceAwsS3BucketVersioningUpdate(s3conn, d); err != nil { 179 return err 180 } 181 } 182 183 return resourceAwsS3BucketRead(d, meta) 184 } 185 186 func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { 187 s3conn := meta.(*AWSClient).s3conn 188 189 var err error 190 _, err = s3conn.HeadBucket(&s3.HeadBucketInput{ 191 Bucket: aws.String(d.Id()), 192 }) 193 if err != nil { 194 if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() == 404 { 195 log.Printf("[WARN] S3 Bucket (%s) not found, error code (404)", d.Id()) 196 d.SetId("") 197 return nil 198 } else { 199 // some of the AWS SDK's errors can be empty strings, so let's add 200 // some additional context. 201 return fmt.Errorf("error reading S3 bucket \"%s\": %s", d.Id(), err) 202 } 203 } 204 205 // Read the policy 206 pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ 207 Bucket: aws.String(d.Id()), 208 }) 209 log.Printf("[DEBUG] S3 bucket: %s, read policy: %v", d.Id(), pol) 210 if err != nil { 211 if err := d.Set("policy", ""); err != nil { 212 return err 213 } 214 } else { 215 if v := pol.Policy; v == nil { 216 if err := d.Set("policy", ""); err != nil { 217 return err 218 } 219 } else if err := d.Set("policy", normalizeJson(*v)); err != nil { 220 return err 221 } 222 } 223 224 // Read the website configuration 225 ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ 226 Bucket: aws.String(d.Id()), 227 }) 228 var websites []map[string]interface{} 229 if err == nil { 230 w := make(map[string]interface{}) 231 232 if v := ws.IndexDocument; v != nil { 233 w["index_document"] = *v.Suffix 234 } 235 236 if v := ws.ErrorDocument; v != nil { 237 w["error_document"] = *v.Key 238 } 239 240 if v := ws.RedirectAllRequestsTo; v != nil { 241 w["redirect_all_requests_to"] = *v.HostName 242 } 243 244 websites = append(websites, w) 245 } 246 if err := d.Set("website", websites); err != nil { 247 return err 248 } 249 250 // Read the versioning configuration 251 versioning, err := s3conn.GetBucketVersioning(&s3.GetBucketVersioningInput{ 252 Bucket: aws.String(d.Id()), 253 }) 254 if err != nil { 255 return err 256 } 257 log.Printf("[DEBUG] S3 Bucket: %s, versioning: %v", d.Id(), versioning) 258 if versioning.Status != nil && *versioning.Status == s3.BucketVersioningStatusEnabled { 259 vcl := make([]map[string]interface{}, 0, 1) 260 vc := make(map[string]interface{}) 261 if *versioning.Status == s3.BucketVersioningStatusEnabled { 262 vc["enabled"] = true 263 } else { 264 vc["enabled"] = false 265 } 266 vcl = append(vcl, vc) 267 if err := d.Set("versioning", vcl); err != nil { 268 return err 269 } 270 } 271 272 // Add the region as an attribute 273 location, err := s3conn.GetBucketLocation( 274 &s3.GetBucketLocationInput{ 275 Bucket: aws.String(d.Id()), 276 }, 277 ) 278 if err != nil { 279 return err 280 } 281 var region string 282 if location.LocationConstraint != nil { 283 region = *location.LocationConstraint 284 } 285 region = normalizeRegion(region) 286 if err := d.Set("region", region); err != nil { 287 return err 288 } 289 290 // Add the hosted zone ID for this bucket's region as an attribute 291 hostedZoneID := HostedZoneIDForRegion(region) 292 if err := d.Set("hosted_zone_id", hostedZoneID); err != nil { 293 return err 294 } 295 296 // Add website_endpoint as an attribute 297 websiteEndpoint, err := websiteEndpoint(s3conn, d) 298 if err != nil { 299 return err 300 } 301 if websiteEndpoint != nil { 302 if err := d.Set("website_endpoint", websiteEndpoint.Endpoint); err != nil { 303 return err 304 } 305 if err := d.Set("website_domain", websiteEndpoint.Domain); err != nil { 306 return err 307 } 308 } 309 310 tagSet, err := getTagSetS3(s3conn, d.Id()) 311 if err != nil { 312 return err 313 } 314 315 if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil { 316 return err 317 } 318 319 return nil 320 } 321 322 func resourceAwsS3BucketDelete(d *schema.ResourceData, meta interface{}) error { 323 s3conn := meta.(*AWSClient).s3conn 324 325 log.Printf("[DEBUG] S3 Delete Bucket: %s", d.Id()) 326 _, err := s3conn.DeleteBucket(&s3.DeleteBucketInput{ 327 Bucket: aws.String(d.Id()), 328 }) 329 if err != nil { 330 ec2err, ok := err.(awserr.Error) 331 if ok && ec2err.Code() == "BucketNotEmpty" { 332 if d.Get("force_destroy").(bool) { 333 // bucket may have things delete them 334 log.Printf("[DEBUG] S3 Bucket attempting to forceDestroy %+v", err) 335 336 bucket := d.Get("bucket").(string) 337 resp, err := s3conn.ListObjects( 338 &s3.ListObjectsInput{ 339 Bucket: aws.String(bucket), 340 }, 341 ) 342 343 if err != nil { 344 return fmt.Errorf("Error S3 Bucket list Objects err: %s", err) 345 } 346 347 objectsToDelete := make([]*s3.ObjectIdentifier, len(resp.Contents)) 348 for i, v := range resp.Contents { 349 objectsToDelete[i] = &s3.ObjectIdentifier{ 350 Key: v.Key, 351 } 352 } 353 _, err = s3conn.DeleteObjects( 354 &s3.DeleteObjectsInput{ 355 Bucket: aws.String(bucket), 356 Delete: &s3.Delete{ 357 Objects: objectsToDelete, 358 }, 359 }, 360 ) 361 if err != nil { 362 return fmt.Errorf("Error S3 Bucket force_destroy error deleting: %s", err) 363 } 364 365 // this line recurses until all objects are deleted or an error is returned 366 return resourceAwsS3BucketDelete(d, meta) 367 } 368 } 369 return fmt.Errorf("Error deleting S3 Bucket: %s", err) 370 } 371 return nil 372 } 373 374 func resourceAwsS3BucketPolicyUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 375 bucket := d.Get("bucket").(string) 376 policy := d.Get("policy").(string) 377 378 if policy != "" { 379 log.Printf("[DEBUG] S3 bucket: %s, put policy: %s", bucket, policy) 380 381 _, err := s3conn.PutBucketPolicy(&s3.PutBucketPolicyInput{ 382 Bucket: aws.String(bucket), 383 Policy: aws.String(policy), 384 }) 385 386 if err != nil { 387 return fmt.Errorf("Error putting S3 policy: %s", err) 388 } 389 } else { 390 log.Printf("[DEBUG] S3 bucket: %s, delete policy: %s", bucket, policy) 391 _, err := s3conn.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{ 392 Bucket: aws.String(bucket), 393 }) 394 395 if err != nil { 396 return fmt.Errorf("Error deleting S3 policy: %s", err) 397 } 398 } 399 400 return nil 401 } 402 403 func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 404 ws := d.Get("website").([]interface{}) 405 406 if len(ws) == 1 { 407 w := ws[0].(map[string]interface{}) 408 return resourceAwsS3BucketWebsitePut(s3conn, d, w) 409 } else if len(ws) == 0 { 410 return resourceAwsS3BucketWebsiteDelete(s3conn, d) 411 } else { 412 return fmt.Errorf("Cannot specify more than one website.") 413 } 414 } 415 416 func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, website map[string]interface{}) error { 417 bucket := d.Get("bucket").(string) 418 419 indexDocument := website["index_document"].(string) 420 errorDocument := website["error_document"].(string) 421 redirectAllRequestsTo := website["redirect_all_requests_to"].(string) 422 423 if indexDocument == "" && redirectAllRequestsTo == "" { 424 return fmt.Errorf("Must specify either index_document or redirect_all_requests_to.") 425 } 426 427 websiteConfiguration := &s3.WebsiteConfiguration{} 428 429 if indexDocument != "" { 430 websiteConfiguration.IndexDocument = &s3.IndexDocument{Suffix: aws.String(indexDocument)} 431 } 432 433 if errorDocument != "" { 434 websiteConfiguration.ErrorDocument = &s3.ErrorDocument{Key: aws.String(errorDocument)} 435 } 436 437 if redirectAllRequestsTo != "" { 438 websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectAllRequestsTo)} 439 } 440 441 putInput := &s3.PutBucketWebsiteInput{ 442 Bucket: aws.String(bucket), 443 WebsiteConfiguration: websiteConfiguration, 444 } 445 446 log.Printf("[DEBUG] S3 put bucket website: %#v", putInput) 447 448 _, err := s3conn.PutBucketWebsite(putInput) 449 if err != nil { 450 return fmt.Errorf("Error putting S3 website: %s", err) 451 } 452 453 return nil 454 } 455 456 func resourceAwsS3BucketWebsiteDelete(s3conn *s3.S3, d *schema.ResourceData) error { 457 bucket := d.Get("bucket").(string) 458 deleteInput := &s3.DeleteBucketWebsiteInput{Bucket: aws.String(bucket)} 459 460 log.Printf("[DEBUG] S3 delete bucket website: %#v", deleteInput) 461 462 _, err := s3conn.DeleteBucketWebsite(deleteInput) 463 if err != nil { 464 return fmt.Errorf("Error deleting S3 website: %s", err) 465 } 466 467 d.Set("website_endpoint", "") 468 d.Set("website_domain", "") 469 470 return nil 471 } 472 473 func websiteEndpoint(s3conn *s3.S3, d *schema.ResourceData) (*S3Website, error) { 474 // If the bucket doesn't have a website configuration, return an empty 475 // endpoint 476 if _, ok := d.GetOk("website"); !ok { 477 return nil, nil 478 } 479 480 bucket := d.Get("bucket").(string) 481 482 // Lookup the region for this bucket 483 location, err := s3conn.GetBucketLocation( 484 &s3.GetBucketLocationInput{ 485 Bucket: aws.String(bucket), 486 }, 487 ) 488 if err != nil { 489 return nil, err 490 } 491 var region string 492 if location.LocationConstraint != nil { 493 region = *location.LocationConstraint 494 } 495 496 return WebsiteEndpoint(bucket, region), nil 497 } 498 499 func WebsiteEndpoint(bucket string, region string) *S3Website { 500 domain := WebsiteDomainUrl(region) 501 return &S3Website{Endpoint: fmt.Sprintf("%s.%s", bucket, domain), Domain: domain} 502 } 503 504 func WebsiteDomainUrl(region string) string { 505 region = normalizeRegion(region) 506 507 // Frankfurt(and probably future) regions uses different syntax for website endpoints 508 // http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html 509 if region == "eu-central-1" { 510 return fmt.Sprintf("s3-website.%s.amazonaws.com", region) 511 } 512 513 return fmt.Sprintf("s3-website-%s.amazonaws.com", region) 514 } 515 516 func resourceAwsS3BucketVersioningUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 517 v := d.Get("versioning").(*schema.Set).List() 518 bucket := d.Get("bucket").(string) 519 vc := &s3.VersioningConfiguration{} 520 521 if len(v) > 0 { 522 c := v[0].(map[string]interface{}) 523 524 if c["enabled"].(bool) { 525 vc.Status = aws.String(s3.BucketVersioningStatusEnabled) 526 } else { 527 vc.Status = aws.String(s3.BucketVersioningStatusSuspended) 528 } 529 } else { 530 vc.Status = aws.String(s3.BucketVersioningStatusSuspended) 531 } 532 533 i := &s3.PutBucketVersioningInput{ 534 Bucket: aws.String(bucket), 535 VersioningConfiguration: vc, 536 } 537 log.Printf("[DEBUG] S3 put bucket versioning: %#v", i) 538 539 _, err := s3conn.PutBucketVersioning(i) 540 if err != nil { 541 return fmt.Errorf("Error putting S3 versioning: %s", err) 542 } 543 544 return nil 545 } 546 547 func normalizeJson(jsonString interface{}) string { 548 if jsonString == nil { 549 return "" 550 } 551 j := make(map[string]interface{}) 552 err := json.Unmarshal([]byte(jsonString.(string)), &j) 553 if err != nil { 554 return fmt.Sprintf("Error parsing JSON: %s", err) 555 } 556 b, _ := json.Marshal(j) 557 return string(b[:]) 558 } 559 560 func normalizeRegion(region string) string { 561 // Default to us-east-1 if the bucket doesn't have a region: 562 // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html 563 if region == "" { 564 region = "us-east-1" 565 } 566 567 return region 568 } 569 570 type S3Website struct { 571 Endpoint, Domain string 572 }