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

provider/aws: AWS SpotFleet Requests now works with Subnets and AZs #8320

Merged
merged 2 commits into from
Aug 24, 2016
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
165 changes: 95 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 probably will need to document this as breaking?

},
"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"] = strconv.FormatFloat(*l.WeightedCapacity, 'f', 0, 64)
}

// 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 Down
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