github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/resource_aws_ebs_volume.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"time"
     7  
     8  	"github.com/aws/aws-sdk-go/aws"
     9  	"github.com/aws/aws-sdk-go/aws/awserr"
    10  	"github.com/aws/aws-sdk-go/service/ec2"
    11  
    12  	"github.com/hashicorp/errwrap"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  )
    16  
    17  func resourceAwsEbsVolume() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsEbsVolumeCreate,
    20  		Read:   resourceAwsEbsVolumeRead,
    21  		Update: resourceAWSEbsVolumeUpdate,
    22  		Delete: resourceAwsEbsVolumeDelete,
    23  		Importer: &schema.ResourceImporter{
    24  			State: schema.ImportStatePassthrough,
    25  		},
    26  
    27  		Schema: map[string]*schema.Schema{
    28  			"availability_zone": {
    29  				Type:     schema.TypeString,
    30  				Required: true,
    31  				ForceNew: true,
    32  			},
    33  			"encrypted": {
    34  				Type:     schema.TypeBool,
    35  				Optional: true,
    36  				Computed: true,
    37  				ForceNew: true,
    38  			},
    39  			"iops": {
    40  				Type:     schema.TypeInt,
    41  				Optional: true,
    42  				Computed: true,
    43  			},
    44  			"kms_key_id": {
    45  				Type:         schema.TypeString,
    46  				Optional:     true,
    47  				Computed:     true,
    48  				ForceNew:     true,
    49  				ValidateFunc: validateArn,
    50  			},
    51  			"size": {
    52  				Type:     schema.TypeInt,
    53  				Optional: true,
    54  				Computed: true,
    55  			},
    56  			"snapshot_id": {
    57  				Type:     schema.TypeString,
    58  				Optional: true,
    59  				Computed: true,
    60  				ForceNew: true,
    61  			},
    62  			"type": {
    63  				Type:     schema.TypeString,
    64  				Optional: true,
    65  				Computed: true,
    66  			},
    67  			"tags": tagsSchema(),
    68  		},
    69  	}
    70  }
    71  
    72  func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error {
    73  	conn := meta.(*AWSClient).ec2conn
    74  
    75  	request := &ec2.CreateVolumeInput{
    76  		AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
    77  	}
    78  	if value, ok := d.GetOk("encrypted"); ok {
    79  		request.Encrypted = aws.Bool(value.(bool))
    80  	}
    81  	if value, ok := d.GetOk("kms_key_id"); ok {
    82  		request.KmsKeyId = aws.String(value.(string))
    83  	}
    84  	if value, ok := d.GetOk("size"); ok {
    85  		request.Size = aws.Int64(int64(value.(int)))
    86  	}
    87  	if value, ok := d.GetOk("snapshot_id"); ok {
    88  		request.SnapshotId = aws.String(value.(string))
    89  	}
    90  
    91  	// IOPs are only valid, and required for, storage type io1. The current minimu
    92  	// is 100. Instead of a hard validation we we only apply the IOPs to the
    93  	// request if the type is io1, and log a warning otherwise. This allows users
    94  	// to "disable" iops. See https://github.com/hashicorp/terraform/pull/4146
    95  	var t string
    96  	if value, ok := d.GetOk("type"); ok {
    97  		t = value.(string)
    98  		request.VolumeType = aws.String(t)
    99  	}
   100  
   101  	iops := d.Get("iops").(int)
   102  	if t != "io1" && iops > 0 {
   103  		log.Printf("[WARN] IOPs is only valid for storate type io1 for EBS Volumes")
   104  	} else if t == "io1" {
   105  		// We add the iops value without validating it's size, to allow AWS to
   106  		// enforce a size requirement (currently 100)
   107  		request.Iops = aws.Int64(int64(iops))
   108  	}
   109  
   110  	log.Printf(
   111  		"[DEBUG] EBS Volume create opts: %s", request)
   112  	result, err := conn.CreateVolume(request)
   113  	if err != nil {
   114  		return fmt.Errorf("Error creating EC2 volume: %s", err)
   115  	}
   116  
   117  	log.Println("[DEBUG] Waiting for Volume to become available")
   118  
   119  	stateConf := &resource.StateChangeConf{
   120  		Pending:    []string{"creating"},
   121  		Target:     []string{"available"},
   122  		Refresh:    volumeStateRefreshFunc(conn, *result.VolumeId),
   123  		Timeout:    5 * time.Minute,
   124  		Delay:      10 * time.Second,
   125  		MinTimeout: 3 * time.Second,
   126  	}
   127  
   128  	_, err = stateConf.WaitForState()
   129  	if err != nil {
   130  		return fmt.Errorf(
   131  			"Error waiting for Volume (%s) to become available: %s",
   132  			*result.VolumeId, err)
   133  	}
   134  
   135  	d.SetId(*result.VolumeId)
   136  
   137  	if _, ok := d.GetOk("tags"); ok {
   138  		if err := setTags(conn, d); err != nil {
   139  			return errwrap.Wrapf("Error setting tags for EBS Volume: {{err}}", err)
   140  		}
   141  	}
   142  
   143  	return readVolume(d, result)
   144  }
   145  
   146  func resourceAWSEbsVolumeUpdate(d *schema.ResourceData, meta interface{}) error {
   147  	conn := meta.(*AWSClient).ec2conn
   148  	if _, ok := d.GetOk("tags"); ok {
   149  		if err := setTags(conn, d); err != nil {
   150  			return errwrap.Wrapf("Error updating tags for EBS Volume: {{err}}", err)
   151  		}
   152  	}
   153  
   154  	requestUpdate := false
   155  	params := &ec2.ModifyVolumeInput{
   156  		VolumeId: aws.String(d.Id()),
   157  	}
   158  
   159  	if d.HasChange("size") {
   160  		requestUpdate = true
   161  		params.Size = aws.Int64(int64(d.Get("size").(int)))
   162  	}
   163  
   164  	if d.HasChange("type") {
   165  		requestUpdate = true
   166  		params.VolumeType = aws.String(d.Get("type").(string))
   167  	}
   168  
   169  	if d.HasChange("iops") {
   170  		requestUpdate = true
   171  		params.Iops = aws.Int64(int64(d.Get("iops").(int)))
   172  	}
   173  
   174  	if requestUpdate {
   175  		result, err := conn.ModifyVolume(params)
   176  		if err != nil {
   177  			return err
   178  		}
   179  
   180  		stateConf := &resource.StateChangeConf{
   181  			Pending:    []string{"creating", "modifying"},
   182  			Target:     []string{"available"},
   183  			Refresh:    volumeStateRefreshFunc(conn, *result.VolumeModification.VolumeId),
   184  			Timeout:    5 * time.Minute,
   185  			Delay:      10 * time.Second,
   186  			MinTimeout: 3 * time.Second,
   187  		}
   188  
   189  		_, err = stateConf.WaitForState()
   190  		if err != nil {
   191  			return fmt.Errorf(
   192  				"Error waiting for Volume (%s) to become available: %s",
   193  				*result.VolumeModification.VolumeId, err)
   194  		}
   195  	}
   196  
   197  	return resourceAwsEbsVolumeRead(d, meta)
   198  }
   199  
   200  // volumeStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   201  // a the state of a Volume. Returns successfully when volume is available
   202  func volumeStateRefreshFunc(conn *ec2.EC2, volumeID string) resource.StateRefreshFunc {
   203  	return func() (interface{}, string, error) {
   204  		resp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{
   205  			VolumeIds: []*string{aws.String(volumeID)},
   206  		})
   207  
   208  		if err != nil {
   209  			if ec2err, ok := err.(awserr.Error); ok {
   210  				// Set this to nil as if we didn't find anything.
   211  				log.Printf("Error on Volume State Refresh: message: \"%s\", code:\"%s\"", ec2err.Message(), ec2err.Code())
   212  				resp = nil
   213  				return nil, "", err
   214  			} else {
   215  				log.Printf("Error on Volume State Refresh: %s", err)
   216  				return nil, "", err
   217  			}
   218  		}
   219  
   220  		v := resp.Volumes[0]
   221  		return v, *v.State, nil
   222  	}
   223  }
   224  
   225  func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error {
   226  	conn := meta.(*AWSClient).ec2conn
   227  
   228  	request := &ec2.DescribeVolumesInput{
   229  		VolumeIds: []*string{aws.String(d.Id())},
   230  	}
   231  
   232  	response, err := conn.DescribeVolumes(request)
   233  	if err != nil {
   234  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVolume.NotFound" {
   235  			d.SetId("")
   236  			return nil
   237  		}
   238  		return fmt.Errorf("Error reading EC2 volume %s: %s", d.Id(), err)
   239  	}
   240  
   241  	return readVolume(d, response.Volumes[0])
   242  }
   243  
   244  func resourceAwsEbsVolumeDelete(d *schema.ResourceData, meta interface{}) error {
   245  	conn := meta.(*AWSClient).ec2conn
   246  
   247  	return resource.Retry(5*time.Minute, func() *resource.RetryError {
   248  		request := &ec2.DeleteVolumeInput{
   249  			VolumeId: aws.String(d.Id()),
   250  		}
   251  		_, err := conn.DeleteVolume(request)
   252  		if err == nil {
   253  			return nil
   254  		}
   255  
   256  		ebsErr, ok := err.(awserr.Error)
   257  		if ebsErr.Code() == "VolumeInUse" {
   258  			return resource.RetryableError(fmt.Errorf("EBS VolumeInUse - trying again while it detaches"))
   259  		}
   260  
   261  		if !ok {
   262  			return resource.NonRetryableError(err)
   263  		}
   264  
   265  		return resource.NonRetryableError(err)
   266  	})
   267  
   268  }
   269  
   270  func readVolume(d *schema.ResourceData, volume *ec2.Volume) error {
   271  	d.SetId(*volume.VolumeId)
   272  
   273  	d.Set("availability_zone", *volume.AvailabilityZone)
   274  	if volume.Encrypted != nil {
   275  		d.Set("encrypted", *volume.Encrypted)
   276  	}
   277  	if volume.KmsKeyId != nil {
   278  		d.Set("kms_key_id", *volume.KmsKeyId)
   279  	}
   280  	if volume.Size != nil {
   281  		d.Set("size", *volume.Size)
   282  	}
   283  	if volume.SnapshotId != nil {
   284  		d.Set("snapshot_id", *volume.SnapshotId)
   285  	}
   286  	if volume.VolumeType != nil {
   287  		d.Set("type", *volume.VolumeType)
   288  	}
   289  
   290  	if volume.VolumeType != nil && *volume.VolumeType == "io1" {
   291  		// Only set the iops attribute if the volume type is io1. Setting otherwise
   292  		// can trigger a refresh/plan loop based on the computed value that is given
   293  		// from AWS, and prevent us from specifying 0 as a valid iops.
   294  		//   See https://github.com/hashicorp/terraform/pull/4146
   295  		if volume.Iops != nil {
   296  			d.Set("iops", *volume.Iops)
   297  		}
   298  	}
   299  
   300  	if volume.Tags != nil {
   301  		d.Set("tags", tagsToMap(volume.Tags))
   302  	}
   303  
   304  	return nil
   305  }