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 }