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