github.com/jmbataller/terraform@v0.6.8-0.20151125192640-b7a12e3a580c/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 "arn": &schema.Schema{ 32 Type: schema.TypeString, 33 Optional: true, 34 Computed: true, 35 }, 36 37 "acl": &schema.Schema{ 38 Type: schema.TypeString, 39 Default: "private", 40 Optional: true, 41 ForceNew: true, 42 }, 43 44 "policy": &schema.Schema{ 45 Type: schema.TypeString, 46 Optional: true, 47 StateFunc: normalizeJson, 48 }, 49 50 "cors_rule": &schema.Schema{ 51 Type: schema.TypeList, 52 Optional: true, 53 Elem: &schema.Resource{ 54 Schema: map[string]*schema.Schema{ 55 "allowed_headers": &schema.Schema{ 56 Type: schema.TypeList, 57 Optional: true, 58 Elem: &schema.Schema{Type: schema.TypeString}, 59 }, 60 "allowed_methods": &schema.Schema{ 61 Type: schema.TypeList, 62 Required: true, 63 Elem: &schema.Schema{Type: schema.TypeString}, 64 }, 65 "allowed_origins": &schema.Schema{ 66 Type: schema.TypeList, 67 Required: true, 68 Elem: &schema.Schema{Type: schema.TypeString}, 69 }, 70 "expose_headers": &schema.Schema{ 71 Type: schema.TypeList, 72 Optional: true, 73 Elem: &schema.Schema{Type: schema.TypeString}, 74 }, 75 "max_age_seconds": &schema.Schema{ 76 Type: schema.TypeInt, 77 Optional: true, 78 }, 79 }, 80 }, 81 }, 82 83 "website": &schema.Schema{ 84 Type: schema.TypeList, 85 Optional: true, 86 Elem: &schema.Resource{ 87 Schema: map[string]*schema.Schema{ 88 "index_document": &schema.Schema{ 89 Type: schema.TypeString, 90 Optional: true, 91 }, 92 93 "error_document": &schema.Schema{ 94 Type: schema.TypeString, 95 Optional: true, 96 }, 97 98 "redirect_all_requests_to": &schema.Schema{ 99 Type: schema.TypeString, 100 ConflictsWith: []string{ 101 "website.0.index_document", 102 "website.0.error_document", 103 }, 104 Optional: true, 105 }, 106 }, 107 }, 108 }, 109 110 "hosted_zone_id": &schema.Schema{ 111 Type: schema.TypeString, 112 Optional: true, 113 Computed: true, 114 }, 115 116 "region": &schema.Schema{ 117 Type: schema.TypeString, 118 Optional: true, 119 Computed: true, 120 }, 121 "website_endpoint": &schema.Schema{ 122 Type: schema.TypeString, 123 Optional: true, 124 Computed: true, 125 }, 126 "website_domain": &schema.Schema{ 127 Type: schema.TypeString, 128 Optional: true, 129 Computed: true, 130 }, 131 132 "versioning": &schema.Schema{ 133 Type: schema.TypeSet, 134 Optional: true, 135 Elem: &schema.Resource{ 136 Schema: map[string]*schema.Schema{ 137 "enabled": &schema.Schema{ 138 Type: schema.TypeBool, 139 Optional: true, 140 Default: false, 141 }, 142 }, 143 }, 144 Set: func(v interface{}) int { 145 var buf bytes.Buffer 146 m := v.(map[string]interface{}) 147 buf.WriteString(fmt.Sprintf("%t-", m["enabled"].(bool))) 148 149 return hashcode.String(buf.String()) 150 }, 151 }, 152 153 "tags": tagsSchema(), 154 155 "force_destroy": &schema.Schema{ 156 Type: schema.TypeBool, 157 Optional: true, 158 Default: false, 159 }, 160 }, 161 } 162 } 163 164 func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error { 165 s3conn := meta.(*AWSClient).s3conn 166 awsRegion := meta.(*AWSClient).region 167 168 // Get the bucket and acl 169 bucket := d.Get("bucket").(string) 170 acl := d.Get("acl").(string) 171 172 log.Printf("[DEBUG] S3 bucket create: %s, ACL: %s", bucket, acl) 173 174 req := &s3.CreateBucketInput{ 175 Bucket: aws.String(bucket), 176 ACL: aws.String(acl), 177 } 178 179 // Special case us-east-1 region and do not set the LocationConstraint. 180 // See "Request Elements: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html 181 if awsRegion != "us-east-1" { 182 req.CreateBucketConfiguration = &s3.CreateBucketConfiguration{ 183 LocationConstraint: aws.String(awsRegion), 184 } 185 } 186 187 _, err := s3conn.CreateBucket(req) 188 if err != nil { 189 return fmt.Errorf("Error creating S3 bucket: %s", err) 190 } 191 192 // Assign the bucket name as the resource ID 193 d.SetId(bucket) 194 195 return resourceAwsS3BucketUpdate(d, meta) 196 } 197 198 func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { 199 s3conn := meta.(*AWSClient).s3conn 200 if err := setTagsS3(s3conn, d); err != nil { 201 return err 202 } 203 204 if d.HasChange("policy") { 205 if err := resourceAwsS3BucketPolicyUpdate(s3conn, d); err != nil { 206 return err 207 } 208 } 209 210 if d.HasChange("cors_rule") { 211 if err := resourceAwsS3BucketCorsUpdate(s3conn, d); err != nil { 212 return err 213 } 214 } 215 216 if d.HasChange("website") { 217 if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { 218 return err 219 } 220 } 221 222 if d.HasChange("versioning") { 223 if err := resourceAwsS3BucketVersioningUpdate(s3conn, d); err != nil { 224 return err 225 } 226 } 227 228 return resourceAwsS3BucketRead(d, meta) 229 } 230 231 func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { 232 s3conn := meta.(*AWSClient).s3conn 233 234 var err error 235 _, err = s3conn.HeadBucket(&s3.HeadBucketInput{ 236 Bucket: aws.String(d.Id()), 237 }) 238 if err != nil { 239 if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() == 404 { 240 log.Printf("[WARN] S3 Bucket (%s) not found, error code (404)", d.Id()) 241 d.SetId("") 242 return nil 243 } else { 244 // some of the AWS SDK's errors can be empty strings, so let's add 245 // some additional context. 246 return fmt.Errorf("error reading S3 bucket \"%s\": %s", d.Id(), err) 247 } 248 } 249 250 // Read the policy 251 pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ 252 Bucket: aws.String(d.Id()), 253 }) 254 log.Printf("[DEBUG] S3 bucket: %s, read policy: %v", d.Id(), pol) 255 if err != nil { 256 if err := d.Set("policy", ""); err != nil { 257 return err 258 } 259 } else { 260 if v := pol.Policy; v == nil { 261 if err := d.Set("policy", ""); err != nil { 262 return err 263 } 264 } else if err := d.Set("policy", normalizeJson(*v)); err != nil { 265 return err 266 } 267 } 268 269 // Read the CORS 270 cors, err := s3conn.GetBucketCors(&s3.GetBucketCorsInput{ 271 Bucket: aws.String(d.Id()), 272 }) 273 log.Printf("[DEBUG] S3 bucket: %s, read CORS: %v", d.Id(), cors) 274 if err != nil { 275 rules := make([]map[string]interface{}, 0, len(cors.CORSRules)) 276 for _, ruleObject := range cors.CORSRules { 277 rule := make(map[string]interface{}) 278 rule["allowed_headers"] = ruleObject.AllowedHeaders 279 rule["allowed_methods"] = ruleObject.AllowedMethods 280 rule["allowed_origins"] = ruleObject.AllowedOrigins 281 rule["expose_headers"] = ruleObject.ExposeHeaders 282 rule["max_age_seconds"] = ruleObject.MaxAgeSeconds 283 rules = append(rules, rule) 284 } 285 if err := d.Set("cors_rule", rules); err != nil { 286 return fmt.Errorf("error reading S3 bucket \"%s\" CORS rules: %s", d.Id(), err) 287 } 288 } 289 290 // Read the website configuration 291 ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ 292 Bucket: aws.String(d.Id()), 293 }) 294 var websites []map[string]interface{} 295 if err == nil { 296 w := make(map[string]interface{}) 297 298 if v := ws.IndexDocument; v != nil { 299 w["index_document"] = *v.Suffix 300 } 301 302 if v := ws.ErrorDocument; v != nil { 303 w["error_document"] = *v.Key 304 } 305 306 if v := ws.RedirectAllRequestsTo; v != nil { 307 w["redirect_all_requests_to"] = *v.HostName 308 } 309 310 websites = append(websites, w) 311 } 312 if err := d.Set("website", websites); err != nil { 313 return err 314 } 315 316 // Read the versioning configuration 317 versioning, err := s3conn.GetBucketVersioning(&s3.GetBucketVersioningInput{ 318 Bucket: aws.String(d.Id()), 319 }) 320 if err != nil { 321 return err 322 } 323 log.Printf("[DEBUG] S3 Bucket: %s, versioning: %v", d.Id(), versioning) 324 if versioning.Status != nil && *versioning.Status == s3.BucketVersioningStatusEnabled { 325 vcl := make([]map[string]interface{}, 0, 1) 326 vc := make(map[string]interface{}) 327 if *versioning.Status == s3.BucketVersioningStatusEnabled { 328 vc["enabled"] = true 329 } else { 330 vc["enabled"] = false 331 } 332 vcl = append(vcl, vc) 333 if err := d.Set("versioning", vcl); err != nil { 334 return err 335 } 336 } 337 338 // Add the region as an attribute 339 location, err := s3conn.GetBucketLocation( 340 &s3.GetBucketLocationInput{ 341 Bucket: aws.String(d.Id()), 342 }, 343 ) 344 if err != nil { 345 return err 346 } 347 var region string 348 if location.LocationConstraint != nil { 349 region = *location.LocationConstraint 350 } 351 region = normalizeRegion(region) 352 if err := d.Set("region", region); err != nil { 353 return err 354 } 355 356 // Add the hosted zone ID for this bucket's region as an attribute 357 hostedZoneID := HostedZoneIDForRegion(region) 358 if err := d.Set("hosted_zone_id", hostedZoneID); err != nil { 359 return err 360 } 361 362 // Add website_endpoint as an attribute 363 websiteEndpoint, err := websiteEndpoint(s3conn, d) 364 if err != nil { 365 return err 366 } 367 if websiteEndpoint != nil { 368 if err := d.Set("website_endpoint", websiteEndpoint.Endpoint); err != nil { 369 return err 370 } 371 if err := d.Set("website_domain", websiteEndpoint.Domain); err != nil { 372 return err 373 } 374 } 375 376 tagSet, err := getTagSetS3(s3conn, d.Id()) 377 if err != nil { 378 return err 379 } 380 381 if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil { 382 return err 383 } 384 385 d.Set("arn", fmt.Sprint("arn:aws:s3:::", d.Id())) 386 387 return nil 388 } 389 390 func resourceAwsS3BucketDelete(d *schema.ResourceData, meta interface{}) error { 391 s3conn := meta.(*AWSClient).s3conn 392 393 log.Printf("[DEBUG] S3 Delete Bucket: %s", d.Id()) 394 _, err := s3conn.DeleteBucket(&s3.DeleteBucketInput{ 395 Bucket: aws.String(d.Id()), 396 }) 397 if err != nil { 398 ec2err, ok := err.(awserr.Error) 399 if ok && ec2err.Code() == "BucketNotEmpty" { 400 if d.Get("force_destroy").(bool) { 401 // bucket may have things delete them 402 log.Printf("[DEBUG] S3 Bucket attempting to forceDestroy %+v", err) 403 404 bucket := d.Get("bucket").(string) 405 resp, err := s3conn.ListObjects( 406 &s3.ListObjectsInput{ 407 Bucket: aws.String(bucket), 408 }, 409 ) 410 411 if err != nil { 412 return fmt.Errorf("Error S3 Bucket list Objects err: %s", err) 413 } 414 415 objectsToDelete := make([]*s3.ObjectIdentifier, len(resp.Contents)) 416 for i, v := range resp.Contents { 417 objectsToDelete[i] = &s3.ObjectIdentifier{ 418 Key: v.Key, 419 } 420 } 421 _, err = s3conn.DeleteObjects( 422 &s3.DeleteObjectsInput{ 423 Bucket: aws.String(bucket), 424 Delete: &s3.Delete{ 425 Objects: objectsToDelete, 426 }, 427 }, 428 ) 429 if err != nil { 430 return fmt.Errorf("Error S3 Bucket force_destroy error deleting: %s", err) 431 } 432 433 // this line recurses until all objects are deleted or an error is returned 434 return resourceAwsS3BucketDelete(d, meta) 435 } 436 } 437 return fmt.Errorf("Error deleting S3 Bucket: %s", err) 438 } 439 return nil 440 } 441 442 func resourceAwsS3BucketPolicyUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 443 bucket := d.Get("bucket").(string) 444 policy := d.Get("policy").(string) 445 446 if policy != "" { 447 log.Printf("[DEBUG] S3 bucket: %s, put policy: %s", bucket, policy) 448 449 _, err := s3conn.PutBucketPolicy(&s3.PutBucketPolicyInput{ 450 Bucket: aws.String(bucket), 451 Policy: aws.String(policy), 452 }) 453 454 if err != nil { 455 return fmt.Errorf("Error putting S3 policy: %s", err) 456 } 457 } else { 458 log.Printf("[DEBUG] S3 bucket: %s, delete policy: %s", bucket, policy) 459 _, err := s3conn.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{ 460 Bucket: aws.String(bucket), 461 }) 462 463 if err != nil { 464 return fmt.Errorf("Error deleting S3 policy: %s", err) 465 } 466 } 467 468 return nil 469 } 470 471 func resourceAwsS3BucketCorsUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 472 bucket := d.Get("bucket").(string) 473 rawCors := d.Get("cors_rule").([]interface{}) 474 475 if len(rawCors) == 0 { 476 // Delete CORS 477 log.Printf("[DEBUG] S3 bucket: %s, delete CORS", bucket) 478 _, err := s3conn.DeleteBucketCors(&s3.DeleteBucketCorsInput{ 479 Bucket: aws.String(bucket), 480 }) 481 if err != nil { 482 return fmt.Errorf("Error deleting S3 CORS: %s", err) 483 } 484 } else { 485 // Put CORS 486 rules := make([]*s3.CORSRule, 0, len(rawCors)) 487 for _, cors := range rawCors { 488 corsMap := cors.(map[string]interface{}) 489 r := &s3.CORSRule{} 490 for k, v := range corsMap { 491 log.Printf("[DEBUG] S3 bucket: %s, put CORS: %#v, %#v", bucket, k, v) 492 if k == "max_age_seconds" { 493 r.MaxAgeSeconds = aws.Int64(int64(v.(int))) 494 } else { 495 vMap := make([]*string, len(v.([]interface{}))) 496 for i, vv := range v.([]interface{}) { 497 str := vv.(string) 498 vMap[i] = aws.String(str) 499 } 500 switch k { 501 case "allowed_headers": 502 r.AllowedHeaders = vMap 503 case "allowed_methods": 504 r.AllowedMethods = vMap 505 case "allowed_origins": 506 r.AllowedOrigins = vMap 507 case "expose_headers": 508 r.ExposeHeaders = vMap 509 } 510 } 511 } 512 rules = append(rules, r) 513 } 514 corsInput := &s3.PutBucketCorsInput{ 515 Bucket: aws.String(bucket), 516 CORSConfiguration: &s3.CORSConfiguration{ 517 CORSRules: rules, 518 }, 519 } 520 log.Printf("[DEBUG] S3 bucket: %s, put CORS: %#v", bucket, corsInput) 521 _, err := s3conn.PutBucketCors(corsInput) 522 if err != nil { 523 return fmt.Errorf("Error putting S3 CORS: %s", err) 524 } 525 } 526 527 return nil 528 } 529 530 func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 531 ws := d.Get("website").([]interface{}) 532 533 if len(ws) == 1 { 534 w := ws[0].(map[string]interface{}) 535 return resourceAwsS3BucketWebsitePut(s3conn, d, w) 536 } else if len(ws) == 0 { 537 return resourceAwsS3BucketWebsiteDelete(s3conn, d) 538 } else { 539 return fmt.Errorf("Cannot specify more than one website.") 540 } 541 } 542 543 func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, website map[string]interface{}) error { 544 bucket := d.Get("bucket").(string) 545 546 indexDocument := website["index_document"].(string) 547 errorDocument := website["error_document"].(string) 548 redirectAllRequestsTo := website["redirect_all_requests_to"].(string) 549 550 if indexDocument == "" && redirectAllRequestsTo == "" { 551 return fmt.Errorf("Must specify either index_document or redirect_all_requests_to.") 552 } 553 554 websiteConfiguration := &s3.WebsiteConfiguration{} 555 556 if indexDocument != "" { 557 websiteConfiguration.IndexDocument = &s3.IndexDocument{Suffix: aws.String(indexDocument)} 558 } 559 560 if errorDocument != "" { 561 websiteConfiguration.ErrorDocument = &s3.ErrorDocument{Key: aws.String(errorDocument)} 562 } 563 564 if redirectAllRequestsTo != "" { 565 websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectAllRequestsTo)} 566 } 567 568 putInput := &s3.PutBucketWebsiteInput{ 569 Bucket: aws.String(bucket), 570 WebsiteConfiguration: websiteConfiguration, 571 } 572 573 log.Printf("[DEBUG] S3 put bucket website: %#v", putInput) 574 575 _, err := s3conn.PutBucketWebsite(putInput) 576 if err != nil { 577 return fmt.Errorf("Error putting S3 website: %s", err) 578 } 579 580 return nil 581 } 582 583 func resourceAwsS3BucketWebsiteDelete(s3conn *s3.S3, d *schema.ResourceData) error { 584 bucket := d.Get("bucket").(string) 585 deleteInput := &s3.DeleteBucketWebsiteInput{Bucket: aws.String(bucket)} 586 587 log.Printf("[DEBUG] S3 delete bucket website: %#v", deleteInput) 588 589 _, err := s3conn.DeleteBucketWebsite(deleteInput) 590 if err != nil { 591 return fmt.Errorf("Error deleting S3 website: %s", err) 592 } 593 594 d.Set("website_endpoint", "") 595 d.Set("website_domain", "") 596 597 return nil 598 } 599 600 func websiteEndpoint(s3conn *s3.S3, d *schema.ResourceData) (*S3Website, error) { 601 // If the bucket doesn't have a website configuration, return an empty 602 // endpoint 603 if _, ok := d.GetOk("website"); !ok { 604 return nil, nil 605 } 606 607 bucket := d.Get("bucket").(string) 608 609 // Lookup the region for this bucket 610 location, err := s3conn.GetBucketLocation( 611 &s3.GetBucketLocationInput{ 612 Bucket: aws.String(bucket), 613 }, 614 ) 615 if err != nil { 616 return nil, err 617 } 618 var region string 619 if location.LocationConstraint != nil { 620 region = *location.LocationConstraint 621 } 622 623 return WebsiteEndpoint(bucket, region), nil 624 } 625 626 func WebsiteEndpoint(bucket string, region string) *S3Website { 627 domain := WebsiteDomainUrl(region) 628 return &S3Website{Endpoint: fmt.Sprintf("%s.%s", bucket, domain), Domain: domain} 629 } 630 631 func WebsiteDomainUrl(region string) string { 632 region = normalizeRegion(region) 633 634 // Frankfurt(and probably future) regions uses different syntax for website endpoints 635 // http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html 636 if region == "eu-central-1" { 637 return fmt.Sprintf("s3-website.%s.amazonaws.com", region) 638 } 639 640 return fmt.Sprintf("s3-website-%s.amazonaws.com", region) 641 } 642 643 func resourceAwsS3BucketVersioningUpdate(s3conn *s3.S3, d *schema.ResourceData) error { 644 v := d.Get("versioning").(*schema.Set).List() 645 bucket := d.Get("bucket").(string) 646 vc := &s3.VersioningConfiguration{} 647 648 if len(v) > 0 { 649 c := v[0].(map[string]interface{}) 650 651 if c["enabled"].(bool) { 652 vc.Status = aws.String(s3.BucketVersioningStatusEnabled) 653 } else { 654 vc.Status = aws.String(s3.BucketVersioningStatusSuspended) 655 } 656 } else { 657 vc.Status = aws.String(s3.BucketVersioningStatusSuspended) 658 } 659 660 i := &s3.PutBucketVersioningInput{ 661 Bucket: aws.String(bucket), 662 VersioningConfiguration: vc, 663 } 664 log.Printf("[DEBUG] S3 put bucket versioning: %#v", i) 665 666 _, err := s3conn.PutBucketVersioning(i) 667 if err != nil { 668 return fmt.Errorf("Error putting S3 versioning: %s", err) 669 } 670 671 return nil 672 } 673 674 func normalizeJson(jsonString interface{}) string { 675 if jsonString == nil { 676 return "" 677 } 678 j := make(map[string]interface{}) 679 err := json.Unmarshal([]byte(jsonString.(string)), &j) 680 if err != nil { 681 return fmt.Sprintf("Error parsing JSON: %s", err) 682 } 683 b, _ := json.Marshal(j) 684 return string(b[:]) 685 } 686 687 func normalizeRegion(region string) string { 688 // Default to us-east-1 if the bucket doesn't have a region: 689 // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html 690 if region == "" { 691 region = "us-east-1" 692 } 693 694 return region 695 } 696 697 type S3Website struct { 698 Endpoint, Domain string 699 }