Merge pull request #2050 from hashicorp/f-aws-volume-attachment
provider/aws: Add resource_aws_volume_attachment
This commit is contained in:
commit
440537a6cf
|
@ -127,6 +127,7 @@ func Provider() terraform.ResourceProvider {
|
|||
"aws_sns_topic": resourceAwsSnsTopic(),
|
||||
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
|
||||
"aws_subnet": resourceAwsSubnet(),
|
||||
"aws_volume_attachment": resourceAwsVolumeAttachment(),
|
||||
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
|
||||
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),
|
||||
"aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(),
|
||||
|
|
|
@ -2,11 +2,14 @@ package aws
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/awslabs/aws-sdk-go/aws"
|
||||
"github.com/awslabs/aws-sdk-go/aws/awserr"
|
||||
"github.com/awslabs/aws-sdk-go/service/ec2"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
|
@ -91,9 +94,55 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error
|
|||
if err != nil {
|
||||
return fmt.Errorf("Error creating EC2 volume: %s", err)
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[DEBUG] Waiting for Volume (%s) to become available",
|
||||
d.Id())
|
||||
|
||||
stateConf := &resource.StateChangeConf{
|
||||
Pending: []string{"creating"},
|
||||
Target: "available",
|
||||
Refresh: volumeStateRefreshFunc(conn, *result.VolumeID),
|
||||
Timeout: 5 * time.Minute,
|
||||
Delay: 10 * time.Second,
|
||||
MinTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
_, err = stateConf.WaitForState()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"Error waiting for Volume (%s) to become available: %s",
|
||||
*result.VolumeID, err)
|
||||
}
|
||||
|
||||
return readVolume(d, result)
|
||||
}
|
||||
|
||||
// volumeStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
||||
// a the state of a Volume. Returns successfully when volume is available
|
||||
func volumeStateRefreshFunc(conn *ec2.EC2, volumeID string) resource.StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{
|
||||
VolumeIDs: []*string{aws.String(volumeID)},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok {
|
||||
// Set this to nil as if we didn't find anything.
|
||||
log.Printf("Error on Volume State Refresh: message: \"%s\", code:\"%s\"", ec2err.Message(), ec2err.Code())
|
||||
resp = nil
|
||||
return nil, "", err
|
||||
} else {
|
||||
log.Printf("Error on Volume State Refresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
v := resp.Volumes[0]
|
||||
return v, *v.State, nil
|
||||
}
|
||||
}
|
||||
|
||||
func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
|
|
|
@ -1,23 +1,59 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/awslabs/aws-sdk-go/aws"
|
||||
"github.com/awslabs/aws-sdk-go/service/ec2"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestAccAWSEBSVolume(t *testing.T) {
|
||||
var v ec2.Volume
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccAwsEbsVolumeConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckVolumeExists("aws_ebs_volume.test", &v),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckVolumeExists(n string, v *ec2.Volume) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[n]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", n)
|
||||
}
|
||||
|
||||
if rs.Primary.ID == "" {
|
||||
return fmt.Errorf("No ID is set")
|
||||
}
|
||||
|
||||
conn := testAccProvider.Meta().(*AWSClient).ec2conn
|
||||
|
||||
request := &ec2.DescribeVolumesInput{
|
||||
VolumeIDs: []*string{aws.String(rs.Primary.ID)},
|
||||
}
|
||||
|
||||
response, err := conn.DescribeVolumes(request)
|
||||
if err == nil {
|
||||
if response.Volumes != nil && len(response.Volumes) > 0 {
|
||||
*v = *response.Volumes[0]
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Error finding EC2 volume %s", rs.Primary.ID)
|
||||
}
|
||||
}
|
||||
|
||||
const testAccAwsEbsVolumeConfig = `
|
||||
resource "aws_ebs_volume" "test" {
|
||||
availability_zone = "us-west-2a"
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/awslabs/aws-sdk-go/aws"
|
||||
"github.com/awslabs/aws-sdk-go/aws/awserr"
|
||||
"github.com/awslabs/aws-sdk-go/service/ec2"
|
||||
"github.com/hashicorp/terraform/helper/hashcode"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
)
|
||||
|
||||
func resourceAwsVolumeAttachment() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceAwsVolumeAttachmentCreate,
|
||||
Read: resourceAwsVolumeAttachmentRead,
|
||||
Delete: resourceAwsVolumeAttachmentDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"device_name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"instance_id": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"volume_id": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"force_detach": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceAwsVolumeAttachmentCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
name := d.Get("device_name").(string)
|
||||
iID := d.Get("instance_id").(string)
|
||||
vID := d.Get("volume_id").(string)
|
||||
|
||||
opts := &ec2.AttachVolumeInput{
|
||||
Device: aws.String(name),
|
||||
InstanceID: aws.String(iID),
|
||||
VolumeID: aws.String(vID),
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Attaching Volume (%s) to Instance (%s)", vID, iID)
|
||||
_, err := conn.AttachVolume(opts)
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
return fmt.Errorf("[WARN] Error attaching volume (%s) to instance (%s), message: \"%s\", code: \"%s\"",
|
||||
vID, iID, awsErr.Message(), awsErr.Code())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
stateConf := &resource.StateChangeConf{
|
||||
Pending: []string{"attaching"},
|
||||
Target: "attached",
|
||||
Refresh: volumeAttachmentStateRefreshFunc(conn, vID, iID),
|
||||
Timeout: 5 * time.Minute,
|
||||
Delay: 10 * time.Second,
|
||||
MinTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
_, err = stateConf.WaitForState()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"Error waiting for Volume (%s) to attach to Instance: %s, error:",
|
||||
vID, iID, err)
|
||||
}
|
||||
|
||||
d.SetId(volumeAttachmentID(name, vID, iID))
|
||||
return resourceAwsVolumeAttachmentRead(d, meta)
|
||||
}
|
||||
|
||||
func volumeAttachmentStateRefreshFunc(conn *ec2.EC2, volumeID, instanceID string) resource.StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
|
||||
request := &ec2.DescribeVolumesInput{
|
||||
VolumeIDs: []*string{aws.String(volumeID)},
|
||||
Filters: []*ec2.Filter{
|
||||
&ec2.Filter{
|
||||
Name: aws.String("attachment.instance-id"),
|
||||
Values: []*string{aws.String(instanceID)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := conn.DescribeVolumes(request)
|
||||
if err != nil {
|
||||
if awsErr, ok := err.(awserr.Error); ok {
|
||||
return nil, "failed", fmt.Errorf("code: %s, message: %s", awsErr.Code(), awsErr.Message())
|
||||
}
|
||||
return nil, "failed", err
|
||||
}
|
||||
|
||||
if len(resp.Volumes) > 0 {
|
||||
v := resp.Volumes[0]
|
||||
for _, a := range v.Attachments {
|
||||
if a.InstanceID != nil && *a.InstanceID == instanceID {
|
||||
return a, *a.State, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// assume detached if volume count is 0
|
||||
return 42, "detached", nil
|
||||
}
|
||||
}
|
||||
func resourceAwsVolumeAttachmentRead(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
request := &ec2.DescribeVolumesInput{
|
||||
VolumeIDs: []*string{aws.String(d.Get("volume_id").(string))},
|
||||
Filters: []*ec2.Filter{
|
||||
&ec2.Filter{
|
||||
Name: aws.String("attachment.instance-id"),
|
||||
Values: []*string{aws.String(d.Get("instance_id").(string))},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := conn.DescribeVolumes(request)
|
||||
if err != nil {
|
||||
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidVolume.NotFound" {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Error reading EC2 volume %s for instance: %s: %#v", d.Get("volume_id").(string), d.Get("instance_id").(string), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceAwsVolumeAttachmentDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
vID := d.Get("volume_id").(string)
|
||||
iID := d.Get("instance_id").(string)
|
||||
|
||||
opts := &ec2.DetachVolumeInput{
|
||||
Device: aws.String(d.Get("device_name").(string)),
|
||||
InstanceID: aws.String(iID),
|
||||
VolumeID: aws.String(vID),
|
||||
Force: aws.Boolean(d.Get("force_detach").(bool)),
|
||||
}
|
||||
|
||||
_, err := conn.DetachVolume(opts)
|
||||
stateConf := &resource.StateChangeConf{
|
||||
Pending: []string{"detaching"},
|
||||
Target: "detached",
|
||||
Refresh: volumeAttachmentStateRefreshFunc(conn, vID, iID),
|
||||
Timeout: 5 * time.Minute,
|
||||
Delay: 10 * time.Second,
|
||||
MinTimeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Detaching Volume (%s) from Instance (%s)", vID, iID)
|
||||
_, err = stateConf.WaitForState()
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"Error waiting for Volume (%s) to detach from Instance: %s",
|
||||
vID, iID)
|
||||
}
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
func volumeAttachmentID(name, volumeID, instanceID string) string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(fmt.Sprintf("%s-", name))
|
||||
buf.WriteString(fmt.Sprintf("%s-", instanceID))
|
||||
buf.WriteString(fmt.Sprintf("%s-", volumeID))
|
||||
|
||||
return fmt.Sprintf("vai-%d", hashcode.String(buf.String()))
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/awslabs/aws-sdk-go/service/ec2"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestAccAWSVolumeAttachment_basic(t *testing.T) {
|
||||
var i ec2.Instance
|
||||
var v ec2.Volume
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
CheckDestroy: testAccCheckVolumeAttachmentDestroy,
|
||||
Steps: []resource.TestStep{
|
||||
resource.TestStep{
|
||||
Config: testAccVolumeAttachmentConfig,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_volume_attachment.ebs_att", "device_name", "/dev/sdh"),
|
||||
testAccCheckInstanceExists(
|
||||
"aws_instance.web", &i),
|
||||
testAccCheckVolumeExists(
|
||||
"aws_ebs_volume.example", &v),
|
||||
testAccCheckVolumeAttachmentExists(
|
||||
"aws_volume_attachment.ebs_att", &i, &v),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckVolumeAttachmentExists(n string, i *ec2.Instance, v *ec2.Volume) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[n]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", n)
|
||||
}
|
||||
|
||||
if rs.Primary.ID == "" {
|
||||
return fmt.Errorf("No ID is set")
|
||||
}
|
||||
|
||||
for _, b := range i.BlockDeviceMappings {
|
||||
if rs.Primary.Attributes["device_name"] == *b.DeviceName {
|
||||
if b.EBS.VolumeID != nil && rs.Primary.Attributes["volume_id"] == *b.EBS.VolumeID {
|
||||
// pass
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Error finding instance/volume")
|
||||
}
|
||||
}
|
||||
|
||||
func testAccCheckVolumeAttachmentDestroy(s *terraform.State) error {
|
||||
for _, rs := range s.RootModule().Resources {
|
||||
log.Printf("\n\n----- This is never called")
|
||||
if rs.Type != "aws_volume_attachment" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const testAccVolumeAttachmentConfig = `
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-21f78e11"
|
||||
availability_zone = "us-west-2a"
|
||||
instance_type = "t1.micro"
|
||||
tags {
|
||||
Name = "HelloWorld"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ebs_volume" "example" {
|
||||
availability_zone = "us-west-2a"
|
||||
size = 1
|
||||
}
|
||||
|
||||
resource "aws_volume_attachment" "ebs_att" {
|
||||
device_name = "/dev/sdh"
|
||||
volume_id = "${aws_ebs_volume.example.id}"
|
||||
instance_id = "${aws_instance.web.id}"
|
||||
}
|
||||
`
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
layout: "aws"
|
||||
page_title: "AWS: aws_volume_attachment"
|
||||
sidebar_current: "docs-aws-resource-volume-attachment"
|
||||
description: |-
|
||||
Provides an AWS EBS Volume Attachment
|
||||
---
|
||||
|
||||
# aws\_volume\_attachment
|
||||
|
||||
Provides an AWS EBS Volume Attachment as a top level resource, to attach and
|
||||
detach volumes from AWS Instances.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
resource "aws_volume_attachment" "ebs_att" {
|
||||
device_name = "/dev/sdh"
|
||||
volume_id = "${aws_ebs_volume.example.id}"
|
||||
instance_id = "${aws_instance.web.id}"
|
||||
}
|
||||
|
||||
resource "aws_instance" "web" {
|
||||
ami = "ami-21f78e11"
|
||||
availability_zone = "us-west-2a"
|
||||
instance_type = "t1.micro"
|
||||
tags {
|
||||
Name = "HelloWorld"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ebs_volume" "example" {
|
||||
availability_zone = "us-west-2a"
|
||||
size = 1
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `device_name` - (Required) The device name to expose to the instance (for
|
||||
example, `/dev/sdh` or `xvdh`)
|
||||
* `instance_id` - (Required) ID of the Instance to attach to
|
||||
* `volume_id` - (Required) ID of the Volume to be attached
|
||||
* `force_detach` - (Optional, Boolean) Set to `true` if you want to force the
|
||||
volume to detach. Useful if previous attempts failed, but use this option only
|
||||
as a last resort, as this can result in **data loss**. See
|
||||
[Detaching an Amazon EBS Volume from an Instance][1] for more information.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
* `device_name` - The device name exposed to the instance
|
||||
* `instance_id` - ID of the Instance
|
||||
* `volume_id` - ID of the Volume
|
||||
|
||||
[1]: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-detaching-volume.html
|
|
@ -180,6 +180,10 @@
|
|||
<a href="/docs/providers/aws/r/subnet.html">aws_subnet</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-aws-resource-volume-attachment") %>>
|
||||
<a href="/docs/providers/aws/r/volume_attachment.html">aws_volume_attachment</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-aws-resource-vpc") %>>
|
||||
<a href="/docs/providers/aws/r/vpc.html">aws_vpc</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue