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 }