mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-09 23:54:17 -06:00
99d8c2a3b3
This commit resolves the issue where lack of snapshot ID in the device mapping section of the API response to DescribeImagesResponse would cause Terraform to crash due to a nil pointer dereference. Usually, the snapshot ID is included, but in some unique cases (e.g. ECS-enabled AMI from Amazon available on the Market Place) a volume that is attached might not have it. The API documentation does not clearly define whether the snapshot ID either should be or must be included for any volume in the response. Signed-off-by: Krzysztof Wilczynski <krzysztof.wilczynski@linux.com>
525 lines
15 KiB
Go
525 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 {
|
|
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,
|
|
},
|
|
}
|
|
}
|