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