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