Skip to content

Commit

Permalink
provider/aws: Change Spot Fleet Request to allow a combination of
Browse files Browse the repository at this point in the history
subnet_id and availability_zone

Also added a complete set of tests that reflect all of the use cases
that Amazon document
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-examples.html

It is important to note there that Terraform will be suggesting that
users create multiple launch configurations rather than AWS's version of
combing values into CSV based parameters. This will ensure that we are
able to enforce the correct state

Also note that `associate_public_ip_address` now defaults to `false` - a migration has been
included in this PR to migration users of this functionality. This needs
to be noted in the changelog. The last part of changing functionality
here is waiting for the state of the request to become `active`. Before
we get to this state, we cannot guarantee that Amazon have accepted the
request or it could have failed validation.

```
% make testacc TEST=./builtin/providers/aws
% TESTARGS='-run=TestAccAWSSpotFleetRequest_'
% 2 ↵
==> Checking that code complies with gofmt requirements...
/Users/stacko/Code/go/bin/stringer
go generate $(go list ./... | grep -v /terraform/vendor/)
2016/08/22 15:44:21 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/aws -v
-run=TestAccAWSSpotFleetRequest_ -timeout 120m
=== RUN   TestAccAWSSpotFleetRequest_changePriceForcesNewRequest
--- PASS: TestAccAWSSpotFleetRequest_changePriceForcesNewRequest (133.90s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion (76.67s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList (75.22s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList (96.95s)
=== RUN   TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz
--- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz (74.44s)
=== RUN   TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet
--- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet (97.82s)
=== RUN   TestAccAWSSpotFleetRequest_overriddingSpotPrice
--- PASS: TestAccAWSSpotFleetRequest_overriddingSpotPrice (76.22s)
=== RUN   TestAccAWSSpotFleetRequest_diversifiedAllocation
--- PASS: TestAccAWSSpotFleetRequest_diversifiedAllocation (79.81s)
=== RUN   TestAccAWSSpotFleetRequest_withWeightedCapacity
--- PASS: TestAccAWSSpotFleetRequest_withWeightedCapacity (77.15s)
=== RUN   TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName
--- PASS: TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName (0.00s)
PASS
ok      github.com/hashicorp/terraform/builtin/providers/aws    788.184s
```
  • Loading branch information
stack72 committed Aug 22, 2016
1 parent b5e0f2e commit dcce17b
Show file tree
Hide file tree
Showing 5 changed files with 781 additions and 104 deletions.
169 changes: 99 additions & 70 deletions builtin/providers/aws/resource_aws_spot_fleet_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Delete: resourceAwsSpotFleetRequestDelete,
Update: resourceAwsSpotFleetRequestUpdate,

SchemaVersion: 1,
MigrateState: resourceAwsSpotFleetRequestMigrateState,

Schema: map[string]*schema.Schema{
"iam_fleet_role": &schema.Schema{
Type: schema.TypeString,
Expand All @@ -49,7 +52,7 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
"associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
Default: false,
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
Expand Down Expand Up @@ -192,7 +195,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
},
// "network_interface_set"
"placement_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Expand All @@ -204,12 +206,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Optional: true,
ForceNew: true,
},
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"user_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Expand All @@ -229,9 +225,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Optional: true,
ForceNew: true,
},
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"availability_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
Expand Down Expand Up @@ -291,19 +294,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{}) (*ec2.SpotFleetLaunchSpecification, error) {
conn := meta.(*AWSClient).ec2conn

_, hasSubnet := d["subnet_id"]
_, hasAZ := d["availability_zone"]
if !hasAZ && !hasSubnet {
return nil, fmt.Errorf("LaunchSpecification must include a subnet_id or an availability_zone")
}

opts := &ec2.SpotFleetLaunchSpecification{
ImageId: aws.String(d["ami"].(string)),
InstanceType: aws.String(d["instance_type"].(string)),
SpotPrice: aws.String(d["spot_price"].(string)),
Placement: &ec2.SpotPlacement{
AvailabilityZone: aws.String(d["availability_zone"].(string)),
},
}

if v, ok := d["availability_zone"]; ok {
opts.Placement = &ec2.SpotPlacement{
AvailabilityZone: aws.String(v.(string)),
}
}

if v, ok := d["ebs_optimized"]; ok {
Expand All @@ -327,31 +327,45 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{
base64.StdEncoding.EncodeToString([]byte(v.(string))))
}

// check for non-default Subnet, and cast it to a String
subnet, hasSubnet := d["subnet_id"]
subnetID := subnet.(string)
if v, ok := d["key_name"]; ok {
opts.KeyName = aws.String(v.(string))
}

var associatePublicIPAddress bool
if v, ok := d["associate_public_ip_address"]; ok {
associatePublicIPAddress = v.(bool)
if v, ok := d["weighted_capacity"]; ok && v != "" {
wc, err := strconv.ParseFloat(v.(string), 64)
if err != nil {
return nil, err
}
opts.WeightedCapacity = aws.Float64(wc)
}

var groups []*string
if v, ok := d["security_groups"]; ok {
// Security group names.
// For a nondefault VPC, you must use security group IDs instead.
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html
sgs := v.(*schema.Set).List()
if len(sgs) > 0 && hasSubnet {
log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.")
}
for _, v := range sgs {
str := v.(string)
groups = append(groups, aws.String(str))
}
}

if hasSubnet && associatePublicIPAddress {
var groupIds []*string
if v, ok := d["vpc_security_group_ids"]; ok {
if s := v.(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))})
groupIds = append(groupIds, aws.String(v.(string)))
}
}
}

subnetId, hasSubnetId := d["subnet_id"]
if hasSubnetId {
opts.SubnetId = aws.String(subnetId.(string))
}

associatePublicIpAddress, hasPublicIpAddress := d["associate_public_ip_address"]
if hasPublicIpAddress && associatePublicIpAddress.(bool) == true && hasSubnetId {

// If we have a non-default VPC / Subnet specified, we can flag
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
Expand All @@ -360,47 +374,14 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress),
AssociatePublicIpAddress: aws.Bool(true),
DeviceIndex: aws.Int64(int64(0)),
SubnetId: aws.String(subnetID),
Groups: groups,
}

if v, ok := d["private_ip"]; ok {
ni.PrivateIpAddress = aws.String(v.(string))
}

if v := d["vpc_security_group_ids"].(*schema.Set); v.Len() > 0 {
for _, v := range v.List() {
ni.Groups = append(ni.Groups, aws.String(v.(string)))
}
SubnetId: aws.String(subnetId.(string)),
Groups: groupIds,
}

opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" {
opts.SubnetId = aws.String(subnetID)
}

if v, ok := d["vpc_security_group_ids"]; ok {
if s := v.(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))})
}
}
}
}

if v, ok := d["key_name"]; ok {
opts.KeyName = aws.String(v.(string))
}

if v, ok := d["weighted_capacity"]; ok && v != "" {
wc, err := strconv.ParseFloat(v.(string), 64)
if err != nil {
return nil, err
}
opts.WeightedCapacity = aws.Float64(wc)
opts.SubnetId = aws.String("")
}

blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn)
Expand Down Expand Up @@ -617,9 +598,52 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{})

d.SetId(*resp.SpotFleetRequestId)

log.Printf("[INFO] Spot Fleet Request ID: %s", d.Id())
log.Println("[INFO] Waiting for Spot Fleet Request to be active")
stateConf := &resource.StateChangeConf{
Pending: []string{"submitted"},
Target: []string{"active"},
Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta),
Timeout: 10 * time.Minute,
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second,
}

_, err = stateConf.WaitForState()
if err != nil {
return err
}

return resourceAwsSpotFleetRequestRead(d, meta)
}

func resourceAwsSpotFleetRequestStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeSpotFleetRequests(req)

if err != nil {
log.Printf("Error on retrieving Spot Fleet Request when waiting: %s", err)
return nil, "", nil
}

if resp == nil {
return nil, "", nil
}

if len(resp.SpotFleetRequestConfigs) == 0 {
return nil, "", nil
}

spotFleetRequest := resp.SpotFleetRequestConfigs[0]

return spotFleetRequest, *spotFleetRequest.SpotFleetRequestState, nil
}
}

func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html
conn := meta.(*AWSClient).ec2conn
Expand Down Expand Up @@ -773,7 +797,7 @@ func launchSpecToMap(
}

if l.WeightedCapacity != nil {
m["weighted_capacity"] = fmt.Sprintf("%.3f", aws.Float64Value(l.WeightedCapacity))
m["weighted_capacity"] = floatToString(*l.WeightedCapacity)
}

// m["security_groups"] = securityGroupsToSet(l.SecutiryGroups)
Expand Down Expand Up @@ -941,9 +965,10 @@ func hashLaunchSpecification(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["ami"].(string)))
if m["availability_zone"] != nil && m["availability_zone"] != "" {
if m["availability_zone"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string)))
} else if m["subnet_id"] != nil && m["subnet_id"] != "" {
}
if m["subnet_id"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string)))
}
buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string)))
Expand All @@ -959,3 +984,7 @@ func hashEbsBlockDevice(v interface{}) int {
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
return hashcode.String(buf.String())
}

func floatToString(input float64) string {
return strconv.FormatFloat(input, 'f', 0, 64)
}
33 changes: 33 additions & 0 deletions builtin/providers/aws/resource_aws_spot_fleet_request_migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package aws

import (
"fmt"
"log"

"github.com/hashicorp/terraform/terraform"
)

func resourceAwsSpotFleetRequestMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS Spot Fleet Request State v0; migrating to v1")
return migrateSpotFleetRequestV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
}

func migrateSpotFleetRequestV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
if is.Empty() {
log.Println("[DEBUG] Empty Spot Fleet Request State; nothing to migrate.")
return is, nil
}

log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)

is.Attributes["associate_public_ip_address"] = "false"

log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package aws

import (
"testing"

"github.com/hashicorp/terraform/terraform"
)

func TestAWSSpotFleetRequestMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
ID string
Attributes map[string]string
Expected string
Meta interface{}
}{
"v0_1": {
StateVersion: 0,
ID: "some_id",
Attributes: map[string]string{
"associate_public_ip_address": "true",
},
Expected: "false",
},
}

for tn, tc := range cases {
is := &terraform.InstanceState{
ID: tc.ID,
Attributes: tc.Attributes,
}
is, err := resourceAwsSpotFleetRequestMigrateState(
tc.StateVersion, is, tc.Meta)

if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}

if is.Attributes["associate_public_ip_address"] != tc.Expected {
t.Fatalf("bad Spot Fleet Request Migrate: %s\n\n expected: %s", is.Attributes["associate_public_ip_address"], tc.Expected)
}
}
}
Loading

0 comments on commit dcce17b

Please sign in to comment.