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