github.com/hobbeswalsh/terraform@v0.3.7-0.20150619183303-ad17cf55a0fa/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 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 437 // Frankfurt(and probably future) regions uses different syntax for website endpoints 438 // http://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteEndpoints.html 439 if region == "eu-central-1" { 440 return fmt.Sprintf("%s.s3-website.%s.amazonaws.com", bucket, region) 441 } 442 443 return fmt.Sprintf("%s.s3-website-%s.amazonaws.com", bucket, region) 444 } 445 446 func normalizeJson(jsonString interface{}) string { 447 if jsonString == nil { 448 return "" 449 } 450 j := make(map[string]interface{}) 451 err := json.Unmarshal([]byte(jsonString.(string)), &j) 452 if err != nil { 453 return fmt.Sprintf("Error parsing JSON: %s", err) 454 } 455 b, _ := json.Marshal(j) 456 return string(b[:]) 457 } 458 459 func normalizeRegion(region string) string { 460 // Default to us-east-1 if the bucket doesn't have a region: 461 // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html 462 if region == "" { 463 region = "us-east-1" 464 } 465 466 return region 467 }