github.com/jsoriano/terraform@v0.6.7-0.20151026070445-8b70867fdd95/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  			"acl": &schema.Schema{
    32  				Type:     schema.TypeString,
    33  				Default:  "private",
    34  				Optional: true,
    35  				ForceNew: true,
    36  			},
    37  
    38  			"policy": &schema.Schema{
    39  				Type:      schema.TypeString,
    40  				Optional:  true,
    41  				StateFunc: normalizeJson,
    42  			},
    43  
    44  			"website": &schema.Schema{
    45  				Type:     schema.TypeList,
    46  				Optional: true,
    47  				Elem: &schema.Resource{
    48  					Schema: map[string]*schema.Schema{
    49  						"index_document": &schema.Schema{
    50  							Type:     schema.TypeString,
    51  							Optional: true,
    52  						},
    53  
    54  						"error_document": &schema.Schema{
    55  							Type:     schema.TypeString,
    56  							Optional: true,
    57  						},
    58  
    59  						"redirect_all_requests_to": &schema.Schema{
    60  							Type: schema.TypeString,
    61  							ConflictsWith: []string{
    62  								"website.0.index_document",
    63  								"website.0.error_document",
    64  							},
    65  							Optional: true,
    66  						},
    67  					},
    68  				},
    69  			},
    70  
    71  			"hosted_zone_id": &schema.Schema{
    72  				Type:     schema.TypeString,
    73  				Optional: true,
    74  				Computed: true,
    75  			},
    76  
    77  			"region": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Optional: true,
    80  				Computed: true,
    81  			},
    82  			"website_endpoint": &schema.Schema{
    83  				Type:     schema.TypeString,
    84  				Optional: true,
    85  				Computed: true,
    86  			},
    87  			"website_domain": &schema.Schema{
    88  				Type:     schema.TypeString,
    89  				Optional: true,
    90  				Computed: true,
    91  			},
    92  
    93  			"versioning": &schema.Schema{
    94  				Type:     schema.TypeSet,
    95  				Optional: true,
    96  				Elem: &schema.Resource{
    97  					Schema: map[string]*schema.Schema{
    98  						"enabled": &schema.Schema{
    99  							Type:     schema.TypeBool,
   100  							Optional: true,
   101  							Default:  false,
   102  						},
   103  					},
   104  				},
   105  				Set: func(v interface{}) int {
   106  					var buf bytes.Buffer
   107  					m := v.(map[string]interface{})
   108  					buf.WriteString(fmt.Sprintf("%t-", m["enabled"].(bool)))
   109  
   110  					return hashcode.String(buf.String())
   111  				},
   112  			},
   113  
   114  			"tags": tagsSchema(),
   115  
   116  			"force_destroy": &schema.Schema{
   117  				Type:     schema.TypeBool,
   118  				Optional: true,
   119  				Default:  false,
   120  			},
   121  		},
   122  	}
   123  }
   124  
   125  func resourceAwsS3BucketCreate(d *schema.ResourceData, meta interface{}) error {
   126  	s3conn := meta.(*AWSClient).s3conn
   127  	awsRegion := meta.(*AWSClient).region
   128  
   129  	// Get the bucket and acl
   130  	bucket := d.Get("bucket").(string)
   131  	acl := d.Get("acl").(string)
   132  
   133  	log.Printf("[DEBUG] S3 bucket create: %s, ACL: %s", bucket, acl)
   134  
   135  	req := &s3.CreateBucketInput{
   136  		Bucket: aws.String(bucket),
   137  		ACL:    aws.String(acl),
   138  	}
   139  
   140  	// Special case us-east-1 region and do not set the LocationConstraint.
   141  	// See "Request Elements: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUT.html
   142  	if awsRegion != "us-east-1" {
   143  		req.CreateBucketConfiguration = &s3.CreateBucketConfiguration{
   144  			LocationConstraint: aws.String(awsRegion),
   145  		}
   146  	}
   147  
   148  	_, err := s3conn.CreateBucket(req)
   149  	if err != nil {
   150  		return fmt.Errorf("Error creating S3 bucket: %s", err)
   151  	}
   152  
   153  	// Assign the bucket name as the resource ID
   154  	d.SetId(bucket)
   155  
   156  	return resourceAwsS3BucketUpdate(d, meta)
   157  }
   158  
   159  func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error {
   160  	s3conn := meta.(*AWSClient).s3conn
   161  	if err := setTagsS3(s3conn, d); err != nil {
   162  		return err
   163  	}
   164  
   165  	if d.HasChange("policy") {
   166  		if err := resourceAwsS3BucketPolicyUpdate(s3conn, d); err != nil {
   167  			return err
   168  		}
   169  	}
   170  
   171  	if d.HasChange("website") {
   172  		if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil {
   173  			return err
   174  		}
   175  	}
   176  
   177  	if d.HasChange("versioning") {
   178  		if err := resourceAwsS3BucketVersioningUpdate(s3conn, d); err != nil {
   179  			return err
   180  		}
   181  	}
   182  
   183  	return resourceAwsS3BucketRead(d, meta)
   184  }
   185  
   186  func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
   187  	s3conn := meta.(*AWSClient).s3conn
   188  
   189  	var err error
   190  	_, err = s3conn.HeadBucket(&s3.HeadBucketInput{
   191  		Bucket: aws.String(d.Id()),
   192  	})
   193  	if err != nil {
   194  		if awsError, ok := err.(awserr.RequestFailure); ok && awsError.StatusCode() == 404 {
   195  			log.Printf("[WARN] S3 Bucket (%s) not found, error code (404)", d.Id())
   196  			d.SetId("")
   197  			return nil
   198  		} else {
   199  			// some of the AWS SDK's errors can be empty strings, so let's add
   200  			// some additional context.
   201  			return fmt.Errorf("error reading S3 bucket \"%s\": %s", d.Id(), err)
   202  		}
   203  	}
   204  
   205  	// Read the policy
   206  	pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{
   207  		Bucket: aws.String(d.Id()),
   208  	})
   209  	log.Printf("[DEBUG] S3 bucket: %s, read policy: %v", d.Id(), pol)
   210  	if err != nil {
   211  		if err := d.Set("policy", ""); err != nil {
   212  			return err
   213  		}
   214  	} else {
   215  		if v := pol.Policy; v == nil {
   216  			if err := d.Set("policy", ""); err != nil {
   217  				return err
   218  			}
   219  		} else if err := d.Set("policy", normalizeJson(*v)); err != nil {
   220  			return err
   221  		}
   222  	}
   223  
   224  	// Read the website configuration
   225  	ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{
   226  		Bucket: aws.String(d.Id()),
   227  	})
   228  	var websites []map[string]interface{}
   229  	if err == nil {
   230  		w := make(map[string]interface{})
   231  
   232  		if v := ws.IndexDocument; v != nil {
   233  			w["index_document"] = *v.Suffix
   234  		}
   235  
   236  		if v := ws.ErrorDocument; v != nil {
   237  			w["error_document"] = *v.Key
   238  		}
   239  
   240  		if v := ws.RedirectAllRequestsTo; v != nil {
   241  			w["redirect_all_requests_to"] = *v.HostName
   242  		}
   243  
   244  		websites = append(websites, w)
   245  	}
   246  	if err := d.Set("website", websites); err != nil {
   247  		return err
   248  	}
   249  
   250  	// Read the versioning configuration
   251  	versioning, err := s3conn.GetBucketVersioning(&s3.GetBucketVersioningInput{
   252  		Bucket: aws.String(d.Id()),
   253  	})
   254  	if err != nil {
   255  		return err
   256  	}
   257  	log.Printf("[DEBUG] S3 Bucket: %s, versioning: %v", d.Id(), versioning)
   258  	if versioning.Status != nil && *versioning.Status == s3.BucketVersioningStatusEnabled {
   259  		vcl := make([]map[string]interface{}, 0, 1)
   260  		vc := make(map[string]interface{})
   261  		if *versioning.Status == s3.BucketVersioningStatusEnabled {
   262  			vc["enabled"] = true
   263  		} else {
   264  			vc["enabled"] = false
   265  		}
   266  		vcl = append(vcl, vc)
   267  		if err := d.Set("versioning", vcl); err != nil {
   268  			return err
   269  		}
   270  	}
   271  
   272  	// Add the region as an attribute
   273  	location, err := s3conn.GetBucketLocation(
   274  		&s3.GetBucketLocationInput{
   275  			Bucket: aws.String(d.Id()),
   276  		},
   277  	)
   278  	if err != nil {
   279  		return err
   280  	}
   281  	var region string
   282  	if location.LocationConstraint != nil {
   283  		region = *location.LocationConstraint
   284  	}
   285  	region = normalizeRegion(region)
   286  	if err := d.Set("region", region); err != nil {
   287  		return err
   288  	}
   289  
   290  	// Add the hosted zone ID for this bucket's region as an attribute
   291  	hostedZoneID := HostedZoneIDForRegion(region)
   292  	if err := d.Set("hosted_zone_id", hostedZoneID); err != nil {
   293  		return err
   294  	}
   295  
   296  	// Add website_endpoint as an attribute
   297  	websiteEndpoint, err := websiteEndpoint(s3conn, d)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	if websiteEndpoint != nil {
   302  		if err := d.Set("website_endpoint", websiteEndpoint.Endpoint); err != nil {
   303  			return err
   304  		}
   305  		if err := d.Set("website_domain", websiteEndpoint.Domain); err != nil {
   306  			return err
   307  		}
   308  	}
   309  
   310  	tagSet, err := getTagSetS3(s3conn, d.Id())
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	if err := d.Set("tags", tagsToMapS3(tagSet)); err != nil {
   316  		return err
   317  	}
   318  
   319  	return nil
   320  }
   321  
   322  func resourceAwsS3BucketDelete(d *schema.ResourceData, meta interface{}) error {
   323  	s3conn := meta.(*AWSClient).s3conn
   324  
   325  	log.Printf("[DEBUG] S3 Delete Bucket: %s", d.Id())
   326  	_, err := s3conn.DeleteBucket(&s3.DeleteBucketInput{
   327  		Bucket: aws.String(d.Id()),
   328  	})
   329  	if err != nil {
   330  		ec2err, ok := err.(awserr.Error)
   331  		if ok && ec2err.Code() == "BucketNotEmpty" {
   332  			if d.Get("force_destroy").(bool) {
   333  				// bucket may have things delete them
   334  				log.Printf("[DEBUG] S3 Bucket attempting to forceDestroy %+v", err)
   335  
   336  				bucket := d.Get("bucket").(string)
   337  				resp, err := s3conn.ListObjects(
   338  					&s3.ListObjectsInput{
   339  						Bucket: aws.String(bucket),
   340  					},
   341  				)
   342  
   343  				if err != nil {
   344  					return fmt.Errorf("Error S3 Bucket list Objects err: %s", err)
   345  				}
   346  
   347  				objectsToDelete := make([]*s3.ObjectIdentifier, len(resp.Contents))
   348  				for i, v := range resp.Contents {
   349  					objectsToDelete[i] = &s3.ObjectIdentifier{
   350  						Key: v.Key,
   351  					}
   352  				}
   353  				_, err = s3conn.DeleteObjects(
   354  					&s3.DeleteObjectsInput{
   355  						Bucket: aws.String(bucket),
   356  						Delete: &s3.Delete{
   357  							Objects: objectsToDelete,
   358  						},
   359  					},
   360  				)
   361  				if err != nil {
   362  					return fmt.Errorf("Error S3 Bucket force_destroy error deleting: %s", err)
   363  				}
   364  
   365  				// this line recurses until all objects are deleted or an error is returned
   366  				return resourceAwsS3BucketDelete(d, meta)
   367  			}
   368  		}
   369  		return fmt.Errorf("Error deleting S3 Bucket: %s", err)
   370  	}
   371  	return nil
   372  }
   373  
   374  func resourceAwsS3BucketPolicyUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
   375  	bucket := d.Get("bucket").(string)
   376  	policy := d.Get("policy").(string)
   377  
   378  	if policy != "" {
   379  		log.Printf("[DEBUG] S3 bucket: %s, put policy: %s", bucket, policy)
   380  
   381  		_, err := s3conn.PutBucketPolicy(&s3.PutBucketPolicyInput{
   382  			Bucket: aws.String(bucket),
   383  			Policy: aws.String(policy),
   384  		})
   385  
   386  		if err != nil {
   387  			return fmt.Errorf("Error putting S3 policy: %s", err)
   388  		}
   389  	} else {
   390  		log.Printf("[DEBUG] S3 bucket: %s, delete policy: %s", bucket, policy)
   391  		_, err := s3conn.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{
   392  			Bucket: aws.String(bucket),
   393  		})
   394  
   395  		if err != nil {
   396  			return fmt.Errorf("Error deleting S3 policy: %s", err)
   397  		}
   398  	}
   399  
   400  	return nil
   401  }
   402  
   403  func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
   404  	ws := d.Get("website").([]interface{})
   405  
   406  	if len(ws) == 1 {
   407  		w := ws[0].(map[string]interface{})
   408  		return resourceAwsS3BucketWebsitePut(s3conn, d, w)
   409  	} else if len(ws) == 0 {
   410  		return resourceAwsS3BucketWebsiteDelete(s3conn, d)
   411  	} else {
   412  		return fmt.Errorf("Cannot specify more than one website.")
   413  	}
   414  }
   415  
   416  func resourceAwsS3BucketWebsitePut(s3conn *s3.S3, d *schema.ResourceData, website map[string]interface{}) error {
   417  	bucket := d.Get("bucket").(string)
   418  
   419  	indexDocument := website["index_document"].(string)
   420  	errorDocument := website["error_document"].(string)
   421  	redirectAllRequestsTo := website["redirect_all_requests_to"].(string)
   422  
   423  	if indexDocument == "" && redirectAllRequestsTo == "" {
   424  		return fmt.Errorf("Must specify either index_document or redirect_all_requests_to.")
   425  	}
   426  
   427  	websiteConfiguration := &s3.WebsiteConfiguration{}
   428  
   429  	if indexDocument != "" {
   430  		websiteConfiguration.IndexDocument = &s3.IndexDocument{Suffix: aws.String(indexDocument)}
   431  	}
   432  
   433  	if errorDocument != "" {
   434  		websiteConfiguration.ErrorDocument = &s3.ErrorDocument{Key: aws.String(errorDocument)}
   435  	}
   436  
   437  	if redirectAllRequestsTo != "" {
   438  		websiteConfiguration.RedirectAllRequestsTo = &s3.RedirectAllRequestsTo{HostName: aws.String(redirectAllRequestsTo)}
   439  	}
   440  
   441  	putInput := &s3.PutBucketWebsiteInput{
   442  		Bucket:               aws.String(bucket),
   443  		WebsiteConfiguration: websiteConfiguration,
   444  	}
   445  
   446  	log.Printf("[DEBUG] S3 put bucket website: %#v", putInput)
   447  
   448  	_, err := s3conn.PutBucketWebsite(putInput)
   449  	if err != nil {
   450  		return fmt.Errorf("Error putting S3 website: %s", err)
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  func resourceAwsS3BucketWebsiteDelete(s3conn *s3.S3, d *schema.ResourceData) error {
   457  	bucket := d.Get("bucket").(string)
   458  	deleteInput := &s3.DeleteBucketWebsiteInput{Bucket: aws.String(bucket)}
   459  
   460  	log.Printf("[DEBUG] S3 delete bucket website: %#v", deleteInput)
   461  
   462  	_, err := s3conn.DeleteBucketWebsite(deleteInput)
   463  	if err != nil {
   464  		return fmt.Errorf("Error deleting S3 website: %s", err)
   465  	}
   466  
   467  	d.Set("website_endpoint", "")
   468  	d.Set("website_domain", "")
   469  
   470  	return nil
   471  }
   472  
   473  func websiteEndpoint(s3conn *s3.S3, d *schema.ResourceData) (*S3Website, error) {
   474  	// If the bucket doesn't have a website configuration, return an empty
   475  	// endpoint
   476  	if _, ok := d.GetOk("website"); !ok {
   477  		return nil, nil
   478  	}
   479  
   480  	bucket := d.Get("bucket").(string)
   481  
   482  	// Lookup the region for this bucket
   483  	location, err := s3conn.GetBucketLocation(
   484  		&s3.GetBucketLocationInput{
   485  			Bucket: aws.String(bucket),
   486  		},
   487  	)
   488  	if err != nil {
   489  		return nil, err
   490  	}
   491  	var region string
   492  	if location.LocationConstraint != nil {
   493  		region = *location.LocationConstraint
   494  	}
   495  
   496  	return WebsiteEndpoint(bucket, region), nil
   497  }
   498  
   499  func WebsiteEndpoint(bucket string, region string) *S3Website {
   500  	domain := WebsiteDomainUrl(region)
   501  	return &S3Website{Endpoint: fmt.Sprintf("%s.%s", bucket, domain), Domain: domain}
   502  }
   503  
   504  func WebsiteDomainUrl(region string) string {
   505  	region = normalizeRegion(region)
   506  
   507  	// Frankfurt(and probably future) regions uses different syntax for website endpoints
   508  	// http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html
   509  	if region == "eu-central-1" {
   510  		return fmt.Sprintf("s3-website.%s.amazonaws.com", region)
   511  	}
   512  
   513  	return fmt.Sprintf("s3-website-%s.amazonaws.com", region)
   514  }
   515  
   516  func resourceAwsS3BucketVersioningUpdate(s3conn *s3.S3, d *schema.ResourceData) error {
   517  	v := d.Get("versioning").(*schema.Set).List()
   518  	bucket := d.Get("bucket").(string)
   519  	vc := &s3.VersioningConfiguration{}
   520  
   521  	if len(v) > 0 {
   522  		c := v[0].(map[string]interface{})
   523  
   524  		if c["enabled"].(bool) {
   525  			vc.Status = aws.String(s3.BucketVersioningStatusEnabled)
   526  		} else {
   527  			vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
   528  		}
   529  	} else {
   530  		vc.Status = aws.String(s3.BucketVersioningStatusSuspended)
   531  	}
   532  
   533  	i := &s3.PutBucketVersioningInput{
   534  		Bucket:                  aws.String(bucket),
   535  		VersioningConfiguration: vc,
   536  	}
   537  	log.Printf("[DEBUG] S3 put bucket versioning: %#v", i)
   538  
   539  	_, err := s3conn.PutBucketVersioning(i)
   540  	if err != nil {
   541  		return fmt.Errorf("Error putting S3 versioning: %s", err)
   542  	}
   543  
   544  	return nil
   545  }
   546  
   547  func normalizeJson(jsonString interface{}) string {
   548  	if jsonString == nil {
   549  		return ""
   550  	}
   551  	j := make(map[string]interface{})
   552  	err := json.Unmarshal([]byte(jsonString.(string)), &j)
   553  	if err != nil {
   554  		return fmt.Sprintf("Error parsing JSON: %s", err)
   555  	}
   556  	b, _ := json.Marshal(j)
   557  	return string(b[:])
   558  }
   559  
   560  func normalizeRegion(region string) string {
   561  	// Default to us-east-1 if the bucket doesn't have a region:
   562  	// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html
   563  	if region == "" {
   564  		region = "us-east-1"
   565  	}
   566  
   567  	return region
   568  }
   569  
   570  type S3Website struct {
   571  	Endpoint, Domain string
   572  }