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 }