github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/providers/aws/resource_aws_s3_bucket.go (about)

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