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