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  }