opentofu/builtin/providers/aws/resource_aws_ami.go
Martin Atkins ea0bc04277 provider/aws: aws_ami: handle deletion of AMIs (#9721)
Previously this resource (and, by extension, the aws_ami_copy and
aws_ami_from_instance resources that share much of its implementation)
was handling correctly the case where an AMI had been recently
deregistered, and was thus still returned from the API, but not correctly
dealing with the situation where the AMI has been removed altogether.

Now we additionally handle the NotFound error returned by the API when
we request a non-existent AMI, and remove the AMI from the state in the
same way we do for deregistered AMIs.
2016-10-31 09:51:59 +00:00

531 lines
15 KiB
Go

package aws
import (
"bytes"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAmi() *schema.Resource {
// Our schema is shared also with aws_ami_copy and aws_ami_from_instance
resourceSchema := resourceAwsAmiCommonSchema(false)
return &schema.Resource{
Create: resourceAwsAmiCreate,
Schema: resourceSchema,
// The Read, Update and Delete operations are shared with aws_ami_copy
// and aws_ami_from_instance, since they differ only in how the image
// is created.
Read: resourceAwsAmiRead,
Update: resourceAwsAmiUpdate,
Delete: resourceAwsAmiDelete,
}
}
func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.RegisterImageInput{
Name: aws.String(d.Get("name").(string)),
Description: aws.String(d.Get("description").(string)),
Architecture: aws.String(d.Get("architecture").(string)),
ImageLocation: aws.String(d.Get("image_location").(string)),
RootDeviceName: aws.String(d.Get("root_device_name").(string)),
SriovNetSupport: aws.String(d.Get("sriov_net_support").(string)),
VirtualizationType: aws.String(d.Get("virtualization_type").(string)),
}
if kernelId := d.Get("kernel_id").(string); kernelId != "" {
req.KernelId = aws.String(kernelId)
}
if ramdiskId := d.Get("ramdisk_id").(string); ramdiskId != "" {
req.RamdiskId = aws.String(ramdiskId)
}
ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set)
ephemeralBlockDevsSet := d.Get("ephemeral_block_device").(*schema.Set)
for _, ebsBlockDevI := range ebsBlockDevsSet.List() {
ebsBlockDev := ebsBlockDevI.(map[string]interface{})
blockDev := &ec2.BlockDeviceMapping{
DeviceName: aws.String(ebsBlockDev["device_name"].(string)),
Ebs: &ec2.EbsBlockDevice{
DeleteOnTermination: aws.Bool(ebsBlockDev["delete_on_termination"].(bool)),
VolumeSize: aws.Int64(int64(ebsBlockDev["volume_size"].(int))),
VolumeType: aws.String(ebsBlockDev["volume_type"].(string)),
},
}
if iops := ebsBlockDev["iops"].(int); iops != 0 {
blockDev.Ebs.Iops = aws.Int64(int64(iops))
}
encrypted := ebsBlockDev["encrypted"].(bool)
if snapshotId := ebsBlockDev["snapshot_id"].(string); snapshotId != "" {
blockDev.Ebs.SnapshotId = aws.String(snapshotId)
if encrypted {
return errors.New("can't set both 'snapshot_id' and 'encrypted'")
}
} else if encrypted {
blockDev.Ebs.Encrypted = aws.Bool(true)
}
req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev)
}
for _, ephemeralBlockDevI := range ephemeralBlockDevsSet.List() {
ephemeralBlockDev := ephemeralBlockDevI.(map[string]interface{})
blockDev := &ec2.BlockDeviceMapping{
DeviceName: aws.String(ephemeralBlockDev["device_name"].(string)),
VirtualName: aws.String(ephemeralBlockDev["virtual_name"].(string)),
}
req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev)
}
res, err := client.RegisterImage(req)
if err != nil {
return err
}
id := *res.ImageId
d.SetId(id)
d.Partial(true) // make sure we record the id even if the rest of this gets interrupted
d.Set("id", id)
d.Set("manage_ebs_block_devices", false)
d.SetPartial("id")
d.SetPartial("manage_ebs_block_devices")
d.Partial(false)
_, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
return resourceAwsAmiUpdate(d, meta)
}
func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
id := d.Id()
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(id)},
}
res, err := client.DescribeImages(req)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" {
log.Printf("[DEBUG] %s no longer exists, so we'll drop it from the state", id)
d.SetId("")
return nil
}
return err
}
if len(res.Images) != 1 {
d.SetId("")
return nil
}
image := res.Images[0]
state := *image.State
if state == "pending" {
// This could happen if a user manually adds an image we didn't create
// to the state. We'll wait for the image to become available
// before we continue. We should never take this branch in normal
// circumstances since we would've waited for availability during
// the "Create" step.
image, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
state = *image.State
}
if state == "deregistered" {
d.SetId("")
return nil
}
if state != "available" {
return fmt.Errorf("AMI has become %s", state)
}
d.Set("name", image.Name)
d.Set("description", image.Description)
d.Set("image_location", image.ImageLocation)
d.Set("architecture", image.Architecture)
d.Set("kernel_id", image.KernelId)
d.Set("ramdisk_id", image.RamdiskId)
d.Set("root_device_name", image.RootDeviceName)
d.Set("sriov_net_support", image.SriovNetSupport)
d.Set("virtualization_type", image.VirtualizationType)
var ebsBlockDevs []map[string]interface{}
var ephemeralBlockDevs []map[string]interface{}
for _, blockDev := range image.BlockDeviceMappings {
if blockDev.Ebs != nil {
ebsBlockDev := map[string]interface{}{
"device_name": *blockDev.DeviceName,
"delete_on_termination": *blockDev.Ebs.DeleteOnTermination,
"encrypted": *blockDev.Ebs.Encrypted,
"iops": 0,
"volume_size": int(*blockDev.Ebs.VolumeSize),
"volume_type": *blockDev.Ebs.VolumeType,
}
if blockDev.Ebs.Iops != nil {
ebsBlockDev["iops"] = int(*blockDev.Ebs.Iops)
}
// The snapshot ID might not be set.
if blockDev.Ebs.SnapshotId != nil {
ebsBlockDev["snapshot_id"] = *blockDev.Ebs.SnapshotId
}
ebsBlockDevs = append(ebsBlockDevs, ebsBlockDev)
} else {
ephemeralBlockDevs = append(ephemeralBlockDevs, map[string]interface{}{
"device_name": *blockDev.DeviceName,
"virtual_name": *blockDev.VirtualName,
})
}
}
d.Set("ebs_block_device", ebsBlockDevs)
d.Set("ephemeral_block_device", ephemeralBlockDevs)
d.Set("tags", tagsToMap(image.Tags))
return nil
}
func resourceAwsAmiUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
d.Partial(true)
if err := setTags(client, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
if d.Get("description").(string) != "" {
_, err := client.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{
ImageId: aws.String(d.Id()),
Description: &ec2.AttributeValue{
Value: aws.String(d.Get("description").(string)),
},
})
if err != nil {
return err
}
d.SetPartial("description")
}
d.Partial(false)
return resourceAwsAmiRead(d, meta)
}
func resourceAwsAmiDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.DeregisterImageInput{
ImageId: aws.String(d.Id()),
}
_, err := client.DeregisterImage(req)
if err != nil {
return err
}
// If we're managing the EBS snapshots then we need to delete those too.
if d.Get("manage_ebs_snapshots").(bool) {
errs := map[string]error{}
ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set)
req := &ec2.DeleteSnapshotInput{}
for _, ebsBlockDevI := range ebsBlockDevsSet.List() {
ebsBlockDev := ebsBlockDevI.(map[string]interface{})
snapshotId := ebsBlockDev["snapshot_id"].(string)
if snapshotId != "" {
req.SnapshotId = aws.String(snapshotId)
_, err := client.DeleteSnapshot(req)
if err != nil {
errs[snapshotId] = err
}
}
}
if len(errs) > 0 {
errParts := []string{"Errors while deleting associated EBS snapshots:"}
for snapshotId, err := range errs {
errParts = append(errParts, fmt.Sprintf("%s: %s", snapshotId, err))
}
errParts = append(errParts, "These are no longer managed by Terraform and must be deleted manually.")
return errors.New(strings.Join(errParts, "\n"))
}
}
d.SetId("")
return nil
}
func resourceAwsAmiWaitForAvailable(id string, client *ec2.EC2) (*ec2.Image, error) {
log.Printf("Waiting for AMI %s to become available...", id)
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(id)},
}
pollsWhereNotFound := 0
for {
res, err := client.DescribeImages(req)
if err != nil {
// When using RegisterImage (for aws_ami) the AMI sometimes isn't available at all
// right after the API responds, so we need to tolerate a couple Not Found errors
// before an available AMI shows up.
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" {
pollsWhereNotFound++
// We arbitrarily stop polling after getting a "not found" error five times,
// assuming that the AMI has been deleted by something other than Terraform.
if pollsWhereNotFound > 5 {
return nil, fmt.Errorf("gave up waiting for AMI to be created: %s", err)
}
time.Sleep(4 * time.Second)
continue
}
return nil, fmt.Errorf("error reading AMI: %s", err)
}
if len(res.Images) != 1 {
return nil, fmt.Errorf("new AMI vanished while pending")
}
state := *res.Images[0].State
if state == "pending" {
// Give it a few seconds before we poll again.
time.Sleep(4 * time.Second)
continue
}
if state == "available" {
// We're done!
return res.Images[0], nil
}
// If we're not pending or available then we're in one of the invalid/error
// states, so stop polling and bail out.
stateReason := *res.Images[0].StateReason
return nil, fmt.Errorf("new AMI became %s while pending: %s", state, stateReason)
}
}
func resourceAwsAmiCommonSchema(computed bool) map[string]*schema.Schema {
// The "computed" parameter controls whether we're making
// a schema for an AMI that's been implicitly registered (aws_ami_copy, aws_ami_from_instance)
// or whether we're making a schema for an explicit registration (aws_ami).
// When set, almost every attribute is marked as "computed".
// When not set, only the "id" attribute is computed.
// "name" and "description" are never computed, since they must always
// be provided by the user.
var virtualizationTypeDefault interface{}
var deleteEbsOnTerminationDefault interface{}
var sriovNetSupportDefault interface{}
var architectureDefault interface{}
var volumeTypeDefault interface{}
if !computed {
virtualizationTypeDefault = "paravirtual"
deleteEbsOnTerminationDefault = true
sriovNetSupportDefault = "simple"
architectureDefault = "x86_64"
volumeTypeDefault = "standard"
}
return map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"image_location": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: true,
ForceNew: !computed,
},
"architecture": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: architectureDefault,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"kernel_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"ramdisk_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"root_device_name": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"sriov_net_support": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: sriovNetSupportDefault,
},
"virtualization_type": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: virtualizationTypeDefault,
},
// The following block device attributes intentionally mimick the
// corresponding attributes on aws_instance, since they have the
// same meaning.
// However, we don't use root_block_device here because the constraint
// on which root device attributes can be overridden for an instance to
// not apply when registering an AMI.
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: !computed,
Default: deleteEbsOnTerminationDefault,
ForceNew: !computed,
Computed: computed,
},
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: !computed,
ForceNew: !computed,
Computed: computed,
},
"encrypted": &schema.Schema{
Type: schema.TypeBool,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: !computed,
Computed: true,
ForceNew: !computed,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: volumeTypeDefault,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
return hashcode.String(buf.String())
},
},
"ephemeral_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: !computed,
Computed: computed,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Required: !computed,
Computed: computed,
},
},
},
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
return hashcode.String(buf.String())
},
},
"tags": tagsSchema(),
// Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance
// resources record that they implicitly created new EBS snapshots that we should
// now manage. Not set by aws_ami, since the snapshots used there are presumed to
// be independently managed.
"manage_ebs_snapshots": &schema.Schema{
Type: schema.TypeBool,
Computed: true,
ForceNew: true,
},
}
}