diff --git a/aws/data_source_aws_ami.go b/aws/data_source_aws_ami.go index a1d1aba2f54..7f53f605fc5 100644 --- a/aws/data_source_aws_ami.go +++ b/aws/data_source_aws_ami.go @@ -329,6 +329,7 @@ func amiBlockDeviceMappings(m []*ec2.BlockDeviceMapping) *schema.Set { "delete_on_termination": fmt.Sprintf("%t", aws.BoolValue(v.Ebs.DeleteOnTermination)), "encrypted": fmt.Sprintf("%t", aws.BoolValue(v.Ebs.Encrypted)), "iops": fmt.Sprintf("%d", aws.Int64Value(v.Ebs.Iops)), + "throughput": fmt.Sprintf("%d", aws.Int64Value(v.Ebs.Throughput)), "volume_size": fmt.Sprintf("%d", aws.Int64Value(v.Ebs.VolumeSize)), "snapshot_id": aws.StringValue(v.Ebs.SnapshotId), "volume_type": aws.StringValue(v.Ebs.VolumeType), diff --git a/aws/data_source_aws_ami_test.go b/aws/data_source_aws_ami_test.go index e17e87aad73..9def968d79c 100644 --- a/aws/data_source_aws_ami_test.go +++ b/aws/data_source_aws_ami_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) @@ -55,6 +56,7 @@ func TestAccAWSAmiDataSource_natInstance(t *testing.T) { }, }) } + func TestAccAWSAmiDataSource_windowsInstance(t *testing.T) { resourceName := "data.aws_ami.windows_ami" resource.ParallelTest(t, resource.TestCase{ @@ -147,6 +149,37 @@ func TestAccAWSAmiDataSource_localNameFilter(t *testing.T) { }) } +func TestAccAWSAmiDataSource_Gp3BlockDevice(t *testing.T) { + resourceName := "aws_ami.test" + datasourceName := "data.aws_ami.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAmiDataSourceConfigGp3BlockDevice(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAmiDataSourceID(datasourceName), + resource.TestCheckResourceAttrPair(datasourceName, "architecture", resourceName, "architecture"), + resource.TestCheckResourceAttrPair(datasourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasourceName, "block_device_mappings.#", resourceName, "ebs_block_device.#"), + resource.TestCheckResourceAttrPair(datasourceName, "description", resourceName, "description"), + resource.TestCheckResourceAttrPair(datasourceName, "image_id", resourceName, "id"), + testAccCheckResourceAttrAccountID(datasourceName, "owner_id"), + resource.TestCheckResourceAttrPair(datasourceName, "root_device_name", resourceName, "root_device_name"), + resource.TestCheckResourceAttr(datasourceName, "root_device_type", "ebs"), + resource.TestCheckResourceAttrPair(datasourceName, "root_snapshot_id", resourceName, "root_snapshot_id"), + resource.TestCheckResourceAttrPair(datasourceName, "sriov_net_support", resourceName, "sriov_net_support"), + resource.TestCheckResourceAttrPair(datasourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(datasourceName, "virtualization_type", resourceName, "virtualization_type"), + ), + }, + }, + }) +} + func testAccCheckAwsAmiDataSourceID(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -234,3 +267,20 @@ data "aws_ami" "name_regex_filtered_ami" { name_regex = "^amzn-ami-min[a-z]{4}-hvm" } ` + +func testAccAmiDataSourceConfigGp3BlockDevice(rName string) string { + return composeConfig( + testAccAmiConfigGp3BlockDevice(rName), + ` +data "aws_caller_identity" "current" {} + +data "aws_ami" "test" { + owners = [data.aws_caller_identity.current.account_id] + + filter { + name = "image-id" + values = [aws_ami.test.id] + } +} +`) +} diff --git a/aws/resource_aws_ami.go b/aws/resource_aws_ami.go index 43d94dbfc83..1fe711e8fd3 100644 --- a/aws/resource_aws_ami.go +++ b/aws/resource_aws_ami.go @@ -110,6 +110,13 @@ func resourceAwsAmi() *schema.Resource { ForceNew: true, }, + "throughput": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "volume_size": { Type: schema.TypeInt, Optional: true, @@ -243,45 +250,36 @@ func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error { req.RamdiskId = aws.String(ramdiskId) } - ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set) - ephemeralBlockDevsSet := d.Get("ephemeral_block_device").(*schema.Set) - for _, ebsBlockDevI := range ebsBlockDevsSet.List() { - ebsBlockDev := ebsBlockDevI.(map[string]interface{}) - blockDev := &ec2.BlockDeviceMapping{ - DeviceName: aws.String(ebsBlockDev["device_name"].(string)), - Ebs: &ec2.EbsBlockDevice{ - DeleteOnTermination: aws.Bool(ebsBlockDev["delete_on_termination"].(bool)), - VolumeType: aws.String(ebsBlockDev["volume_type"].(string)), - }, - } - if iops, ok := ebsBlockDev["iops"]; ok { - if iop := iops.(int); iop != 0 { - blockDev.Ebs.Iops = aws.Int64(int64(iop)) + if v, ok := d.GetOk("ebs_block_device"); ok && v.(*schema.Set).Len() > 0 { + for _, tfMapRaw := range v.(*schema.Set).List() { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue } - } - if size, ok := ebsBlockDev["volume_size"]; ok { - if s := size.(int); s != 0 { - blockDev.Ebs.VolumeSize = aws.Int64(int64(s)) + + var encrypted bool + + if v, ok := tfMap["encrypted"].(bool); ok { + encrypted = v } - } - encrypted := ebsBlockDev["encrypted"].(bool) - if snapshotId := ebsBlockDev["snapshot_id"].(string); snapshotId != "" { - blockDev.Ebs.SnapshotId = aws.String(snapshotId) - if encrypted { + + var snapshot string + + if v, ok := tfMap["snapshot_id"].(string); ok && v != "" { + snapshot = v + } + + if snapshot != "" && encrypted { return errors.New("can't set both 'snapshot_id' and 'encrypted'") } - } else if encrypted { - blockDev.Ebs.Encrypted = aws.Bool(true) } - req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) + + req.BlockDeviceMappings = expandEc2BlockDeviceMappingsForAmiEbsBlockDevice(v.(*schema.Set).List()) } - for _, ephemeralBlockDevI := range ephemeralBlockDevsSet.List() { - ephemeralBlockDev := ephemeralBlockDevI.(map[string]interface{}) - blockDev := &ec2.BlockDeviceMapping{ - DeviceName: aws.String(ephemeralBlockDev["device_name"].(string)), - VirtualName: aws.String(ephemeralBlockDev["virtual_name"].(string)), - } - req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev) + + if v, ok := d.GetOk("ephemeral_block_device"); ok && v.(*schema.Set).Len() > 0 { + req.BlockDeviceMappings = append(req.BlockDeviceMappings, expandEc2BlockDeviceMappingsForAmiEphemeralBlockDevice(v.(*schema.Set).List())...) } res, err := client.RegisterImage(req) @@ -394,40 +392,16 @@ func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error { d.Set("arn", imageArn) - var ebsBlockDevs []map[string]interface{} - var ephemeralBlockDevs []map[string]interface{} - - for _, blockDev := range image.BlockDeviceMappings { - if blockDev.Ebs != nil { - ebsBlockDev := map[string]interface{}{ - "device_name": *blockDev.DeviceName, - "delete_on_termination": *blockDev.Ebs.DeleteOnTermination, - "encrypted": *blockDev.Ebs.Encrypted, - "iops": 0, - "volume_size": int(*blockDev.Ebs.VolumeSize), - "volume_type": *blockDev.Ebs.VolumeType, - } - if blockDev.Ebs.Iops != nil { - ebsBlockDev["iops"] = int(*blockDev.Ebs.Iops) - } - // The snapshot ID might not be set. - if blockDev.Ebs.SnapshotId != nil { - ebsBlockDev["snapshot_id"] = *blockDev.Ebs.SnapshotId - } - ebsBlockDevs = append(ebsBlockDevs, ebsBlockDev) - } else { - ephemeralBlockDevs = append(ephemeralBlockDevs, map[string]interface{}{ - "device_name": *blockDev.DeviceName, - "virtual_name": *blockDev.VirtualName, - }) - } + if err := d.Set("ebs_block_device", flattenEc2BlockDeviceMappingsForAmiEbsBlockDevice(image.BlockDeviceMappings)); err != nil { + return fmt.Errorf("error setting ebs_block_device: %w", err) } - d.Set("ebs_block_device", ebsBlockDevs) - d.Set("ephemeral_block_device", ephemeralBlockDevs) + if err := d.Set("ephemeral_block_device", flattenEc2BlockDeviceMappingsForAmiEphemeralBlockDevice(image.BlockDeviceMappings)); err != nil { + return fmt.Errorf("error setting ephemeral_block_device: %w", err) + } if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(image.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %s", err) + return fmt.Errorf("error setting tags: %w", err) } return nil @@ -568,3 +542,224 @@ func resourceAwsAmiWaitForAvailable(timeout time.Duration, id string, client *ec } return info.(*ec2.Image), nil } + +func expandEc2BlockDeviceMappingForAmiEbsBlockDevice(tfMap map[string]interface{}) *ec2.BlockDeviceMapping { + if tfMap == nil { + return nil + } + + apiObject := &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + } + + if v, ok := tfMap["delete_on_termination"].(bool); ok { + apiObject.Ebs.DeleteOnTermination = aws.Bool(v) + } + + if v, ok := tfMap["device_name"].(string); ok && v != "" { + apiObject.DeviceName = aws.String(v) + } + + if v, ok := tfMap["iops"].(int); ok && v != 0 { + apiObject.Ebs.Iops = aws.Int64(int64(v)) + } + + // "Parameter encrypted is invalid. You cannot specify the encrypted flag if specifying a snapshot id in a block device mapping." + if v, ok := tfMap["snapshot_id"].(string); ok && v != "" { + apiObject.Ebs.SnapshotId = aws.String(v) + } else if v, ok := tfMap["encrypted"].(bool); ok { + apiObject.Ebs.Encrypted = aws.Bool(v) + } + + if v, ok := tfMap["throughput"].(int); ok && v != 0 { + apiObject.Ebs.Throughput = aws.Int64(int64(v)) + } + + if v, ok := tfMap["volume_size"].(int); ok && v != 0 { + apiObject.Ebs.VolumeSize = aws.Int64(int64(v)) + } + + if v, ok := tfMap["volume_type"].(string); ok && v != "" { + apiObject.Ebs.VolumeType = aws.String(v) + } + + return apiObject +} + +func expandEc2BlockDeviceMappingsForAmiEbsBlockDevice(tfList []interface{}) []*ec2.BlockDeviceMapping { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*ec2.BlockDeviceMapping + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandEc2BlockDeviceMappingForAmiEbsBlockDevice(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenEc2BlockDeviceMappingForAmiEbsBlockDevice(apiObject *ec2.BlockDeviceMapping) map[string]interface{} { + if apiObject == nil { + return nil + } + + if apiObject.Ebs == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Ebs.DeleteOnTermination; v != nil { + tfMap["delete_on_termination"] = aws.BoolValue(v) + } + + if v := apiObject.DeviceName; v != nil { + tfMap["device_name"] = aws.StringValue(v) + } + + if v := apiObject.Ebs.Encrypted; v != nil { + tfMap["encrypted"] = aws.BoolValue(v) + } + + if v := apiObject.Ebs.Iops; v != nil { + tfMap["iops"] = aws.Int64Value(v) + } + + if v := apiObject.Ebs.SnapshotId; v != nil { + tfMap["snapshot_id"] = aws.StringValue(v) + } + + if v := apiObject.Ebs.Throughput; v != nil { + tfMap["throughput"] = aws.Int64Value(v) + } + + if v := apiObject.Ebs.VolumeSize; v != nil { + tfMap["volume_size"] = aws.Int64Value(v) + } + + if v := apiObject.Ebs.VolumeType; v != nil { + tfMap["volume_type"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenEc2BlockDeviceMappingsForAmiEbsBlockDevice(apiObjects []*ec2.BlockDeviceMapping) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + if apiObject.Ebs == nil { + continue + } + + tfList = append(tfList, flattenEc2BlockDeviceMappingForAmiEbsBlockDevice(apiObject)) + } + + return tfList +} + +func expandEc2BlockDeviceMappingForAmiEphemeralBlockDevice(tfMap map[string]interface{}) *ec2.BlockDeviceMapping { + if tfMap == nil { + return nil + } + + apiObject := &ec2.BlockDeviceMapping{} + + if v, ok := tfMap["device_name"].(string); ok && v != "" { + apiObject.DeviceName = aws.String(v) + } + + if v, ok := tfMap["virtual_name"].(string); ok && v != "" { + apiObject.VirtualName = aws.String(v) + } + + return apiObject +} + +func expandEc2BlockDeviceMappingsForAmiEphemeralBlockDevice(tfList []interface{}) []*ec2.BlockDeviceMapping { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*ec2.BlockDeviceMapping + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandEc2BlockDeviceMappingForAmiEphemeralBlockDevice(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenEc2BlockDeviceMappingForAmiEphemeralBlockDevice(apiObject *ec2.BlockDeviceMapping) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.DeviceName; v != nil { + tfMap["device_name"] = aws.StringValue(v) + } + + if v := apiObject.VirtualName; v != nil { + tfMap["virtual_name"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenEc2BlockDeviceMappingsForAmiEphemeralBlockDevice(apiObjects []*ec2.BlockDeviceMapping) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + if apiObject.Ebs != nil { + continue + } + + tfList = append(tfList, flattenEc2BlockDeviceMappingForAmiEphemeralBlockDevice(apiObject)) + } + + return tfList +} diff --git a/aws/resource_aws_ami_copy.go b/aws/resource_aws_ami_copy.go index 1ed5bb2ea93..07e0e92ebec 100644 --- a/aws/resource_aws_ami_copy.go +++ b/aws/resource_aws_ami_copy.go @@ -67,6 +67,11 @@ func resourceAwsAmiCopy() *schema.Resource { Computed: true, }, + "throughput": { + Type: schema.TypeInt, + Computed: true, + }, + "volume_size": { Type: schema.TypeInt, Computed: true, @@ -215,17 +220,16 @@ func resourceAwsAmiCopyCreate(d *schema.ResourceData, meta interface{}) error { return err } - id := *res.ImageId - d.SetId(id) + d.SetId(aws.StringValue(res.ImageId)) d.Set("manage_ebs_snapshots", true) if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - if err := keyvaluetags.Ec2CreateTags(client, id, v); err != nil { + if err := keyvaluetags.Ec2CreateTags(client, d.Id(), v); err != nil { return fmt.Errorf("error adding tags: %s", err) } } - _, err = resourceAwsAmiWaitForAvailable(d.Timeout(schema.TimeoutCreate), id, client) + _, err = resourceAwsAmiWaitForAvailable(d.Timeout(schema.TimeoutCreate), d.Id(), client) if err != nil { return err } diff --git a/aws/resource_aws_ami_from_instance.go b/aws/resource_aws_ami_from_instance.go index 2dc7dbbfb9c..9cdadc90815 100644 --- a/aws/resource_aws_ami_from_instance.go +++ b/aws/resource_aws_ami_from_instance.go @@ -67,6 +67,11 @@ func resourceAwsAmiFromInstance() *schema.Resource { Computed: true, }, + "throughput": { + Type: schema.TypeInt, + Computed: true, + }, + "volume_size": { Type: schema.TypeInt, Computed: true, @@ -197,17 +202,16 @@ func resourceAwsAmiFromInstanceCreate(d *schema.ResourceData, meta interface{}) return err } - id := *res.ImageId - d.SetId(id) + d.SetId(aws.StringValue(res.ImageId)) d.Set("manage_ebs_snapshots", true) if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { - if err := keyvaluetags.Ec2CreateTags(client, id, v); err != nil { + if err := keyvaluetags.Ec2CreateTags(client, d.Id(), v); err != nil { return fmt.Errorf("error adding tags: %s", err) } } - _, err = resourceAwsAmiWaitForAvailable(d.Timeout(schema.TimeoutCreate), id, client) + _, err = resourceAwsAmiWaitForAvailable(d.Timeout(schema.TimeoutCreate), d.Id(), client) if err != nil { return err } diff --git a/aws/resource_aws_ami_test.go b/aws/resource_aws_ami_test.go index 3e6e399629a..730c052688d 100644 --- a/aws/resource_aws_ami_test.go +++ b/aws/resource_aws_ami_test.go @@ -17,6 +17,7 @@ import ( func TestAccAWSAMI_basic(t *testing.T) { var ami ec2.Image resourceName := "aws_ami.test" + snapshotResourceName := "aws_ebs_snapshot.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ @@ -25,15 +26,32 @@ func TestAccAWSAMI_basic(t *testing.T) { CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ { - Config: testAccAmiConfigBasic(rName, 8), + Config: testAccAmiConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), + resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "true", + "device_name": "/dev/sda1", + "encrypted": "false", + "iops": "0", + "throughput": "0", + "volume_size": "8", + "volume_type": "standard", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "ebs_block_device.*.snapshot_id", snapshotResourceName, "id"), resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), + resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), resource.TestCheckResourceAttr(resourceName, "name", rName), - testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), + resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), + resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), - resource.TestMatchResourceAttr(resourceName, "root_snapshot_id", regexp.MustCompile("^snap-")), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, @@ -52,6 +70,7 @@ func TestAccAWSAMI_basic(t *testing.T) { func TestAccAWSAMI_description(t *testing.T) { var ami ec2.Image resourceName := "aws_ami.test" + snapshotResourceName := "aws_ebs_snapshot.test" rName := acctest.RandomWithPrefix("tf-acc-test") desc := acctest.RandomWithPrefix("desc") descUpdated := acctest.RandomWithPrefix("desc-updated") @@ -62,10 +81,33 @@ func TestAccAWSAMI_description(t *testing.T) { CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ { - Config: testAccAmiConfigDesc(rName, desc, 8), + Config: testAccAmiConfigDesc(rName, desc), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), + resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), resource.TestCheckResourceAttr(resourceName, "description", desc), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "true", + "device_name": "/dev/sda1", + "encrypted": "false", + "iops": "0", + "throughput": "0", + "volume_size": "8", + "volume_type": "standard", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "ebs_block_device.*.snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), + resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), + resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), + resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), + resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { @@ -77,10 +119,33 @@ func TestAccAWSAMI_description(t *testing.T) { }, }, { - Config: testAccAmiConfigDesc(rName, descUpdated, 8), + Config: testAccAmiConfigDesc(rName, descUpdated), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), + resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), resource.TestCheckResourceAttr(resourceName, "description", descUpdated), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "true", + "device_name": "/dev/sda1", + "encrypted": "false", + "iops": "0", + "throughput": "0", + "volume_size": "8", + "volume_type": "standard", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "ebs_block_device.*.snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), + resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), + resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), + resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), + resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, }, @@ -98,7 +163,7 @@ func TestAccAWSAMI_disappears(t *testing.T) { CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ { - Config: testAccAmiConfigBasic(rName, 8), + Config: testAccAmiConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), testAccCheckResourceDisappears(testAccProvider, resourceAwsAmi(), resourceName), @@ -109,9 +174,10 @@ func TestAccAWSAMI_disappears(t *testing.T) { }) } -func TestAccAWSAMI_tags(t *testing.T) { +func TestAccAWSAMI_EphemeralBlockDevices(t *testing.T) { var ami ec2.Image resourceName := "aws_ami.test" + snapshotResourceName := "aws_ebs_snapshot.test" rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ @@ -120,11 +186,40 @@ func TestAccAWSAMI_tags(t *testing.T) { CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ { - Config: testAccAmiConfigTags1(rName, "key1", "value1", 8), + Config: testAccAmiConfigEphemeralBlockDevices(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "true", + "device_name": "/dev/sda1", + "encrypted": "false", + "iops": "0", + "throughput": "0", + "volume_size": "8", + "volume_type": "standard", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "ebs_block_device.*.snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "ena_support", "true"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ephemeral_block_device.*", map[string]string{ + "device_name": "/dev/sdb", + "virtual_name": "ephemeral0", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ephemeral_block_device.*", map[string]string{ + "device_name": "/dev/sdc", + "virtual_name": "ephemeral1", + }), + resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), + resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), + resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), + resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { @@ -135,54 +230,88 @@ func TestAccAWSAMI_tags(t *testing.T) { "manage_ebs_snapshots", }, }, + }, + }) +} + +func TestAccAWSAMI_Gp3BlockDevice(t *testing.T) { + var ami ec2.Image + resourceName := "aws_ami.test" + snapshotResourceName := "aws_ebs_snapshot.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAmiDestroy, + Steps: []resource.TestStep{ { - Config: testAccAmiConfigTags2(rName, "key1", "value1updated", "key2", "value2", 8), + Config: testAccAmiConfigGp3BlockDevice(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), - resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), - resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), + testAccMatchResourceAttrRegionalARNNoAccount(resourceName, "arn", "ec2", regexp.MustCompile(`image/ami-.+`)), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "ebs_block_device.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "true", + "device_name": "/dev/sda1", + "encrypted": "false", + "iops": "0", + "throughput": "0", + "volume_size": "8", + "volume_type": "standard", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "ebs_block_device.*.snapshot_id", snapshotResourceName, "id"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{ + "delete_on_termination": "false", + "device_name": "/dev/sdb", + "encrypted": "true", + "iops": "100", + "throughput": "500", + "volume_size": "10", + "volume_type": "gp3", + }), + resource.TestCheckResourceAttr(resourceName, "ena_support", "false"), + resource.TestCheckResourceAttr(resourceName, "ephemeral_block_device.#", "0"), + resource.TestCheckResourceAttr(resourceName, "kernel_id", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "ramdisk_id", ""), + resource.TestCheckResourceAttr(resourceName, "root_device_name", "/dev/sda1"), + resource.TestCheckResourceAttrPair(resourceName, "root_snapshot_id", snapshotResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "sriov_net_support", "simple"), + resource.TestCheckResourceAttr(resourceName, "virtualization_type", "hvm"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), }, { - Config: testAccAmiConfigTags1(rName, "key2", "value2", 8), - Check: resource.ComposeTestCheckFunc( - testAccCheckAmiExists(resourceName, &ami), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - ), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "manage_ebs_snapshots", + }, }, }, }) } -func TestAccAWSAMI_snapshotSize(t *testing.T) { +func TestAccAWSAMI_tags(t *testing.T) { var ami ec2.Image - var bd ec2.BlockDeviceMapping resourceName := "aws_ami.test" rName := acctest.RandomWithPrefix("tf-acc-test") - expectedDevice := &ec2.EbsBlockDevice{ - DeleteOnTermination: aws.Bool(true), - Encrypted: aws.Bool(false), - Iops: aws.Int64(0), - VolumeSize: aws.Int64(20), - VolumeType: aws.String("standard"), - } - resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckAmiDestroy, Steps: []resource.TestStep{ { - Config: testAccAmiConfigBasic(rName, 20), + Config: testAccAmiConfigTags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckAmiExists(resourceName, &ami), - testAccCheckAmiBlockDevice(&ami, &bd, "/dev/sda1"), - testAccCheckAmiEbsBlockDevice(&bd, expectedDevice), - resource.TestCheckResourceAttr(resourceName, "architecture", "x86_64"), - resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), }, { @@ -193,6 +322,23 @@ func TestAccAWSAMI_snapshotSize(t *testing.T) { "manage_ebs_snapshots", }, }, + { + Config: testAccAmiConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAmiExists(resourceName, &ami), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAmiConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAmiExists(resourceName, &ami), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, }, }) } @@ -270,80 +416,16 @@ func testAccCheckAmiExists(n string, ami *ec2.Image) resource.TestCheckFunc { } } -func testAccCheckAmiBlockDevice(ami *ec2.Image, blockDevice *ec2.BlockDeviceMapping, n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - devices := make(map[string]*ec2.BlockDeviceMapping) - for _, device := range ami.BlockDeviceMappings { - devices[*device.DeviceName] = device - } - - // Check if the block device exists - if _, ok := devices[n]; !ok { - return fmt.Errorf("block device doesn't exist: %s", n) - } - - *blockDevice = *devices[n] - return nil - } -} - -func testAccCheckAmiEbsBlockDevice(bd *ec2.BlockDeviceMapping, ed *ec2.EbsBlockDevice) resource.TestCheckFunc { - return func(s *terraform.State) error { - // Test for things that ed has, don't care about unset values - cd := bd.Ebs - if ed.VolumeType != nil { - if *ed.VolumeType != *cd.VolumeType { - return fmt.Errorf("Volume type mismatch. Expected: %s Got: %s", - *ed.VolumeType, *cd.VolumeType) - } - } - if ed.DeleteOnTermination != nil { - if *ed.DeleteOnTermination != *cd.DeleteOnTermination { - return fmt.Errorf("DeleteOnTermination mismatch. Expected: %t Got: %t", - *ed.DeleteOnTermination, *cd.DeleteOnTermination) - } - } - if ed.Encrypted != nil { - if *ed.Encrypted != *cd.Encrypted { - return fmt.Errorf("Encrypted mismatch. Expected: %t Got: %t", - *ed.Encrypted, *cd.Encrypted) - } - } - // Integer defaults need to not be `0` so we don't get a panic - if ed.Iops != nil && *ed.Iops != 0 { - if *ed.Iops != *cd.Iops { - return fmt.Errorf("IOPS mismatch. Expected: %d Got: %d", - *ed.Iops, *cd.Iops) - } - } - if ed.VolumeSize != nil && *ed.VolumeSize != 0 { - if *ed.VolumeSize != *cd.VolumeSize { - return fmt.Errorf("Volume Size mismatch. Expected: %d Got: %d", - *ed.VolumeSize, *cd.VolumeSize) - } - } - - return nil - } -} - -func testAccAmiConfigBase(rName string, size int) string { - return fmt.Sprintf(` -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - +func testAccAmiConfigBase(rName string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` resource "aws_ebs_volume" "test" { availability_zone = data.aws_availability_zones.available.names[0] - size = %d + size = 8 tags = { - Name = "%[2]s" + Name = %[1]q } } @@ -351,15 +433,16 @@ resource "aws_ebs_snapshot" "test" { volume_id = aws_ebs_volume.test.id tags = { - Name = "%[2]s" + Name = %[1]q } } - -`, size, rName) +`, rName)) } -func testAccAmiConfigBasic(rName string, size int) string { - return testAccAmiConfigBase(rName, size) + fmt.Sprintf(` +func testAccAmiConfigBasic(rName string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` resource "aws_ami" "test" { ena_support = true name = %[1]q @@ -371,11 +454,13 @@ resource "aws_ami" "test" { snapshot_id = aws_ebs_snapshot.test.id } } -`, rName) +`, rName)) } -func testAccAmiConfigDesc(rName, desc string, size int) string { - return testAccAmiConfigBase(rName, size) + fmt.Sprintf(` +func testAccAmiConfigDesc(rName, desc string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` resource "aws_ami" "test" { ena_support = true name = %[1]q @@ -388,11 +473,69 @@ resource "aws_ami" "test" { snapshot_id = aws_ebs_snapshot.test.id } } -`, rName, desc) +`, rName, desc)) +} + +func testAccAmiConfigEphemeralBlockDevices(rName string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` +resource "aws_ami" "test" { + ena_support = true + name = %[1]q + root_device_name = "/dev/sda1" + virtualization_type = "hvm" + + ebs_block_device { + device_name = "/dev/sda1" + snapshot_id = aws_ebs_snapshot.test.id + } + + ephemeral_block_device { + device_name = "/dev/sdb" + virtual_name = "ephemeral0" + } + + ephemeral_block_device { + device_name = "/dev/sdc" + virtual_name = "ephemeral1" + } +} +`, rName)) +} + +func testAccAmiConfigGp3BlockDevice(rName string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` +resource "aws_ami" "test" { + ena_support = false + name = %[1]q + root_device_name = "/dev/sda1" + virtualization_type = "hvm" + + ebs_block_device { + device_name = "/dev/sda1" + snapshot_id = aws_ebs_snapshot.test.id + } + + ebs_block_device { + delete_on_termination = false + device_name = "/dev/sdb" + encrypted = true + iops = 100 + throughput = 500 + volume_size = 10 + volume_type = "gp3" + } +} +`, rName)) } -func testAccAmiConfigTags1(rName, tagKey1, tagValue1 string, size int) string { - return testAccAmiConfigBase(rName, size) + fmt.Sprintf(` +func testAccAmiConfigTags1(rName, tagKey1, tagValue1 string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` resource "aws_ami" "test" { ena_support = true name = %[1]q @@ -408,11 +551,13 @@ resource "aws_ami" "test" { %[2]q = %[3]q } } -`, rName, tagKey1, tagValue1) +`, rName, tagKey1, tagValue1)) } -func testAccAmiConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string, size int) string { - return testAccAmiConfigBase(rName, size) + fmt.Sprintf(` +func testAccAmiConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return composeConfig( + testAccAmiConfigBase(rName), + fmt.Sprintf(` resource "aws_ami" "test" { ena_support = true name = %[1]q @@ -429,5 +574,5 @@ resource "aws_ami" "test" { %[4]q = %[5]q } } -`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) } diff --git a/website/docs/d/ami.html.markdown b/website/docs/d/ami.html.markdown index e8e99a13444..4f39df2df54 100644 --- a/website/docs/d/ami.html.markdown +++ b/website/docs/d/ami.html.markdown @@ -82,6 +82,7 @@ interpolation. not a provisioned IOPS image, otherwise the supported IOPS count. * `block_device_mappings.#.ebs.snapshot_id` - The ID of the snapshot. * `block_device_mappings.#.ebs.volume_size` - The size of the volume, in GiB. + * `block_device_mappings.#.ebs.throughput` - The throughput that the EBS volume supports, in MiB/s. * `block_device_mappings.#.ebs.volume_type` - The volume type. * `block_device_mappings.#.no_device` - Suppresses the specified device included in the block device mapping of the AMI. diff --git a/website/docs/r/ami.html.markdown b/website/docs/r/ami.html.markdown index f013c5f4d56..21b8d543bbc 100644 --- a/website/docs/r/ami.html.markdown +++ b/website/docs/r/ami.html.markdown @@ -74,16 +74,16 @@ Nested `ebs_block_device` blocks have the following structure: * `delete_on_termination` - (Optional) Boolean controlling whether the EBS volumes created to support each created instance will be deleted once that instance is terminated. * `encrypted` - (Optional) Boolean controlling whether the created EBS volumes will be encrypted. Can't be used with `snapshot_id`. -* `iops` - (Required only when `volume_type` is "io1/io2") Number of I/O operations per second the +* `iops` - (Required only when `volume_type` is `io1` or `io2`) Number of I/O operations per second the created volumes will support. * `snapshot_id` - (Optional) The id of an EBS snapshot that will be used to initialize the created EBS volumes. If set, the `volume_size` attribute must be at least as large as the referenced snapshot. +* `throughput` - (Optional) The throughput that the EBS volume supports, in MiB/s. Only valid for `volume_type` of `gp3`. * `volume_size` - (Required unless `snapshot_id` is set) The size of created volumes in GiB. If `snapshot_id` is set and `volume_size` is omitted then the volume will have the same size as the selected snapshot. -* `volume_type` - (Optional) The type of EBS volume to create. Can be one of "standard" (the - default), "io1", "io2" or "gp2". +* `volume_type` - (Optional) The type of EBS volume to create. Can be `standard`, `gp2`, `gp3`, `io1`, `io2`, `sc1` or `st1` (Default: `standard`). * `kms_key_id` - (Optional) The full ARN of the AWS Key Management Service (AWS KMS) CMK to use when encrypting the snapshots of an image during a copy operation. This parameter is only required if you want to use a non-default CMK; if this parameter is not specified, the default CMK for EBS is used