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  }