diff --git a/aws/resource_aws_ami.go b/aws/resource_aws_ami.go index bc4f1421bb0..39e3c902056 100644 --- a/aws/resource_aws_ami.go +++ b/aws/resource_aws_ami.go @@ -25,9 +25,6 @@ const ( ) func resourceAwsAmi() *schema.Resource { - // Our schema is shared also with aws_ami_copy and aws_ami_from_instance - resourceSchema := resourceAwsAmiCommonSchema(false) - return &schema.Resource{ Create: resourceAwsAmiCreate, @@ -37,7 +34,167 @@ func resourceAwsAmi() *schema.Resource { Delete: schema.DefaultTimeout(AWSAMIDeleteRetryTimeout), }, - Schema: resourceSchema, + Schema: map[string]*schema.Schema{ + "image_location": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "architecture": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "x86_64", + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + // The following block device attributes intentionally mimick the + // corresponding attributes on aws_instance, since they have the + // same meaning. + // However, we don't use root_block_device here because the constraint + // on which root device attributes can be overridden for an instance to + // not apply when registering an AMI. + "ebs_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "device_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "encrypted": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "iops": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "snapshot_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "volume_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "standard", + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + return hashcode.String(buf.String()) + }, + }, + "ena_support": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "ephemeral_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Required: true, + }, + + "virtual_name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, + }, + "kernel_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance + // resources record that they implicitly created new EBS snapshots that we should + // now manage. Not set by aws_ami, since the snapshots used there are presumed to + // be independently managed. + "manage_ebs_snapshots": { + Type: schema.TypeBool, + Computed: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ramdisk_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "root_device_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "root_snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + "sriov_net_support": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "simple", + }, + "tags": tagsSchema(), + "virtualization_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "paravirtual", + }, + }, // The Read, Update and Delete operations are shared with aws_ami_copy // and aws_ami_from_instance, since they differ only in how the image @@ -374,207 +531,3 @@ func resourceAwsAmiWaitForAvailable(timeout time.Duration, id string, client *ec } return info.(*ec2.Image), nil } - -func resourceAwsAmiCommonSchema(computed bool) map[string]*schema.Schema { - // The "computed" parameter controls whether we're making - // a schema for an AMI that's been implicitly registered (aws_ami_copy, aws_ami_from_instance) - // or whether we're making a schema for an explicit registration (aws_ami). - // When set, almost every attribute is marked as "computed". - // When not set, only the "id" attribute is computed. - // "name" and "description" are never computed, since they must always - // be provided by the user. - - var virtualizationTypeDefault interface{} - var deleteEbsOnTerminationDefault interface{} - var sriovNetSupportDefault interface{} - var architectureDefault interface{} - var volumeTypeDefault interface{} - if !computed { - virtualizationTypeDefault = "paravirtual" - deleteEbsOnTerminationDefault = true - sriovNetSupportDefault = "simple" - architectureDefault = "x86_64" - volumeTypeDefault = "standard" - } - - return map[string]*schema.Schema{ - "image_location": { - Type: schema.TypeString, - Optional: !computed, - Computed: true, - ForceNew: !computed, - }, - "architecture": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - Default: architectureDefault, - }, - "description": { - Type: schema.TypeString, - Optional: true, - }, - "kernel_id": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "ramdisk_id": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - "root_device_name": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - "root_snapshot_id": { - Type: schema.TypeString, - Computed: true, - }, - "sriov_net_support": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - Default: sriovNetSupportDefault, - }, - "virtualization_type": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - Default: virtualizationTypeDefault, - }, - - // The following block device attributes intentionally mimick the - // corresponding attributes on aws_instance, since they have the - // same meaning. - // However, we don't use root_block_device here because the constraint - // on which root device attributes can be overridden for an instance to - // not apply when registering an AMI. - - "ebs_block_device": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "delete_on_termination": { - Type: schema.TypeBool, - Optional: !computed, - Default: deleteEbsOnTerminationDefault, - ForceNew: !computed, - Computed: computed, - }, - - "device_name": { - Type: schema.TypeString, - Required: !computed, - ForceNew: !computed, - Computed: computed, - }, - - "encrypted": { - Type: schema.TypeBool, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - - "iops": { - Type: schema.TypeInt, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - - "snapshot_id": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - }, - - "volume_size": { - Type: schema.TypeInt, - Optional: !computed, - Computed: true, - ForceNew: !computed, - }, - - "volume_type": { - Type: schema.TypeString, - Optional: !computed, - Computed: computed, - ForceNew: !computed, - Default: volumeTypeDefault, - }, - }, - }, - Set: func(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) - return hashcode.String(buf.String()) - }, - }, - - "ephemeral_block_device": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "device_name": { - Type: schema.TypeString, - Required: !computed, - Computed: computed, - }, - - "virtual_name": { - Type: schema.TypeString, - Required: !computed, - Computed: computed, - }, - }, - }, - Set: func(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) - return hashcode.String(buf.String()) - }, - }, - - "tags": tagsSchema(), - - // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance - // resources record that they implicitly created new EBS snapshots that we should - // now manage. Not set by aws_ami, since the snapshots used there are presumed to - // be independently managed. - "manage_ebs_snapshots": { - Type: schema.TypeBool, - Computed: true, - ForceNew: true, - }, - "ena_support": { - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - }, - } -} diff --git a/aws/resource_aws_ami_copy.go b/aws/resource_aws_ami_copy.go index efd27146fac..e74d74f864e 100644 --- a/aws/resource_aws_ami_copy.go +++ b/aws/resource_aws_ami_copy.go @@ -1,44 +1,17 @@ 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 resourceAwsAmiCopy() *schema.Resource { - // Inherit all of the common AMI attributes from aws_ami, since we're - // implicitly creating an aws_ami resource. - resourceSchema := resourceAwsAmiCommonSchema(true) - - // Additional attributes unique to the copy operation. - resourceSchema["source_ami_id"] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - } - resourceSchema["source_ami_region"] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - } - - resourceSchema["encrypted"] = &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - } - - resourceSchema["kms_key_id"] = &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ValidateFunc: validateArn, - } - return &schema.Resource{ Create: resourceAwsAmiCopyCreate, @@ -48,7 +21,168 @@ func resourceAwsAmiCopy() *schema.Resource { Delete: schema.DefaultTimeout(AWSAMIDeleteRetryTimeout), }, - Schema: resourceSchema, + Schema: map[string]*schema.Schema{ + "architecture": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + // The following block device attributes intentionally mimick the + // corresponding attributes on aws_instance, since they have the + // same meaning. + // However, we don't use root_block_device here because the constraint + // on which root device attributes can be overridden for an instance to + // not apply when registering an AMI. + "ebs_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Computed: true, + }, + + "device_name": { + Type: schema.TypeString, + Computed: true, + }, + + "encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + + "iops": { + Type: schema.TypeInt, + Computed: true, + }, + + "snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + + "volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + + "volume_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + return hashcode.String(buf.String()) + }, + }, + "ephemeral_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Computed: true, + }, + + "virtual_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, + }, + "ena_support": { + Type: schema.TypeBool, + Computed: true, + }, + "encrypted": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + "image_location": { + Type: schema.TypeString, + Computed: true, + }, + "kernel_id": { + Type: schema.TypeString, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance + // resources record that they implicitly created new EBS snapshots that we should + // now manage. Not set by aws_ami, since the snapshots used there are presumed to + // be independently managed. + "manage_ebs_snapshots": { + Type: schema.TypeBool, + Computed: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ramdisk_id": { + Type: schema.TypeString, + Computed: true, + }, + "root_device_name": { + Type: schema.TypeString, + Computed: true, + }, + "root_snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + "source_ami_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "source_ami_region": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "sriov_net_support": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "virtualization_type": { + Type: schema.TypeString, + Computed: true, + }, + }, // The remaining operations are shared with the generic aws_ami resource, // since the aws_ami_copy resource only differs in how it's created. diff --git a/aws/resource_aws_ami_copy_test.go b/aws/resource_aws_ami_copy_test.go index 2099640dad8..71e3958a37f 100644 --- a/aws/resource_aws_ami_copy_test.go +++ b/aws/resource_aws_ami_copy_test.go @@ -1,143 +1,141 @@ package aws import ( - "errors" "fmt" - "strings" "testing" "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/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) -func TestAccAWSAMICopy(t *testing.T) { - var amiId string - snapshots := []string{} +func TestAccAWSAMICopy_basic(t *testing.T) { + var image ec2.Image + resourceName := "aws_ami_copy.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAMICopyDestroy, Steps: []resource.TestStep{ - resource.TestStep{ + { Config: testAccAWSAMICopyConfig, - Check: func(state *terraform.State) error { - rs, ok := state.RootModule().Resources["aws_ami_copy.test"] - if !ok { - return fmt.Errorf("AMI resource not found") - } - - amiId = rs.Primary.ID - - if amiId == "" { - return fmt.Errorf("AMI id is not set") - } - - conn := testAccProvider.Meta().(*AWSClient).ec2conn - req := &ec2.DescribeImagesInput{ - ImageIds: []*string{aws.String(amiId)}, - } - describe, err := conn.DescribeImages(req) - if err != nil { - return err - } - - if len(describe.Images) != 1 || - *describe.Images[0].ImageId != rs.Primary.ID { - return fmt.Errorf("AMI not found") - } - - image := describe.Images[0] - if expected := "available"; *image.State != expected { - return fmt.Errorf("invalid image state; expected %v, got %v", expected, image.State) - } - if expected := "machine"; *image.ImageType != expected { - return fmt.Errorf("wrong image type; expected %v, got %v", expected, image.ImageType) - } - if expected := "terraform-acc-ami-copy"; *image.Name != expected { - return fmt.Errorf("wrong name; expected %v, got %v", expected, image.Name) - } - - for _, bdm := range image.BlockDeviceMappings { - // The snapshot ID might not be set, - // even for a block device that is an - // EBS volume. - if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil { - snapshots = append(snapshots, *bdm.Ebs.SnapshotId) - } - } - - if expected := 1; len(snapshots) != expected { - return fmt.Errorf("wrong number of snapshots; expected %v, got %v", expected, len(snapshots)) - } - - return nil - }, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAMICopyExists(resourceName, &image), + testAccCheckAWSAMICopyAttributes(&image), + ), }, }, - CheckDestroy: func(state *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn - diReq := &ec2.DescribeImagesInput{ - ImageIds: []*string{aws.String(amiId)}, - } - diRes, err := conn.DescribeImages(diReq) - if err != nil { - return err - } + }) +} - if len(diRes.Images) > 0 { - state := diRes.Images[0].State - return fmt.Errorf("AMI %v remains in state %v", amiId, state) - } +func TestAccAWSAMICopy_EnaSupport(t *testing.T) { + var image ec2.Image + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_ami_copy.test" - stillExist := make([]string, 0, len(snapshots)) - checkErrors := make(map[string]error) - for _, snapshotId := range snapshots { - dsReq := &ec2.DescribeSnapshotsInput{ - SnapshotIds: []*string{aws.String(snapshotId)}, - } - _, err := conn.DescribeSnapshots(dsReq) - if err == nil { - stillExist = append(stillExist, snapshotId) - continue - } - - awsErr, ok := err.(awserr.Error) - if !ok { - checkErrors[snapshotId] = err - continue - } - - if awsErr.Code() != "InvalidSnapshot.NotFound" { - checkErrors[snapshotId] = err - continue - } - } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAMICopyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAMICopyConfig_ENASupport(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAMICopyExists(resourceName, &image), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + ), + }, + }, + }) +} + +func testAccCheckAWSAMICopyExists(resourceName string, image *ec2.Image) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID set for %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + input := &ec2.DescribeImagesInput{ + ImageIds: []*string{aws.String(rs.Primary.ID)}, + } + output, err := conn.DescribeImages(input) + if err != nil { + return err + } + + if len(output.Images) == 0 || aws.StringValue(output.Images[0].ImageId) != rs.Primary.ID { + return fmt.Errorf("AMI %q not found", rs.Primary.ID) + } - if len(stillExist) > 0 || len(checkErrors) > 0 { - errParts := []string{ - "Expected all snapshots to be gone, but:", - } - for _, snapshotId := range stillExist { - errParts = append( - errParts, - fmt.Sprintf("- %v still exists", snapshotId), - ) - } - for snapshotId, err := range checkErrors { - errParts = append( - errParts, - fmt.Sprintf("- checking %v gave error: %v", snapshotId, err), - ) - } - return errors.New(strings.Join(errParts, "\n")) + *image = *output.Images[0] + + return nil + } +} + +func testAccCheckAWSAMICopyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ami_copy" { + continue + } + + input := &ec2.DescribeImagesInput{ + ImageIds: []*string{aws.String(rs.Primary.ID)}, + } + output, err := conn.DescribeImages(input) + if err != nil { + return err + } + + if output != nil && len(output.Images) > 0 && aws.StringValue(output.Images[0].ImageId) == rs.Primary.ID { + return fmt.Errorf("AMI %q still exists in state: %s", rs.Primary.ID, aws.StringValue(output.Images[0].State)) + } + } + + // Check for managed EBS snapshots + return testAccCheckAWSEbsSnapshotDestroy(s) +} + +func testAccCheckAWSAMICopyAttributes(image *ec2.Image) resource.TestCheckFunc { + return func(s *terraform.State) error { + if expected := "available"; aws.StringValue(image.State) != expected { + return fmt.Errorf("invalid image state; expected %s, got %s", expected, aws.StringValue(image.State)) + } + if expected := "machine"; aws.StringValue(image.ImageType) != expected { + return fmt.Errorf("wrong image type; expected %s, got %s", expected, aws.StringValue(image.ImageType)) + } + if expected := "terraform-acc-ami-copy"; aws.StringValue(image.Name) != expected { + return fmt.Errorf("wrong name; expected %s, got %s", expected, aws.StringValue(image.Name)) + } + + snapshots := []string{} + for _, bdm := range image.BlockDeviceMappings { + // The snapshot ID might not be set, + // even for a block device that is an + // EBS volume. + if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil { + snapshots = append(snapshots, aws.StringValue(bdm.Ebs.SnapshotId)) } + } - return nil - }, - }) + if expected := 1; len(snapshots) != expected { + return fmt.Errorf("wrong number of snapshots; expected %v, got %v", expected, len(snapshots)) + } + + return nil + } } var testAccAWSAMICopyConfig = ` @@ -202,3 +200,45 @@ resource "aws_ami_copy" "test" { source_ami_region = "us-east-1" } ` + +func testAccAWSAMICopyConfig_ENASupport(rName string) string { + return fmt.Sprintf(` +data "aws_availability_zones" "available" {} +data "aws_region" "current" {} + +resource "aws_ebs_volume" "test" { + availability_zone = "${data.aws_availability_zones.available.names[0]}" + size = 1 + + tags { + Name = %q + } +} + +resource "aws_ebs_snapshot" "test" { + volume_id = "${aws_ebs_volume.test.id}" + + tags { + Name = %q + } +} + +resource "aws_ami" "test" { + ena_support = true + name = "%s-source" + virtualization_type = "hvm" + root_device_name = "/dev/sda1" + + ebs_block_device { + device_name = "/dev/sda1" + snapshot_id = "${aws_ebs_snapshot.test.id}" + } +} + +resource "aws_ami_copy" "test" { + name = "%s-copy" + source_ami_id = "${aws_ami.test.id}" + source_ami_region = "${data.aws_region.current.name}" +} +`, rName, rName, rName, rName) +} diff --git a/aws/resource_aws_ami_from_instance.go b/aws/resource_aws_ami_from_instance.go index 0db9d336346..af05db1f1ff 100644 --- a/aws/resource_aws_ami_from_instance.go +++ b/aws/resource_aws_ami_from_instance.go @@ -1,29 +1,17 @@ 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 resourceAwsAmiFromInstance() *schema.Resource { - // Inherit all of the common AMI attributes from aws_ami, since we're - // implicitly creating an aws_ami resource. - resourceSchema := resourceAwsAmiCommonSchema(true) - - // Additional attributes unique to the copy operation. - resourceSchema["source_instance_id"] = &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - } - resourceSchema["snapshot_without_reboot"] = &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - } - return &schema.Resource{ Create: resourceAwsAmiFromInstanceCreate, @@ -33,7 +21,155 @@ func resourceAwsAmiFromInstance() *schema.Resource { Delete: schema.DefaultTimeout(AWSAMIDeleteRetryTimeout), }, - Schema: resourceSchema, + Schema: map[string]*schema.Schema{ + "architecture": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + // The following block device attributes intentionally mimick the + // corresponding attributes on aws_instance, since they have the + // same meaning. + // However, we don't use root_block_device here because the constraint + // on which root device attributes can be overridden for an instance to + // not apply when registering an AMI. + "ebs_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Computed: true, + }, + + "device_name": { + Type: schema.TypeString, + Computed: true, + }, + + "encrypted": { + Type: schema.TypeBool, + Computed: true, + }, + + "iops": { + Type: schema.TypeInt, + Computed: true, + }, + + "snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + + "volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + + "volume_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + return hashcode.String(buf.String()) + }, + }, + "ena_support": { + Type: schema.TypeBool, + Computed: true, + }, + "ephemeral_block_device": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Computed: true, + }, + + "virtual_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, + }, + "image_location": { + Type: schema.TypeString, + Computed: true, + }, + "kernel_id": { + Type: schema.TypeString, + Computed: true, + }, + // Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance + // resources record that they implicitly created new EBS snapshots that we should + // now manage. Not set by aws_ami, since the snapshots used there are presumed to + // be independently managed. + "manage_ebs_snapshots": { + Type: schema.TypeBool, + Computed: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "ramdisk_id": { + Type: schema.TypeString, + Computed: true, + }, + "root_device_name": { + Type: schema.TypeString, + Computed: true, + }, + "root_snapshot_id": { + Type: schema.TypeString, + Computed: true, + }, + "source_instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "snapshot_without_reboot": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "sriov_net_support": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "virtualization_type": { + Type: schema.TypeString, + Computed: true, + }, + }, // The remaining operations are shared with the generic aws_ami resource, // since the aws_ami_copy resource only differs in how it's created. diff --git a/aws/resource_aws_ebs_snapshot_test.go b/aws/resource_aws_ebs_snapshot_test.go index 254ab40d0ff..df713f335f1 100644 --- a/aws/resource_aws_ebs_snapshot_test.go +++ b/aws/resource_aws_ebs_snapshot_test.go @@ -40,8 +40,9 @@ func TestAccAWSEBSSnapshot_basic(t *testing.T) { } resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEbsSnapshotDestroy, Steps: []resource.TestStep{ { Config: testAccAwsEbsSnapshotConfigBasic(rName), @@ -67,8 +68,9 @@ func TestAccAWSEBSSnapshot_withDescription(t *testing.T) { rName := fmt.Sprintf("tf-acc-ebs-snapshot-desc-%s", acctest.RandString(7)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEbsSnapshotDestroy, Steps: []resource.TestStep{ { Config: testAccAwsEbsSnapshotConfigWithDescription(rName), @@ -86,8 +88,9 @@ func TestAccAWSEBSSnapshot_withKms(t *testing.T) { rName := fmt.Sprintf("tf-acc-ebs-snapshot-kms-%s", acctest.RandString(7)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEbsSnapshotDestroy, Steps: []resource.TestStep{ { Config: testAccAwsEbsSnapshotConfigWithKms(rName), @@ -129,6 +132,32 @@ func testAccCheckSnapshotExists(n string, v *ec2.Snapshot) resource.TestCheckFun } } +func testAccCheckAWSEbsSnapshotDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ebs_snapshot" { + continue + } + input := &ec2.DescribeSnapshotsInput{ + SnapshotIds: []*string{aws.String(rs.Primary.ID)}, + } + + output, err := conn.DescribeSnapshots(input) + if err != nil { + if isAWSErr(err, "InvalidSnapshot.NotFound", "") { + continue + } + return err + } + if output != nil && len(output.Snapshots) > 0 && aws.StringValue(output.Snapshots[0].SnapshotId) == rs.Primary.ID { + return fmt.Errorf("EBS Snapshot %q still exists", rs.Primary.ID) + } + } + + return nil +} + func testAccAwsEbsSnapshotConfigBasic(rName string) string { return fmt.Sprintf(` data "aws_region" "current" {}