Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EC2: API idempotency #27561

Merged
merged 13 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changelog/27561.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
```release-note:bug
resource/aws_instance: Use EC2 API idempotency to ensure that only a single Instance is created
```

```release-note:enhancement
resource/aws_ami_copy: Add `imds_support` attribute
```

```release-note:enhancement
resource/aws_ami_from_instance: Add `imds_support` attribute
```
3 changes: 2 additions & 1 deletion internal/service/ec2/ebs_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
Expand Down Expand Up @@ -122,6 +123,7 @@ func resourceEBSVolumeCreate(d *schema.ResourceData, meta interface{}) error {

input := &ec2.CreateVolumeInput{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
ClientToken: aws.String(resource.UniqueId()),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeVolume),
}

Expand Down Expand Up @@ -161,7 +163,6 @@ func resourceEBSVolumeCreate(d *schema.ResourceData, meta interface{}) error {
input.VolumeType = aws.String(value.(string))
}

log.Printf("[DEBUG] Creating EBS Volume: %s", input)
output, err := conn.CreateVolume(input)

if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/service/ec2/ec2_ami.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ const (
func ResourceAMI() *schema.Resource {
return &schema.Resource{
Create: resourceAMICreate,
// 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
// is created.
// 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 is created.
Read: resourceAMIRead,
Update: resourceAMIUpdate,
Delete: resourceAMIDelete,
Expand All @@ -48,6 +47,7 @@ func ResourceAMI() *schema.Resource {
Delete: schema.DefaultTimeout(amiDeleteTimeout),
},

// Keep in sync with aws_ami_copy's and aws_ami_from_instance's schemas.
Schema: map[string]*schema.Schema{
"architecture": {
Type: schema.TypeString,
Expand Down
13 changes: 10 additions & 3 deletions internal/service/ec2/ec2_ami_copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
Expand All @@ -30,6 +31,7 @@ func ResourceAMICopy() *schema.Resource {
Delete: schema.DefaultTimeout(amiDeleteTimeout),
},

// Keep in sync with aws_ami's schema.
Schema: map[string]*schema.Schema{
"architecture": {
Type: schema.TypeString,
Expand Down Expand Up @@ -168,6 +170,10 @@ func ResourceAMICopy() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"imds_support": {
Type: schema.TypeString,
Computed: true,
},
"kernel_id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -262,6 +268,7 @@ func resourceAMICopyCreate(d *schema.ResourceData, meta interface{}) error {
name := d.Get("name").(string)
sourceImageID := d.Get("source_ami_id").(string)
input := &ec2.CopyImageInput{
ClientToken: aws.String(resource.UniqueId()),
Description: aws.String(d.Get("description").(string)),
Encrypted: aws.Bool(d.Get("encrypted").(bool)),
Name: aws.String(name),
Expand All @@ -280,20 +287,20 @@ func resourceAMICopyCreate(d *schema.ResourceData, meta interface{}) error {
output, err := conn.CopyImage(input)

if err != nil {
return fmt.Errorf("error creating EC2 AMI (%s) from source EC2 AMI (%s): %w", name, sourceImageID, err)
return fmt.Errorf("creating EC2 AMI (%s) from source EC2 AMI (%s): %w", name, sourceImageID, err)
}

d.SetId(aws.StringValue(output.ImageId))
d.Set("manage_ebs_snapshots", true)

if len(tags) > 0 {
if err := CreateTags(conn, d.Id(), tags); err != nil {
return fmt.Errorf("error adding tags: %s", err)
return fmt.Errorf("adding tags: %w", err)
}
}

if _, err := WaitImageAvailable(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
return fmt.Errorf("error waiting for EC2 AMI (%s) create: %w", d.Id(), err)
return fmt.Errorf("waiting for EC2 AMI (%s) create: %w", d.Id(), err)
}

if v, ok := d.GetOk("deprecation_time"); ok {
Expand Down
9 changes: 7 additions & 2 deletions internal/service/ec2/ec2_ami_from_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func ResourceAMIFromInstance() *schema.Resource {
Delete: schema.DefaultTimeout(amiDeleteTimeout),
},

// Keep in sync with aws_ami's schema.
Schema: map[string]*schema.Schema{
"architecture": {
Type: schema.TypeString,
Expand Down Expand Up @@ -157,6 +158,10 @@ func ResourceAMIFromInstance() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"imds_support": {
Type: schema.TypeString,
Computed: true,
},
"kernel_id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -254,14 +259,14 @@ func resourceAMIFromInstanceCreate(d *schema.ResourceData, meta interface{}) err
output, err := conn.CreateImage(input)

if err != nil {
return fmt.Errorf("error creating EC2 AMI (%s) from EC2 Instance (%s): %w", name, instanceID, err)
return fmt.Errorf("creating EC2 AMI (%s) from EC2 Instance (%s): %w", name, instanceID, err)
}

d.SetId(aws.StringValue(output.ImageId))
d.Set("manage_ebs_snapshots", true)

if _, err := WaitImageAvailable(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
return fmt.Errorf("error waiting for EC2 AMI (%s) create: %w", d.Id(), err)
return fmt.Errorf("waiting for EC2 AMI (%s) create: %w", d.Id(), err)
}

if v, ok := d.GetOk("deprecation_time"); ok {
Expand Down
23 changes: 12 additions & 11 deletions internal/service/ec2/ec2_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
Expand Down Expand Up @@ -84,6 +85,7 @@ func resourceHostCreate(d *schema.ResourceData, meta interface{}) error {
input := &ec2.AllocateHostsInput{
AutoPlacement: aws.String(d.Get("auto_placement").(string)),
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
ClientToken: aws.String(resource.UniqueId()),
HostRecovery: aws.String(d.Get("host_recovery").(string)),
Quantity: aws.Int64(1),
}
Expand All @@ -104,17 +106,16 @@ func resourceHostCreate(d *schema.ResourceData, meta interface{}) error {
input.TagSpecifications = tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeDedicatedHost)
}

log.Printf("[DEBUG] Creating EC2 Host: %s", input)
output, err := conn.AllocateHosts(input)

if err != nil {
return fmt.Errorf("error allocating EC2 Host: %w", err)
return fmt.Errorf("allocating EC2 Host: %w", err)
}

d.SetId(aws.StringValue(output.HostIds[0]))

if _, err := WaitHostCreated(conn, d.Id()); err != nil {
return fmt.Errorf("error waiting for EC2 Host (%s) create: %w", d.Id(), err)
return fmt.Errorf("waiting for EC2 Host (%s) create: %w", d.Id(), err)
}

return resourceHostRead(d, meta)
Expand All @@ -134,7 +135,7 @@ func resourceHostRead(d *schema.ResourceData, meta interface{}) error {
}

if err != nil {
return fmt.Errorf("error reading EC2 Host (%s): %w", d.Id(), err)
return fmt.Errorf("reading EC2 Host (%s): %w", d.Id(), err)
}

arn := arn.ARN{
Expand All @@ -157,11 +158,11 @@ func resourceHostRead(d *schema.ResourceData, meta interface{}) error {

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %w", err)
return fmt.Errorf("setting tags: %w", err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return fmt.Errorf("error setting tags_all: %w", err)
return fmt.Errorf("setting tags_all: %w", err)
}

return nil
Expand Down Expand Up @@ -198,18 +199,18 @@ func resourceHostUpdate(d *schema.ResourceData, meta interface{}) error {
}

if err != nil {
return fmt.Errorf("error modifying EC2 Host (%s): %w", d.Id(), err)
return fmt.Errorf("modifying EC2 Host (%s): %w", d.Id(), err)
}

if _, err := WaitHostUpdated(conn, d.Id()); err != nil {
return fmt.Errorf("error waiting for EC2 Host (%s) update: %w", d.Id(), err)
return fmt.Errorf("waiting for EC2 Host (%s) update: %w", d.Id(), err)
}
}

if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")
if err := UpdateTags(conn, d.Id(), o, n); err != nil {
return fmt.Errorf("error updating EC2 Host (%s) tags: %w", d.Id(), err)
return fmt.Errorf("updating EC2 Host (%s) tags: %w", d.Id(), err)
}
}

Expand All @@ -233,11 +234,11 @@ func resourceHostDelete(d *schema.ResourceData, meta interface{}) error {
}

if err != nil {
return fmt.Errorf("error releasing EC2 Host (%s): %w", d.Id(), err)
return fmt.Errorf("releasing EC2 Host (%s): %w", d.Id(), err)
}

if _, err := WaitHostDeleted(conn, d.Id()); err != nil {
return fmt.Errorf("error waiting for EC2 Host (%s) delete: %w", d.Id(), err)
return fmt.Errorf("waiting for EC2 Host (%s) delete: %w", d.Id(), err)
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/service/ec2/ec2_host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func testAccCheckHostDestroy(s *terraform.State) error {
func testAccHostConfig_basic() string {
return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), `
resource "aws_ec2_host" "test" {
availability_zone = data.aws_availability_zones.available.names[0]
availability_zone = data.aws_availability_zones.available.names[1]
instance_type = "a1.large"
}
`)
Expand Down
1 change: 1 addition & 0 deletions internal/service/ec2/ec2_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ func resourceInstanceCreate(d *schema.ResourceData, meta interface{}) error {
input := &ec2.RunInstancesInput{
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
CapacityReservationSpecification: instanceOpts.CapacityReservationSpecification,
ClientToken: aws.String(resource.UniqueId()),
CpuOptions: instanceOpts.CpuOptions,
CreditSpecification: instanceOpts.CreditSpecification,
DisableApiTermination: instanceOpts.DisableAPITermination,
Expand Down
8 changes: 4 additions & 4 deletions internal/service/ec2/ec2_spot_fleet_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,14 +836,14 @@ func resourceSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) er

// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData
spotFleetConfig := &ec2.SpotFleetRequestConfigData{
ClientToken: aws.String(resource.UniqueId()),
IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)),
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behaviour").(string)),
ReplaceUnhealthyInstances: aws.Bool(d.Get("replace_unhealthy_instances").(bool)),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotFleetRequest),
TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))),
ClientToken: aws.String(resource.UniqueId()),
TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
ReplaceUnhealthyInstances: aws.Bool(d.Get("replace_unhealthy_instances").(bool)),
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behaviour").(string)),
Type: aws.String(d.Get("fleet_type").(string)),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotFleetRequest),
}

if launchSpecificationOk {
Expand Down
14 changes: 7 additions & 7 deletions internal/service/ec2/ec2_spot_instance_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func ResourceSpotInstanceRequest() *schema.Resource {
Read: resourceSpotInstanceRequestRead,
Delete: resourceSpotInstanceRequestDelete,
Update: resourceSpotInstanceRequestUpdate,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Expand Down Expand Up @@ -140,16 +141,12 @@ func resourceSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{})
}

spotOpts := &ec2.RequestSpotInstancesInput{
SpotPrice: aws.String(d.Get("spot_price").(string)),
Type: aws.String(d.Get("spot_type").(string)),
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behavior").(string)),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotInstancesRequest),

ClientToken: aws.String(resource.UniqueId()),
// Though the AWS API supports creating spot instance requests for multiple
// instances, for TF purposes we fix this to one instance per request.
// Users can get equivalent behavior out of TF's "count" meta-parameter.
InstanceCount: aws.Int64(1),

InstanceCount: aws.Int64(1),
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behavior").(string)),
LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
EbsOptimized: instanceOpts.EBSOptimized,
Expand All @@ -164,6 +161,9 @@ func resourceSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{})
UserData: instanceOpts.UserData64,
NetworkInterfaces: instanceOpts.NetworkInterfaces,
},
SpotPrice: aws.String(d.Get("spot_price").(string)),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotInstancesRequest),
Type: aws.String(d.Get("spot_type").(string)),
}

if v, ok := d.GetOk("block_duration_minutes"); ok {
Expand Down
15 changes: 8 additions & 7 deletions internal/service/ec2/vpc_egress_only_internet_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
Expand Down Expand Up @@ -45,15 +46,15 @@ func resourceEgressOnlyInternetGatewayCreate(d *schema.ResourceData, meta interf
tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{})))

input := &ec2.CreateEgressOnlyInternetGatewayInput{
ClientToken: aws.String(resource.UniqueId()),
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeEgressOnlyInternetGateway),
VpcId: aws.String(d.Get("vpc_id").(string)),
}

log.Printf("[DEBUG] Creating EC2 Egress-only Internet Gateway: %s", input)
output, err := conn.CreateEgressOnlyInternetGateway(input)

if err != nil {
return fmt.Errorf("error creating EC2 Egress-only Internet Gateway: %w", err)
return fmt.Errorf("creating EC2 Egress-only Internet Gateway: %w", err)
}

d.SetId(aws.StringValue(output.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId))
Expand All @@ -77,7 +78,7 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac
}

if err != nil {
return fmt.Errorf("error reading EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
return fmt.Errorf("reading EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
}

ig := outputRaw.(*ec2.EgressOnlyInternetGateway)
Expand All @@ -92,11 +93,11 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return fmt.Errorf("error setting tags: %w", err)
return fmt.Errorf("setting tags: %w", err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return fmt.Errorf("error setting tags_all: %w", err)
return fmt.Errorf("setting tags_all: %w", err)
}

return nil
Expand All @@ -109,7 +110,7 @@ func resourceEgressOnlyInternetGatewayUpdate(d *schema.ResourceData, meta interf
o, n := d.GetChange("tags_all")

if err := UpdateTags(conn, d.Id(), o, n); err != nil {
return fmt.Errorf("error updating EC2 Egress-only Internet Gateway (%s) tags: %w", d.Id(), err)
return fmt.Errorf("updating EC2 Egress-only Internet Gateway (%s) tags: %w", d.Id(), err)
}
}

Expand All @@ -129,7 +130,7 @@ func resourceEgressOnlyInternetGatewayDelete(d *schema.ResourceData, meta interf
}

if err != nil {
return fmt.Errorf("error deleting EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
return fmt.Errorf("deleting EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
}

return nil
Expand Down
Loading