From 7fde952d02734a3d1b2989dc2d1c2bf4c82d96ee Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Mon, 21 Nov 2016 12:40:22 +0200 Subject: [PATCH] provider/aws: New Resource aws_ebs_snapshot (#10017) * provider/aws: Add ability to create aws_ebs_snapshot ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSEBSSnapshot_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2016/11/10 14:18:36 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSEBSSnapshot_ -timeout 120m === RUN TestAccAWSEBSSnapshot_basic --- PASS: TestAccAWSEBSSnapshot_basic (31.56s) === RUN TestAccAWSEBSSnapshot_withDescription --- PASS: TestAccAWSEBSSnapshot_withDescription (189.35s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws220.928s ``` * docs/aws: Addition of the docs for aws_ebs_snapshot resource * provider/aws: Creation of shared schema funcs for common AWS data source patterns * provider/aws: Create aws_ebs_snapshot datasource Fixes #8828 This data source will use a number of filters, owner_ids, snapshot_ids and restorable_by_user_ids in order to find the correct snapshot. The data source has no real use case for most_recent and will error on no snapshots found or greater than 1 snapshot found ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSEbsSnapshotDataSource_' ==> Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) 2016/11/10 14:34:33 Generated command/internal_plugin_list.go TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSEbsSnapshotDataSource_ -timeout 120m === RUN TestAccAWSEbsSnapshotDataSource_basic --- PASS: TestAccAWSEbsSnapshotDataSource_basic (192.66s) === RUN TestAccAWSEbsSnapshotDataSource_multipleFilters --- PASS: TestAccAWSEbsSnapshotDataSource_multipleFilters (33.84s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws226.522s ``` * docs/aws: Addition of docs for the aws_ebs_snapshot data source Adds the new resource `aws_ebs_snapshot` --- builtin/providers/aws/data_source_aws_ami.go | 86 +---------- .../aws/data_source_aws_common_schema.go | 91 +++++++++++ .../aws/data_source_aws_ebs_snapshot.go | 143 +++++++++++++++++ .../aws/data_source_aws_ebs_snapshot_test.go | 97 ++++++++++++ .../aws/data_source_aws_ebs_volume.go | 83 +--------- builtin/providers/aws/provider.go | 2 + .../aws/resource_aws_ebs_snapshot.go | 145 ++++++++++++++++++ .../aws/resource_aws_ebs_snapshot_test.go | 95 ++++++++++++ .../aws/d/ebs_snapshot.html.markdown | 61 ++++++++ .../docs/providers/aws/r/ebs_snapshot.html.md | 48 ++++++ website/source/layouts/aws.erb | 18 ++- 11 files changed, 701 insertions(+), 168 deletions(-) create mode 100644 builtin/providers/aws/data_source_aws_common_schema.go create mode 100644 builtin/providers/aws/data_source_aws_ebs_snapshot.go create mode 100644 builtin/providers/aws/data_source_aws_ebs_snapshot_test.go create mode 100644 builtin/providers/aws/resource_aws_ebs_snapshot.go create mode 100644 builtin/providers/aws/resource_aws_ebs_snapshot_test.go create mode 100644 website/source/docs/providers/aws/d/ebs_snapshot.html.markdown create mode 100644 website/source/docs/providers/aws/r/ebs_snapshot.html.md diff --git a/builtin/providers/aws/data_source_aws_ami.go b/builtin/providers/aws/data_source_aws_ami.go index bcdce62ea..14c40d28d 100644 --- a/builtin/providers/aws/data_source_aws_ami.go +++ b/builtin/providers/aws/data_source_aws_ami.go @@ -8,7 +8,6 @@ import ( "sort" "time" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" @@ -19,31 +18,13 @@ func dataSourceAwsAmi() *schema.Resource { Read: dataSourceAwsAmiRead, Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), "executable_users": { Type: schema.TypeList, Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "filter": { - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - - "values": { - Type: schema.TypeList, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - }, - }, - }, "name_regex": { Type: schema.TypeString, Optional: true, @@ -186,23 +167,7 @@ func dataSourceAwsAmi() *schema.Resource { Type: schema.TypeMap, Computed: true, }, - "tags": { - Type: schema.TypeSet, - Computed: true, - Set: amiTagsHash, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, + "tags": dataSourceTagsSchema(), }, } } @@ -225,7 +190,7 @@ func dataSourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { params.ExecutableUsers = expandStringList(executableUsers.([]interface{})) } if filtersOk { - params.Filters = buildAmiFilters(filters.(*schema.Set)) + params.Filters = buildAwsDataSourceFilters(filters.(*schema.Set)) } if ownersOk { params.Owners = expandStringList(owners.([]interface{})) @@ -280,23 +245,6 @@ func dataSourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { return amiDescriptionAttributes(d, image) } -// Build a slice of AMI filter options from the filters provided. -func buildAmiFilters(set *schema.Set) []*ec2.Filter { - var filters []*ec2.Filter - for _, v := range set.List() { - m := v.(map[string]interface{}) - var filterValues []*string - for _, e := range m["values"].([]interface{}) { - filterValues = append(filterValues, aws.String(e.(string))) - } - filters = append(filters, &ec2.Filter{ - Name: aws.String(m["name"].(string)), - Values: filterValues, - }) - } - return filters -} - type imageSort []*ec2.Image func (a imageSort) Len() int { return len(a) } @@ -361,7 +309,7 @@ func amiDescriptionAttributes(d *schema.ResourceData, image *ec2.Image) error { if err := d.Set("state_reason", amiStateReason(image.StateReason)); err != nil { return err } - if err := d.Set("tags", amiTags(image.Tags)); err != nil { + if err := d.Set("tags", dataSourceTags(image.Tags)); err != nil { return err } return nil @@ -433,21 +381,6 @@ func amiStateReason(m *ec2.StateReason) map[string]interface{} { return s } -// Returns a set of tags. -func amiTags(m []*ec2.Tag) *schema.Set { - s := &schema.Set{ - F: amiTagsHash, - } - for _, v := range m { - tag := map[string]interface{}{ - "key": *v.Key, - "value": *v.Value, - } - s.Add(tag) - } - return s -} - // Generates a hash for the set hash function used by the block_device_mappings // attribute. func amiBlockDeviceMappingHash(v interface{}) int { @@ -488,17 +421,6 @@ func amiProductCodesHash(v interface{}) int { return hashcode.String(buf.String()) } -// Generates a hash for the set hash function used by the tags -// attribute. -func amiTagsHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - // All keys added in alphabetical order. - buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) - return hashcode.String(buf.String()) -} - func validateNameRegex(v interface{}, k string) (ws []string, errors []error) { value := v.(string) diff --git a/builtin/providers/aws/data_source_aws_common_schema.go b/builtin/providers/aws/data_source_aws_common_schema.go new file mode 100644 index 000000000..839f8a67b --- /dev/null +++ b/builtin/providers/aws/data_source_aws_common_schema.go @@ -0,0 +1,91 @@ +package aws + +import ( + "bytes" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceTagsHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) + return hashcode.String(buf.String()) +} + +func dataSourceTags(m []*ec2.Tag) *schema.Set { + s := &schema.Set{ + F: dataSourceTagsHash, + } + for _, v := range m { + tag := map[string]interface{}{ + "key": *v.Key, + "value": *v.Value, + } + s.Add(tag) + } + return s +} + +func buildAwsDataSourceFilters(set *schema.Set) []*ec2.Filter { + var filters []*ec2.Filter + for _, v := range set.List() { + m := v.(map[string]interface{}) + var filterValues []*string + for _, e := range m["values"].([]interface{}) { + filterValues = append(filterValues, aws.String(e.(string))) + } + filters = append(filters, &ec2.Filter{ + Name: aws.String(m["name"].(string)), + Values: filterValues, + }) + } + return filters +} + +func dataSourceFiltersSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "values": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + } +} + +func dataSourceTagsSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Set: dataSourceTagsHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + } +} diff --git a/builtin/providers/aws/data_source_aws_ebs_snapshot.go b/builtin/providers/aws/data_source_aws_ebs_snapshot.go new file mode 100644 index 000000000..6a7db912b --- /dev/null +++ b/builtin/providers/aws/data_source_aws_ebs_snapshot.go @@ -0,0 +1,143 @@ +package aws + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceAwsEbsSnapshot() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsEbsSnapshotRead, + + Schema: map[string]*schema.Schema{ + //selection criteria + "filter": dataSourceFiltersSchema(), + + "owners": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "snapshot_ids": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "restorable_by_user_ids": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + //Computed values returned + "snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + "volume_id": { + Type: schema.TypeString, + Computed: true, + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "owner_alias": { + Type: schema.TypeString, + Computed: true, + }, + "encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "data_encryption_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": dataSourceTagsSchema(), + }, + } +} + +func dataSourceAwsEbsSnapshotRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + restorableUsers, restorableUsersOk := d.GetOk("restorable_by_user_ids") + filters, filtersOk := d.GetOk("filter") + snapshotIds, snapshotIdsOk := d.GetOk("snapshot_ids") + owners, ownersOk := d.GetOk("owners") + + if restorableUsers == false && filtersOk == false && snapshotIds == false && ownersOk == false { + return fmt.Errorf("One of snapshot_ids, filters, restorable_by_user_ids, or owners must be assigned") + } + + params := &ec2.DescribeSnapshotsInput{} + if restorableUsersOk { + params.RestorableByUserIds = expandStringList(restorableUsers.([]interface{})) + } + if filtersOk { + params.Filters = buildAwsDataSourceFilters(filters.(*schema.Set)) + } + if ownersOk { + params.OwnerIds = expandStringList(owners.([]interface{})) + } + if snapshotIdsOk { + params.SnapshotIds = expandStringList(snapshotIds.([]interface{})) + } + + resp, err := conn.DescribeSnapshots(params) + if err != nil { + return err + } + + if len(resp.Snapshots) < 1 { + return fmt.Errorf("Your query returned no results. Please change your search criteria and try again.") + } + + if len(resp.Snapshots) > 1 { + return fmt.Errorf("Your query returned more than one result. Please try a more specific search criteria.") + } + + //Single Snapshot found so set to state + return snapshotDescriptionAttributes(d, resp.Snapshots[0]) +} + +func snapshotDescriptionAttributes(d *schema.ResourceData, snapshot *ec2.Snapshot) error { + d.SetId(*snapshot.SnapshotId) + d.Set("snapshot_id", snapshot.SnapshotId) + d.Set("volume_id", snapshot.VolumeId) + d.Set("data_encryption_key_id", snapshot.DataEncryptionKeyId) + d.Set("description", snapshot.Description) + d.Set("encrypted", snapshot.Encrypted) + d.Set("kms_key_id", snapshot.KmsKeyId) + d.Set("volume_size", snapshot.VolumeSize) + d.Set("state", snapshot.State) + d.Set("owner_id", snapshot.OwnerId) + d.Set("owner_alias", snapshot.OwnerAlias) + + if err := d.Set("tags", dataSourceTags(snapshot.Tags)); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/data_source_aws_ebs_snapshot_test.go b/builtin/providers/aws/data_source_aws_ebs_snapshot_test.go new file mode 100644 index 000000000..f9e8179f8 --- /dev/null +++ b/builtin/providers/aws/data_source_aws_ebs_snapshot_test.go @@ -0,0 +1,97 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSEbsSnapshotDataSource_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsEbsSnapshotDataSourceConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsEbsSnapshotDataSourceID("data.aws_ebs_snapshot.snapshot"), + resource.TestCheckResourceAttr("data.aws_ebs_snapshot.snapshot", "volume_size", "40"), + ), + }, + }, + }) +} + +func TestAccAWSEbsSnapshotDataSource_multipleFilters(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCheckAwsEbsSnapshotDataSourceConfigWithMultipleFilters, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsEbsSnapshotDataSourceID("data.aws_ebs_snapshot.snapshot"), + resource.TestCheckResourceAttr("data.aws_ebs_snapshot.snapshot", "volume_size", "10"), + ), + }, + }, + }) +} + +func testAccCheckAwsEbsSnapshotDataSourceID(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find Volume data source: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Snapshot data source ID not set") + } + return nil + } +} + +const testAccCheckAwsEbsSnapshotDataSourceConfig = ` +resource "aws_ebs_volume" "example" { + availability_zone = "us-west-2a" + type = "gp2" + size = 40 + tags { + Name = "External Volume" + } +} + +resource "aws_ebs_snapshot" "snapshot" { + volume_id = "${aws_ebs_volume.example.id}" +} + +data "aws_ebs_snapshot" "snapshot" { + snapshot_ids = ["${aws_ebs_snapshot.snapshot.id}"] +} +` + +const testAccCheckAwsEbsSnapshotDataSourceConfigWithMultipleFilters = ` +resource "aws_ebs_volume" "external1" { + availability_zone = "us-west-2a" + type = "gp2" + size = 10 + tags { + Name = "External Volume 1" + } +} + +resource "aws_ebs_snapshot" "snapshot" { + volume_id = "${aws_ebs_volume.external1.id}" +} + +data "aws_ebs_snapshot" "snapshot" { + snapshot_ids = ["${aws_ebs_snapshot.snapshot.id}"] + filter { + name = "volume-size" + values = ["10"] + } +} +` diff --git a/builtin/providers/aws/data_source_aws_ebs_volume.go b/builtin/providers/aws/data_source_aws_ebs_volume.go index 96cda87c6..7794ecf28 100644 --- a/builtin/providers/aws/data_source_aws_ebs_volume.go +++ b/builtin/providers/aws/data_source_aws_ebs_volume.go @@ -1,15 +1,12 @@ package aws import ( - "bytes" "fmt" "log" "sort" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/davecgh/go-spew/spew" - "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -18,25 +15,7 @@ func dataSourceAwsEbsVolume() *schema.Resource { Read: dataSourceAwsEbsVolumeRead, Schema: map[string]*schema.Schema{ - "filter": { - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - - "values": { - Type: schema.TypeList, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - }, - }, - }, + "filter": dataSourceFiltersSchema(), "most_recent": { Type: schema.TypeBool, Optional: true, @@ -75,23 +54,7 @@ func dataSourceAwsEbsVolume() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "tags": { - Type: schema.TypeSet, - Computed: true, - Set: volumeTagsHash, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, + "tags": dataSourceTagsSchema(), }, } } @@ -103,7 +66,7 @@ func dataSourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error params := &ec2.DescribeVolumesInput{} if filtersOk { - params.Filters = buildVolumeFilters(filters.(*schema.Set)) + params.Filters = buildAwsDataSourceFilters(filters.(*schema.Set)) } resp, err := conn.DescribeVolumes(params) @@ -138,22 +101,6 @@ func dataSourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error return volumeDescriptionAttributes(d, volume) } -func buildVolumeFilters(set *schema.Set) []*ec2.Filter { - var filters []*ec2.Filter - for _, v := range set.List() { - m := v.(map[string]interface{}) - var filterValues []*string - for _, e := range m["values"].([]interface{}) { - filterValues = append(filterValues, aws.String(e.(string))) - } - filters = append(filters, &ec2.Filter{ - Name: aws.String(m["name"].(string)), - Values: filterValues, - }) - } - return filters -} - type volumeSort []*ec2.Volume func (a volumeSort) Len() int { return len(a) } @@ -181,31 +128,9 @@ func volumeDescriptionAttributes(d *schema.ResourceData, volume *ec2.Volume) err d.Set("snapshot_id", volume.SnapshotId) d.Set("volume_type", volume.VolumeType) - if err := d.Set("tags", volumeTags(volume.Tags)); err != nil { + if err := d.Set("tags", dataSourceTags(volume.Tags)); err != nil { return err } return nil } - -func volumeTags(m []*ec2.Tag) *schema.Set { - s := &schema.Set{ - F: volumeTagsHash, - } - for _, v := range m { - tag := map[string]interface{}{ - "key": *v.Key, - "value": *v.Value, - } - s.Add(tag) - } - return s -} - -func volumeTagsHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) - return hashcode.String(buf.String()) -} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 468260d2d..2f0d8d23e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -152,6 +152,7 @@ func Provider() terraform.ResourceProvider { "aws_billing_service_account": dataSourceAwsBillingServiceAccount(), "aws_caller_identity": dataSourceAwsCallerIdentity(), "aws_cloudformation_stack": dataSourceAwsCloudFormationStack(), + "aws_ebs_snapshot": dataSourceAwsEbsSnapshot(), "aws_ebs_volume": dataSourceAwsEbsVolume(), "aws_ecs_container_definition": dataSourceAwsEcsContainerDefinition(), "aws_elb_service_account": dataSourceAwsElbServiceAccount(), @@ -223,6 +224,7 @@ func Provider() terraform.ResourceProvider { "aws_db_subnet_group": resourceAwsDbSubnetGroup(), "aws_directory_service_directory": resourceAwsDirectoryServiceDirectory(), "aws_dynamodb_table": resourceAwsDynamoDbTable(), + "aws_ebs_snapshot": resourceAwsEbsSnapshot(), "aws_ebs_volume": resourceAwsEbsVolume(), "aws_ecr_repository": resourceAwsEcrRepository(), "aws_ecr_repository_policy": resourceAwsEcrRepositoryPolicy(), diff --git a/builtin/providers/aws/resource_aws_ebs_snapshot.go b/builtin/providers/aws/resource_aws_ebs_snapshot.go new file mode 100644 index 000000000..e06a7290d --- /dev/null +++ b/builtin/providers/aws/resource_aws_ebs_snapshot.go @@ -0,0 +1,145 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsEbsSnapshot() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEbsSnapshotCreate, + Read: resourceAwsEbsSnapshotRead, + Delete: resourceAwsEbsSnapshotDelete, + + Schema: map[string]*schema.Schema{ + "volume_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "owner_alias": { + Type: schema.TypeString, + Computed: true, + }, + "encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + "volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "data_encryption_key_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsEbsSnapshotCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + request := &ec2.CreateSnapshotInput{ + VolumeId: aws.String(d.Get("volume_id").(string)), + } + if v, ok := d.GetOk("description"); ok { + request.Description = aws.String(v.(string)) + } + + res, err := conn.CreateSnapshot(request) + if err != nil { + return err + } + + d.SetId(*res.SnapshotId) + + err = resourceAwsEbsSnapshotWaitForAvailable(d.Id(), conn) + if err != nil { + return err + } + + return resourceAwsEbsSnapshotRead(d, meta) +} + +func resourceAwsEbsSnapshotRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + req := &ec2.DescribeSnapshotsInput{ + SnapshotIds: []*string{aws.String(d.Id())}, + } + res, err := conn.DescribeSnapshots(req) + if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSnapshotID.NotFound" { + log.Printf("Snapshot %q Not found - removing from state", d.Id()) + d.SetId("") + return nil + } + + snapshot := res.Snapshots[0] + + d.Set("description", snapshot.Description) + d.Set("owner_id", snapshot.OwnerId) + d.Set("encrypted", snapshot.Encrypted) + d.Set("owner_alias", snapshot.OwnerAlias) + d.Set("volume_id", snapshot.VolumeId) + d.Set("data_encryption_key_id", snapshot.DataEncryptionKeyId) + d.Set("kms_keey_id", snapshot.KmsKeyId) + d.Set("volume_size", snapshot.VolumeSize) + + return nil +} + +func resourceAwsEbsSnapshotDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + return resource.Retry(5*time.Minute, func() *resource.RetryError { + request := &ec2.DeleteSnapshotInput{ + SnapshotId: aws.String(d.Id()), + } + _, err := conn.DeleteSnapshot(request) + if err == nil { + return nil + } + + ebsErr, ok := err.(awserr.Error) + if ebsErr.Code() == "SnapshotInUse" { + return resource.RetryableError(fmt.Errorf("EBS SnapshotInUse - trying again while it detaches")) + } + + if !ok { + return resource.NonRetryableError(err) + } + + return resource.NonRetryableError(err) + }) +} + +func resourceAwsEbsSnapshotWaitForAvailable(id string, conn *ec2.EC2) error { + log.Printf("Waiting for Snapshot %s to become available...", id) + + req := &ec2.DescribeSnapshotsInput{ + SnapshotIds: []*string{aws.String(id)}, + } + err := conn.WaitUntilSnapshotCompleted(req) + return err +} diff --git a/builtin/providers/aws/resource_aws_ebs_snapshot_test.go b/builtin/providers/aws/resource_aws_ebs_snapshot_test.go new file mode 100644 index 000000000..8e2c7a2ba --- /dev/null +++ b/builtin/providers/aws/resource_aws_ebs_snapshot_test.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSEBSSnapshot_basic(t *testing.T) { + var v ec2.Snapshot + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsSnapshotConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckSnapshotExists("aws_ebs_snapshot.test", &v), + ), + }, + }, + }) +} + +func TestAccAWSEBSSnapshot_withDescription(t *testing.T) { + var v ec2.Snapshot + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsSnapshotConfigWithDescription, + Check: resource.ComposeTestCheckFunc( + testAccCheckSnapshotExists("aws_ebs_snapshot.test", &v), + resource.TestCheckResourceAttr("aws_ebs_snapshot.test", "description", "EBS Snapshot Acceptance Test"), + ), + }, + }, + }) +} + +func testAccCheckSnapshotExists(n string, v *ec2.Snapshot) 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.DescribeSnapshotsInput{ + SnapshotIds: []*string{aws.String(rs.Primary.ID)}, + } + + response, err := conn.DescribeSnapshots(request) + if err == nil { + if response.Snapshots != nil && len(response.Snapshots) > 0 { + *v = *response.Snapshots[0] + return nil + } + } + return fmt.Errorf("Error finding EC2 Snapshot %s", rs.Primary.ID) + } +} + +const testAccAwsEbsSnapshotConfig = ` +resource "aws_ebs_volume" "test" { + availability_zone = "us-west-2a" + size = 1 +} + +resource "aws_ebs_snapshot" "test" { + volume_id = "${aws_ebs_volume.test.id}" +} +` + +const testAccAwsEbsSnapshotConfigWithDescription = ` +resource "aws_ebs_volume" "description_test" { + availability_zone = "us-west-2a" + size = 1 +} + +resource "aws_ebs_snapshot" "test" { + volume_id = "${aws_ebs_volume.description_test.id}" + description = "EBS Snapshot Acceptance Test" +} +` diff --git a/website/source/docs/providers/aws/d/ebs_snapshot.html.markdown b/website/source/docs/providers/aws/d/ebs_snapshot.html.markdown new file mode 100644 index 000000000..39a55b509 --- /dev/null +++ b/website/source/docs/providers/aws/d/ebs_snapshot.html.markdown @@ -0,0 +1,61 @@ +--- +layout: "aws" +page_title: "AWS: aws_ebs_snapshot" +sidebar_current: "docs-aws-datasource-ebs-snapshot" +description: |- + Get information on an EBS Snapshot. +--- + +# aws\_ebs\_snapshot + +Use this data source to get information about an EBS Snapshot for use when provisioning EBS Volumes + +## Example Usage + +``` +data "aws_ebs_snapshot" "ebs_volume" { + owners = ["self"] + filter { + name = "volume-size" + values = ["40"] + } + filter { + name = "tag:Name" + values = ["Example"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `owners` - (Optional) Returns the snapshots owned by the specified owner id. Multiple owners can be specified. + +* `snapshot_ids` - (Optional) Returns information on a specific snapshot_id. + +* `restorable_by_user_ids` - (Optional) One or more AWS accounts IDs that can create volumes from the snapshot. + +* `filter` - (Optional) One or more name/value pairs to filter off of. There are +several valid keys, for a full reference, check out +[describe-volumes in the AWS CLI reference][1]. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The snapshot ID (e.g. snap-59fcb34e). +* `snapshot_id` - The snapshot ID (e.g. snap-59fcb34e). +* `description` - A description for the snapshot +* `owner_id` - The AWS account ID of the EBS snapshot owner. +* `owner_alias` - Value from an Amazon-maintained list (`amazon`, `aws-marketplace`, `microsoft`) of snapshot owners. +* `volume_id` - The volume ID (e.g. vol-59fcb34e). +* `encrypted` - Whether the snapshot is encrypted. +* `volume_size` - The size of the drive in GiBs. +* `kms_key_id` - The ARN for the KMS encryption key. +* `data_encryption_key_id` - The data encryption key identifier for the snapshot. +* `state` - The snapshot state. +* `tags` - A mapping of tags for the resource. + +[1]: http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-snapshots.html diff --git a/website/source/docs/providers/aws/r/ebs_snapshot.html.md b/website/source/docs/providers/aws/r/ebs_snapshot.html.md new file mode 100644 index 000000000..5c6eebe1e --- /dev/null +++ b/website/source/docs/providers/aws/r/ebs_snapshot.html.md @@ -0,0 +1,48 @@ +--- +layout: "aws" +page_title: "AWS: aws_ebs_snapshot" +sidebar_current: "docs-aws-resource-ebs-snapshot" +description: |- + Provides an elastic block storage snapshot resource. +--- + +# aws\_ebs\_snapshot + +Creates a Snapshot of an EBS Volume. + +## Example Usage + +``` +resource "aws_ebs_volume" "example" { + availability_zone = "us-west-2a" + size = 40 + tags { + Name = "HelloWorld" + } +} + +resource "aws_ebs_snapshot" "example_snapshot" { + volume_id = "${aws_ebs_volume.example.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `volume_id` - (Required) The Volume ID of which to make a snapshot. +* `description` - (Optional) A description of what the snapshot is. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The snapshot ID (e.g. snap-59fcb34e). +* `owner_id` - The AWS account ID of the EBS snapshot owner. +* `owner_alias` - Value from an Amazon-maintained list (`amazon`, `aws-marketplace`, `microsoft`) of snapshot owners. +* `encrypted` - Whether the snapshot is encrypted. +* `volume_size` - The size of the drive in GiBs. +* `kms_key_id` - The ARN for the KMS encryption key. +* `data_encryption_key_id` - The data encryption key identifier for the snapshot. +* `tags` - A mapping of tags for the resource. \ No newline at end of file diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index a7b3c5641..7a5a42ff8 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -20,6 +20,7 @@ > aws_alb + > aws_ami @@ -38,6 +39,9 @@ > aws_cloudformation_stack + > + aws_ebs_snapshot + > aws_ebs_volume @@ -304,10 +308,6 @@ aws_autoscaling_group - > - aws_autoscaling_attachment - - > aws_autoscaling_lifecycle_hook @@ -324,6 +324,10 @@ aws_autoscaling_schedule + > + aws_ebs_snapshot + + > aws_ebs_volume @@ -819,11 +823,11 @@ > aws_waf_web_acl - + > aws_waf_byte_match_set - + > aws_waf_size_constraint_set @@ -835,7 +839,7 @@ > aws_waf_ipset - + > aws_waf_xss_match_set