Skip to content

Commit

Permalink
resource/aws_spot_instance_request: Add import support and refactor t…
Browse files Browse the repository at this point in the history
…ests (#12787)

Output from acceptance testing in AWS Commercial:

```
--- PASS: TestAccAWSEIPAssociation_spotInstance (324.66s)

--- PASS: TestAccAWSSpotInstanceRequest_basic (343.84s)
--- PASS: TestAccAWSSpotInstanceRequest_disappears (313.80s)
--- PASS: TestAccAWSSpotInstanceRequest_getPasswordData (212.62s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptHibernate (327.43s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptStop (88.75s)
--- PASS: TestAccAWSSpotInstanceRequest_NetworkInterfaceAttributes (341.21s)
--- PASS: TestAccAWSSpotInstanceRequest_SubnetAndSGAndPublicIpAddress (351.06s)
--- PASS: TestAccAWSSpotInstanceRequest_tags (152.64s)
--- PASS: TestAccAWSSpotInstanceRequest_validUntil (339.82s)
--- PASS: TestAccAWSSpotInstanceRequest_vpc (341.75s)
--- PASS: TestAccAWSSpotInstanceRequest_withBlockDuration (333.88s)
--- PASS: TestAccAWSSpotInstanceRequest_withLaunchGroup (308.06s)
--- PASS: TestAccAWSSpotInstanceRequest_withoutSpotPrice (79.75s)
```

Output from acceptance testing in AWS GovCloud (US):

```
--- PASS: TestAccAWSEIPAssociation_spotInstance (139.29s)

--- PASS: TestAccAWSSpotInstanceRequest_basic (127.90s)
--- PASS: TestAccAWSSpotInstanceRequest_disappears (309.16s)
--- PASS: TestAccAWSSpotInstanceRequest_getPasswordData (203.89s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptHibernate (125.23s)
--- PASS: TestAccAWSSpotInstanceRequest_InterruptStop (105.36s)
--- PASS: TestAccAWSSpotInstanceRequest_NetworkInterfaceAttributes (138.38s)
--- PASS: TestAccAWSSpotInstanceRequest_SubnetAndSGAndPublicIpAddress (140.94s)
--- PASS: TestAccAWSSpotInstanceRequest_tags (186.82s)
--- PASS: TestAccAWSSpotInstanceRequest_validUntil (317.22s)
--- PASS: TestAccAWSSpotInstanceRequest_vpc (120.42s)
--- PASS: TestAccAWSSpotInstanceRequest_withBlockDuration (326.41s)
--- PASS: TestAccAWSSpotInstanceRequest_withLaunchGroup (315.90s)
--- PASS: TestAccAWSSpotInstanceRequest_withoutSpotPrice (100.39s)
```
  • Loading branch information
DrFaust92 authored Feb 18, 2021
1 parent ec23bc8 commit 4a51c4b
Show file tree
Hide file tree
Showing 4 changed files with 380 additions and 352 deletions.
6 changes: 6 additions & 0 deletions .changelog/12787.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:enhancement
resource/aws_spot_instance_request: Add import support
```
```release-note:enhancement
resource/aws_spot_instance_request: Add plan time validation for `spot_type` and `block_duration_minutes`
```
10 changes: 5 additions & 5 deletions aws/resource_aws_eip_association_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func TestAccAWSEIPAssociation_ec2Classic(t *testing.T) {

func TestAccAWSEIPAssociation_spotInstance(t *testing.T) {
var a ec2.Address
rInt := acctest.RandInt()
rName := acctest.RandomWithPrefix("tf-acc-test")
resourceName := "aws_eip_association.test"

resource.ParallelTest(t, resource.TestCase{
Expand All @@ -129,7 +129,7 @@ func TestAccAWSEIPAssociation_spotInstance(t *testing.T) {
CheckDestroy: testAccCheckAWSEIPAssociationDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSEIPAssociationConfig_spotInstance(rInt),
Config: testAccAWSEIPAssociationConfig_spotInstance(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSEIPExists("aws_eip.test", &a),
testAccCheckAWSEIPAssociationExists(resourceName, &a),
Expand Down Expand Up @@ -419,7 +419,7 @@ resource "aws_eip_association" "test" {
`)
}

func testAccAWSEIPAssociationConfig_spotInstance(rInt int) string {
func testAccAWSEIPAssociationConfig_spotInstance(rName string) string {
return composeConfig(
testAccLatestAmazonLinuxHvmEbsAmiConfig(),
testAccAvailableEc2InstanceTypeForAvailabilityZone("aws_subnet.test.availability_zone", "t3.micro", "t2.micro"),
Expand All @@ -440,7 +440,7 @@ resource "aws_internet_gateway" "test" {
}
resource "aws_key_pair" "test" {
key_name = "tmp-key-%d"
key_name = %[1]q
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 [email protected]"
}
Expand All @@ -461,7 +461,7 @@ resource "aws_eip_association" "test" {
allocation_id = aws_eip.test.id
instance_id = aws_spot_instance_request.test.spot_instance_id
}
`, rInt))
`, rName))
}

func testAccAWSEIPAssociationConfig_instance() string {
Expand Down
162 changes: 90 additions & 72 deletions aws/resource_aws_spot_instance_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package aws
import (
"fmt"
"log"
"math/big"
"strconv"
"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-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand All @@ -20,6 +21,9 @@ func resourceAwsSpotInstanceRequest() *schema.Resource {
Read: resourceAwsSpotInstanceRequestRead,
Delete: resourceAwsSpotInstanceRequestDelete,
Update: resourceAwsSpotInstanceRequestUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Expand Down Expand Up @@ -47,12 +51,20 @@ func resourceAwsSpotInstanceRequest() *schema.Resource {
s["spot_price"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
oldFloat, _ := strconv.ParseFloat(old, 64)
newFloat, _ := strconv.ParseFloat(new, 64)

return big.NewFloat(oldFloat).Cmp(big.NewFloat(newFloat)) == 0
},
}
s["spot_type"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "persistent",
Type: schema.TypeString,
Optional: true,
Default: ec2.SpotInstanceTypePersistent,
ValidateFunc: validation.StringInSlice(ec2.SpotInstanceType_Values(), false),
}
s["wait_for_fulfillment"] = &schema.Schema{
Type: schema.TypeBool,
Expand All @@ -77,20 +89,17 @@ func resourceAwsSpotInstanceRequest() *schema.Resource {
Computed: true,
}
s["block_duration_minutes"] = &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
ValidateFunc: validation.IntDivisibleBy(60),
}
s["instance_interruption_behaviour"] = &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: ec2.InstanceInterruptionBehaviorTerminate,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
ec2.InstanceInterruptionBehaviorTerminate,
ec2.InstanceInterruptionBehaviorStop,
ec2.InstanceInterruptionBehaviorHibernate,
}, false),
Type: schema.TypeString,
Optional: true,
Default: ec2.InstanceInterruptionBehaviorTerminate,
ForceNew: true,
ValidateFunc: validation.StringInSlice(ec2.InstanceInterruptionBehavior_Values(), false),
}
s["valid_from"] = &schema.Schema{
Type: schema.TypeString,
Expand Down Expand Up @@ -155,19 +164,19 @@ func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface
}

if v, ok := d.GetOk("valid_from"); ok {
valid_from, err := time.Parse(time.RFC3339, v.(string))
validFrom, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return err
}
spotOpts.ValidFrom = aws.Time(valid_from)
spotOpts.ValidFrom = aws.Time(validFrom)
}

if v, ok := d.GetOk("valid_until"); ok {
valid_until, err := time.Parse(time.RFC3339, v.(string))
validUntil, err := time.Parse(time.RFC3339, v.(string))
if err != nil {
return err
}
spotOpts.ValidUntil = aws.Time(valid_until)
spotOpts.ValidUntil = aws.Time(validUntil)
}

// Placement GroupName can only be specified when instanceInterruptionBehavior is not set or set to 'terminate'
Expand Down Expand Up @@ -248,7 +257,8 @@ func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}
if err != nil {
// If the spot request was not found, return nil so that we can show
// that it is gone.
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
if isAWSErr(err, "InvalidSpotInstanceRequestID.NotFound", "") {
log.Printf("[WARN] EC2 Spot Instance Request (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}
Expand All @@ -259,14 +269,15 @@ func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}

// If nothing was found, then return no state
if len(resp.SpotInstanceRequests) == 0 {
log.Printf("[WARN] EC2 Spot Instance Request (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

request := resp.SpotInstanceRequests[0]

// if the request is cancelled or closed, then it is gone
if *request.State == "cancelled" || *request.State == "closed" {
if *request.State == ec2.SpotInstanceStateCancelled || *request.State == ec2.SpotInstanceStateClosed {
d.SetId("")
return nil
}
Expand All @@ -292,6 +303,11 @@ func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}
d.Set("instance_interruption_behaviour", request.InstanceInterruptionBehavior)
d.Set("valid_from", aws.TimeValue(request.ValidFrom).Format(time.RFC3339))
d.Set("valid_until", aws.TimeValue(request.ValidUntil).Format(time.RFC3339))
d.Set("spot_type", request.Type)
d.Set("spot_price", request.SpotPrice)
d.Set("key_name", request.LaunchSpecification.KeyName)
d.Set("instance_type", request.LaunchSpecification.InstanceType)
d.Set("ami", request.LaunchSpecification.ImageId)

return nil
}
Expand All @@ -316,62 +332,64 @@ func readInstance(d *schema.ResourceData, meta interface{}) error {
return fmt.Errorf("no instances found")
}

// Set these fields for connection information
if instance != nil {
d.Set("public_dns", instance.PublicDnsName)
d.Set("public_ip", instance.PublicIpAddress)
d.Set("private_dns", instance.PrivateDnsName)
d.Set("private_ip", instance.PrivateIpAddress)

// set connection information
if instance.PublicIpAddress != nil {
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": *instance.PublicIpAddress,
})
} else if instance.PrivateIpAddress != nil {
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": *instance.PrivateIpAddress,
})
}
if err := readBlockDevices(d, instance, conn); err != nil {
return err
}
d.Set("public_dns", instance.PublicDnsName)
d.Set("public_ip", instance.PublicIpAddress)
d.Set("private_dns", instance.PrivateDnsName)
d.Set("private_ip", instance.PrivateIpAddress)
d.Set("source_dest_check", instance.SourceDestCheck)

// set connection information
if instance.PublicIpAddress != nil {
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": *instance.PublicIpAddress,
})
} else if instance.PrivateIpAddress != nil {
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": *instance.PrivateIpAddress,
})
}
if err := readBlockDevices(d, instance, conn); err != nil {
return err
}

var ipv6Addresses []string
if len(instance.NetworkInterfaces) > 0 {
for _, ni := range instance.NetworkInterfaces {
if *ni.Attachment.DeviceIndex == 0 {
d.Set("subnet_id", ni.SubnetId)
d.Set("primary_network_interface_id", ni.NetworkInterfaceId)
d.Set("associate_public_ip_address", ni.Association != nil)
d.Set("ipv6_address_count", len(ni.Ipv6Addresses))

for _, address := range ni.Ipv6Addresses {
ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address)
}
var ipv6Addresses []string
if len(instance.NetworkInterfaces) > 0 {
for _, ni := range instance.NetworkInterfaces {
if *ni.Attachment.DeviceIndex == 0 {
d.Set("subnet_id", ni.SubnetId)
d.Set("primary_network_interface_id", ni.NetworkInterfaceId)
d.Set("associate_public_ip_address", ni.Association != nil)
d.Set("ipv6_address_count", len(ni.Ipv6Addresses))

for _, address := range ni.Ipv6Addresses {
ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address)
}
}
} else {
d.Set("subnet_id", instance.SubnetId)
d.Set("primary_network_interface_id", "")
}
} else {
d.Set("subnet_id", instance.SubnetId)
d.Set("primary_network_interface_id", "")
}

if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil {
log.Printf("[WARN] Error setting ipv6_addresses for AWS Spot Instance (%s): %s", d.Id(), err)
}
if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil {
log.Printf("[WARN] Error setting ipv6_addresses for AWS Spot Instance (%s): %s", d.Id(), err)
}

if d.Get("get_password_data").(bool) {
passwordData, err := getAwsEc2InstancePasswordData(*instance.InstanceId, conn)
if err != nil {
return err
}
d.Set("password_data", passwordData)
} else {
d.Set("get_password_data", false)
d.Set("password_data", nil)
if err := readSecurityGroups(d, instance, conn); err != nil {
return err
}

if d.Get("get_password_data").(bool) {
passwordData, err := getAwsEc2InstancePasswordData(*instance.InstanceId, conn)
if err != nil {
return err
}
d.Set("password_data", passwordData)
} else {
d.Set("get_password_data", false)
d.Set("password_data", nil)
}

return nil
Expand Down Expand Up @@ -424,7 +442,7 @@ func SpotInstanceStateRefreshFunc(
})

if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
if isAWSErr(err, "InvalidSpotInstanceRequestID.NotFound", "") {
// Set this to nil as if we didn't find anything.
resp = nil
} else {
Expand Down
Loading

0 comments on commit 4a51c4b

Please sign in to comment.