mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-20 11:48:24 -06:00
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.
531 lines
15 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|