github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/resource_aws_ami.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "log" 8 "strings" 9 "time" 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/ec2" 14 15 "github.com/hashicorp/terraform/helper/hashcode" 16 "github.com/hashicorp/terraform/helper/resource" 17 "github.com/hashicorp/terraform/helper/schema" 18 ) 19 20 const ( 21 AWSAMIRetryTimeout = 40 * time.Minute 22 AWSAMIDeleteRetryTimeout = 90 * time.Minute 23 AWSAMIRetryDelay = 5 * time.Second 24 AWSAMIRetryMinTimeout = 3 * time.Second 25 ) 26 27 func resourceAwsAmi() *schema.Resource { 28 // Our schema is shared also with aws_ami_copy and aws_ami_from_instance 29 resourceSchema := resourceAwsAmiCommonSchema(false) 30 31 return &schema.Resource{ 32 Create: resourceAwsAmiCreate, 33 34 Schema: resourceSchema, 35 36 // The Read, Update and Delete operations are shared with aws_ami_copy 37 // and aws_ami_from_instance, since they differ only in how the image 38 // is created. 39 Read: resourceAwsAmiRead, 40 Update: resourceAwsAmiUpdate, 41 Delete: resourceAwsAmiDelete, 42 } 43 } 44 45 func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { 46 client := meta.(*AWSClient).ec2conn 47 48 req := &ec2.RegisterImageInput{ 49 Name: aws.String(d.Get("name").(string)), 50 Description: aws.String(d.Get("description").(string)), 51 Architecture: aws.String(d.Get("architecture").(string)), 52 ImageLocation: aws.String(d.Get("image_location").(string)), 53 RootDeviceName: aws.String(d.Get("root_device_name").(string)), 54 SriovNetSupport: aws.String(d.Get("sriov_net_support").(string)), 55 VirtualizationType: aws.String(d.Get("virtualization_type").(string)), 56 } 57 58 if kernelId := d.Get("kernel_id").(string); kernelId != "" { 59 req.KernelId = aws.String(kernelId) 60 } 61 if ramdiskId := d.Get("ramdisk_id").(string); ramdiskId != "" { 62 req.RamdiskId = aws.String(ramdiskId) 63 } 64 65 ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set) 66 ephemeralBlockDevsSet := d.Get("ephemeral_block_device").(*schema.Set) 67 for _, ebsBlockDevI := range ebsBlockDevsSet.List() { 68 ebsBlockDev := ebsBlockDevI.(map[string]interface{}) 69 blockDev := &ec2.BlockDeviceMapping{ 70 DeviceName: aws.String(ebsBlockDev["device_name"].(string)), 71 Ebs: &ec2.EbsBlockDevice{ 72 DeleteOnTermination: aws.Bool(ebsBlockDev["delete_on_termination"].(bool)), 73 VolumeType: aws.String(ebsBlockDev["volume_type"].(string)), 74 }, 75 } 76 if iops, ok := ebsBlockDev["iops"]; ok { 77 if iop := iops.(int); iop != 0 { 78 blockDev.Ebs.Iops = aws.Int64(int64(iop)) 79 } 80 } 81 if size, ok := ebsBlockDev["volume_size"]; ok { 82 if s := size.(int); s != 0 { 83 blockDev.Ebs.VolumeSize = aws.Int64(int64(s)) 84 } 85 } 86 encrypted := ebsBlockDev["encrypted"].(bool) 87 if snapshotId := ebsBlockDev["snapshot_id"].(string); snapshotId != "" { 88 blockDev.Ebs.SnapshotId = aws.String(snapshotId) 89 if encrypted { 90 return errors.New("can't set both 'snapshot_id' and 'encrypted'") 91 } 92 } else if encrypted { 93 blockDev.Ebs.Encrypted = aws.Bool(true) 94 } 95 req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) 96 } 97 for _, ephemeralBlockDevI := range ephemeralBlockDevsSet.List() { 98 ephemeralBlockDev := ephemeralBlockDevI.(map[string]interface{}) 99 blockDev := &ec2.BlockDeviceMapping{ 100 DeviceName: aws.String(ephemeralBlockDev["device_name"].(string)), 101 VirtualName: aws.String(ephemeralBlockDev["virtual_name"].(string)), 102 } 103 req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) 104 } 105 106 res, err := client.RegisterImage(req) 107 if err != nil { 108 return err 109 } 110 111 id := *res.ImageId 112 d.SetId(id) 113 d.Partial(true) // make sure we record the id even if the rest of this gets interrupted 114 d.Set("id", id) 115 d.Set("manage_ebs_block_devices", false) 116 d.SetPartial("id") 117 d.SetPartial("manage_ebs_block_devices") 118 d.Partial(false) 119 120 _, err = resourceAwsAmiWaitForAvailable(id, client) 121 if err != nil { 122 return err 123 } 124 125 return resourceAwsAmiUpdate(d, meta) 126 } 127 128 func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { 129 client := meta.(*AWSClient).ec2conn 130 id := d.Id() 131 132 req := &ec2.DescribeImagesInput{ 133 ImageIds: []*string{aws.String(id)}, 134 } 135 136 res, err := client.DescribeImages(req) 137 if err != nil { 138 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" { 139 log.Printf("[DEBUG] %s no longer exists, so we'll drop it from the state", id) 140 d.SetId("") 141 return nil 142 } 143 144 return err 145 } 146 147 if len(res.Images) != 1 { 148 d.SetId("") 149 return nil 150 } 151 152 image := res.Images[0] 153 state := *image.State 154 155 if state == "pending" { 156 // This could happen if a user manually adds an image we didn't create 157 // to the state. We'll wait for the image to become available 158 // before we continue. We should never take this branch in normal 159 // circumstances since we would've waited for availability during 160 // the "Create" step. 161 image, err = resourceAwsAmiWaitForAvailable(id, client) 162 if err != nil { 163 return err 164 } 165 state = *image.State 166 } 167 168 if state == "deregistered" { 169 d.SetId("") 170 return nil 171 } 172 173 if state != "available" { 174 return fmt.Errorf("AMI has become %s", state) 175 } 176 177 d.Set("name", image.Name) 178 d.Set("description", image.Description) 179 d.Set("image_location", image.ImageLocation) 180 d.Set("architecture", image.Architecture) 181 d.Set("kernel_id", image.KernelId) 182 d.Set("ramdisk_id", image.RamdiskId) 183 d.Set("root_device_name", image.RootDeviceName) 184 d.Set("sriov_net_support", image.SriovNetSupport) 185 d.Set("virtualization_type", image.VirtualizationType) 186 187 var ebsBlockDevs []map[string]interface{} 188 var ephemeralBlockDevs []map[string]interface{} 189 190 for _, blockDev := range image.BlockDeviceMappings { 191 if blockDev.Ebs != nil { 192 ebsBlockDev := map[string]interface{}{ 193 "device_name": *blockDev.DeviceName, 194 "delete_on_termination": *blockDev.Ebs.DeleteOnTermination, 195 "encrypted": *blockDev.Ebs.Encrypted, 196 "iops": 0, 197 "volume_size": int(*blockDev.Ebs.VolumeSize), 198 "volume_type": *blockDev.Ebs.VolumeType, 199 } 200 if blockDev.Ebs.Iops != nil { 201 ebsBlockDev["iops"] = int(*blockDev.Ebs.Iops) 202 } 203 // The snapshot ID might not be set. 204 if blockDev.Ebs.SnapshotId != nil { 205 ebsBlockDev["snapshot_id"] = *blockDev.Ebs.SnapshotId 206 } 207 ebsBlockDevs = append(ebsBlockDevs, ebsBlockDev) 208 } else { 209 ephemeralBlockDevs = append(ephemeralBlockDevs, map[string]interface{}{ 210 "device_name": *blockDev.DeviceName, 211 "virtual_name": *blockDev.VirtualName, 212 }) 213 } 214 } 215 216 d.Set("ebs_block_device", ebsBlockDevs) 217 d.Set("ephemeral_block_device", ephemeralBlockDevs) 218 219 d.Set("tags", tagsToMap(image.Tags)) 220 221 return nil 222 } 223 224 func resourceAwsAmiUpdate(d *schema.ResourceData, meta interface{}) error { 225 client := meta.(*AWSClient).ec2conn 226 227 d.Partial(true) 228 229 if err := setTags(client, d); err != nil { 230 return err 231 } else { 232 d.SetPartial("tags") 233 } 234 235 if d.Get("description").(string) != "" { 236 _, err := client.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{ 237 ImageId: aws.String(d.Id()), 238 Description: &ec2.AttributeValue{ 239 Value: aws.String(d.Get("description").(string)), 240 }, 241 }) 242 if err != nil { 243 return err 244 } 245 d.SetPartial("description") 246 } 247 248 d.Partial(false) 249 250 return resourceAwsAmiRead(d, meta) 251 } 252 253 func resourceAwsAmiDelete(d *schema.ResourceData, meta interface{}) error { 254 client := meta.(*AWSClient).ec2conn 255 256 req := &ec2.DeregisterImageInput{ 257 ImageId: aws.String(d.Id()), 258 } 259 260 _, err := client.DeregisterImage(req) 261 if err != nil { 262 return err 263 } 264 265 // If we're managing the EBS snapshots then we need to delete those too. 266 if d.Get("manage_ebs_snapshots").(bool) { 267 errs := map[string]error{} 268 ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set) 269 req := &ec2.DeleteSnapshotInput{} 270 for _, ebsBlockDevI := range ebsBlockDevsSet.List() { 271 ebsBlockDev := ebsBlockDevI.(map[string]interface{}) 272 snapshotId := ebsBlockDev["snapshot_id"].(string) 273 if snapshotId != "" { 274 req.SnapshotId = aws.String(snapshotId) 275 _, err := client.DeleteSnapshot(req) 276 if err != nil { 277 errs[snapshotId] = err 278 } 279 } 280 } 281 282 if len(errs) > 0 { 283 errParts := []string{"Errors while deleting associated EBS snapshots:"} 284 for snapshotId, err := range errs { 285 errParts = append(errParts, fmt.Sprintf("%s: %s", snapshotId, err)) 286 } 287 errParts = append(errParts, "These are no longer managed by Terraform and must be deleted manually.") 288 return errors.New(strings.Join(errParts, "\n")) 289 } 290 } 291 292 // Verify that the image is actually removed, if not we need to wait for it to be removed 293 if err := resourceAwsAmiWaitForDestroy(d.Id(), client); err != nil { 294 return err 295 } 296 297 // No error, ami was deleted successfully 298 d.SetId("") 299 return nil 300 } 301 302 func AMIStateRefreshFunc(client *ec2.EC2, id string) resource.StateRefreshFunc { 303 return func() (interface{}, string, error) { 304 emptyResp := &ec2.DescribeImagesOutput{} 305 306 resp, err := client.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{aws.String(id)}}) 307 if err != nil { 308 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" { 309 return emptyResp, "destroyed", nil 310 } else if resp != nil && len(resp.Images) == 0 { 311 return emptyResp, "destroyed", nil 312 } else { 313 return emptyResp, "", fmt.Errorf("Error on refresh: %+v", err) 314 } 315 } 316 317 if resp == nil || resp.Images == nil || len(resp.Images) == 0 { 318 return emptyResp, "destroyed", nil 319 } 320 321 // AMI is valid, so return it's state 322 return resp.Images[0], *resp.Images[0].State, nil 323 } 324 } 325 326 func resourceAwsAmiWaitForDestroy(id string, client *ec2.EC2) error { 327 log.Printf("Waiting for AMI %s to be deleted...", id) 328 329 stateConf := &resource.StateChangeConf{ 330 Pending: []string{"available", "pending", "failed"}, 331 Target: []string{"destroyed"}, 332 Refresh: AMIStateRefreshFunc(client, id), 333 Timeout: AWSAMIDeleteRetryTimeout, 334 Delay: AWSAMIRetryDelay, 335 MinTimeout: AWSAMIRetryTimeout, 336 } 337 338 _, err := stateConf.WaitForState() 339 if err != nil { 340 return fmt.Errorf("Error waiting for AMI (%s) to be deleted: %v", id, err) 341 } 342 343 return nil 344 } 345 346 func resourceAwsAmiWaitForAvailable(id string, client *ec2.EC2) (*ec2.Image, error) { 347 log.Printf("Waiting for AMI %s to become available...", id) 348 349 stateConf := &resource.StateChangeConf{ 350 Pending: []string{"pending"}, 351 Target: []string{"available"}, 352 Refresh: AMIStateRefreshFunc(client, id), 353 Timeout: AWSAMIRetryTimeout, 354 Delay: AWSAMIRetryDelay, 355 MinTimeout: AWSAMIRetryMinTimeout, 356 } 357 358 info, err := stateConf.WaitForState() 359 if err != nil { 360 return nil, fmt.Errorf("Error waiting for AMI (%s) to be ready: %v", id, err) 361 } 362 return info.(*ec2.Image), nil 363 } 364 365 func resourceAwsAmiCommonSchema(computed bool) map[string]*schema.Schema { 366 // The "computed" parameter controls whether we're making 367 // a schema for an AMI that's been implicitly registered (aws_ami_copy, aws_ami_from_instance) 368 // or whether we're making a schema for an explicit registration (aws_ami). 369 // When set, almost every attribute is marked as "computed". 370 // When not set, only the "id" attribute is computed. 371 // "name" and "description" are never computed, since they must always 372 // be provided by the user. 373 374 var virtualizationTypeDefault interface{} 375 var deleteEbsOnTerminationDefault interface{} 376 var sriovNetSupportDefault interface{} 377 var architectureDefault interface{} 378 var volumeTypeDefault interface{} 379 if !computed { 380 virtualizationTypeDefault = "paravirtual" 381 deleteEbsOnTerminationDefault = true 382 sriovNetSupportDefault = "simple" 383 architectureDefault = "x86_64" 384 volumeTypeDefault = "standard" 385 } 386 387 return map[string]*schema.Schema{ 388 "id": { 389 Type: schema.TypeString, 390 Computed: true, 391 }, 392 "image_location": { 393 Type: schema.TypeString, 394 Optional: !computed, 395 Computed: true, 396 ForceNew: !computed, 397 }, 398 "architecture": { 399 Type: schema.TypeString, 400 Optional: !computed, 401 Computed: computed, 402 ForceNew: !computed, 403 Default: architectureDefault, 404 }, 405 "description": { 406 Type: schema.TypeString, 407 Optional: true, 408 }, 409 "kernel_id": { 410 Type: schema.TypeString, 411 Optional: !computed, 412 Computed: computed, 413 ForceNew: !computed, 414 }, 415 "name": { 416 Type: schema.TypeString, 417 Required: true, 418 ForceNew: true, 419 }, 420 "ramdisk_id": { 421 Type: schema.TypeString, 422 Optional: !computed, 423 Computed: computed, 424 ForceNew: !computed, 425 }, 426 "root_device_name": { 427 Type: schema.TypeString, 428 Optional: !computed, 429 Computed: computed, 430 ForceNew: !computed, 431 }, 432 "sriov_net_support": { 433 Type: schema.TypeString, 434 Optional: !computed, 435 Computed: computed, 436 ForceNew: !computed, 437 Default: sriovNetSupportDefault, 438 }, 439 "virtualization_type": { 440 Type: schema.TypeString, 441 Optional: !computed, 442 Computed: computed, 443 ForceNew: !computed, 444 Default: virtualizationTypeDefault, 445 }, 446 447 // The following block device attributes intentionally mimick the 448 // corresponding attributes on aws_instance, since they have the 449 // same meaning. 450 // However, we don't use root_block_device here because the constraint 451 // on which root device attributes can be overridden for an instance to 452 // not apply when registering an AMI. 453 454 "ebs_block_device": { 455 Type: schema.TypeSet, 456 Optional: true, 457 Computed: true, 458 Elem: &schema.Resource{ 459 Schema: map[string]*schema.Schema{ 460 "delete_on_termination": { 461 Type: schema.TypeBool, 462 Optional: !computed, 463 Default: deleteEbsOnTerminationDefault, 464 ForceNew: !computed, 465 Computed: computed, 466 }, 467 468 "device_name": { 469 Type: schema.TypeString, 470 Required: !computed, 471 ForceNew: !computed, 472 Computed: computed, 473 }, 474 475 "encrypted": { 476 Type: schema.TypeBool, 477 Optional: !computed, 478 Computed: computed, 479 ForceNew: !computed, 480 }, 481 482 "iops": { 483 Type: schema.TypeInt, 484 Optional: !computed, 485 Computed: computed, 486 ForceNew: !computed, 487 }, 488 489 "snapshot_id": { 490 Type: schema.TypeString, 491 Optional: !computed, 492 Computed: computed, 493 ForceNew: !computed, 494 }, 495 496 "volume_size": { 497 Type: schema.TypeInt, 498 Optional: !computed, 499 Computed: true, 500 ForceNew: !computed, 501 }, 502 503 "volume_type": { 504 Type: schema.TypeString, 505 Optional: !computed, 506 Computed: computed, 507 ForceNew: !computed, 508 Default: volumeTypeDefault, 509 }, 510 }, 511 }, 512 Set: func(v interface{}) int { 513 var buf bytes.Buffer 514 m := v.(map[string]interface{}) 515 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 516 buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) 517 return hashcode.String(buf.String()) 518 }, 519 }, 520 521 "ephemeral_block_device": { 522 Type: schema.TypeSet, 523 Optional: true, 524 Computed: true, 525 ForceNew: true, 526 Elem: &schema.Resource{ 527 Schema: map[string]*schema.Schema{ 528 "device_name": { 529 Type: schema.TypeString, 530 Required: !computed, 531 Computed: computed, 532 }, 533 534 "virtual_name": { 535 Type: schema.TypeString, 536 Required: !computed, 537 Computed: computed, 538 }, 539 }, 540 }, 541 Set: func(v interface{}) int { 542 var buf bytes.Buffer 543 m := v.(map[string]interface{}) 544 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 545 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 546 return hashcode.String(buf.String()) 547 }, 548 }, 549 550 "tags": tagsSchema(), 551 552 // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance 553 // resources record that they implicitly created new EBS snapshots that we should 554 // now manage. Not set by aws_ami, since the snapshots used there are presumed to 555 // be independently managed. 556 "manage_ebs_snapshots": { 557 Type: schema.TypeBool, 558 Computed: true, 559 ForceNew: true, 560 }, 561 } 562 }