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