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

Spotfleet Launch Template and On-Demand Capacity Support #4866

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
69e71af
added launch templates and ondemand capacity
lossanarch Jun 14, 2018
05d2327
Added tests with incorrect subresource ids
lossanarch Jun 18, 2018
52d7ebd
Added documentation
lossanarch Jun 18, 2018
cd800bf
added forceNews, ondemand active instance handling on delete and bett…
lossanarch Jun 25, 2018
8587d5d
Fixed test failure and added better debugging, added tests
lossanarch Jun 28, 2018
cd922e3
remove unnecessary float64 conversion causing lint failure
lossanarch Oct 20, 2018
67bca1a
remove commented validatefuncs
lossanarch Nov 11, 2018
7cdb465
comment customdiff
lossanarch Nov 11, 2018
9384ef6
fix commenting and logic of template to config assignment
lossanarch Nov 11, 2018
a3b4644
comment nonretryable error
lossanarch Nov 11, 2018
9310f97
clean up commented error handling
lossanarch Nov 11, 2018
0bedd0e
clean up commented test
lossanarch Nov 11, 2018
3092b71
fix active instance count if logic
lossanarch Nov 11, 2018
7d93f45
attempt to fix documentation as per review request
lossanarch Dec 8, 2018
3672583
removed on-demand spot instance feature due to moving to a different PR
lossanarch Dec 8, 2018
ba51124
rename overrideToMap to flattenSpotFleetRequestLaunchTemplateOverrides
lossanarch Dec 8, 2018
682500d
remove comment cruft
lossanarch Dec 8, 2018
9b24059
added launch template conflict with launch specification test
lossanarch Dec 8, 2018
19cd82f
fixed lint errors, readded somehow missing tests
lossanarch Dec 8, 2018
c3e1c10
fix missing tests retry
lossanarch Dec 8, 2018
5c64ed3
fmt
Feb 28, 2019
7ccfc98
remove duplicate test
Feb 28, 2019
553b72f
fix arn check regex
Feb 28, 2019
0ba5456
Update website/docs/r/spot_fleet_request.html.markdown
lossanarch Aug 25, 2019
3df1662
Update website/docs/r/spot_fleet_request.html.markdown
lossanarch Aug 25, 2019
cb99970
Update website/docs/r/spot_fleet_request.html.markdown
lossanarch Aug 25, 2019
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
282 changes: 270 additions & 12 deletions aws/resource_aws_spot_fleet_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"fmt"
"log"
"strconv"
"strings"
"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/helper/customdiff"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
Expand Down Expand Up @@ -53,9 +55,10 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetLaunchSpecification
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html
"launch_specification": {
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"launch_template_configs"},
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"vpc_security_group_ids": {
Expand Down Expand Up @@ -273,7 +276,88 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
},
Set: hashLaunchSpecification,
},
// Everything on a spot fleet is ForceNew except target_capacity
"launch_template_configs": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"launch_specification"},
Ninir marked this conversation as resolved.
Show resolved Hide resolved
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"launch_template_specification": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"launch_template_configs.0.launch_template_specification.0.name"},
Copy link
Contributor

Choose a reason for hiding this comment

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

In order to ensure this will never break, can we add an acceptance test to expect the conflicting error?

Copy link
Contributor

Choose a reason for hiding this comment

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

@lossanarch Not sure to see the test on this one, were you able to check it?

Copy link
Author

Choose a reason for hiding this comment

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

Hey I believe this one is covered by TestAccAWSSpotFleetRequest_launchTemplateConflictLaunchSpecification, line 166 of the tests file.

ValidateFunc: validateLaunchTemplateId,
},
"name": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"launch_template_configs.0.launch_template_specification.0.id"},
Copy link
Contributor

Choose a reason for hiding this comment

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

This ConflictsWith value will not work since launch_template_configs is of TypeSet, and this syntax is allowed by TypeList only.
I'm thinking this should be handled in the CustomizeDiff instead, as it would allow to do it in a more efficient way.

ValidateFunc: validateLaunchTemplateName,
},
"version": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validation.StringLenBetween(1, 255),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we only have 255 versions?

Copy link
Author

Choose a reason for hiding this comment

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

I think this may have meant to be for a "version_description" attr, which is limited to 255 characters. The api lists "version" as type long though, so this definitely needs fixing.

},
},
},
},
"overrides": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems this is missing the priority attribute as per https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_LaunchTemplateOverrides.html. Can you add that in, along with a test?

"availability_zone": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"instance_type": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"spot_price": {
Type: schema.TypeString,
Optional: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to mark this as computed? what about the computed attribute for subnet_id and weighted_capacity

Computed: true,
ForceNew: true,
},
"subnet_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"weighted_capacity": {
Type: schema.TypeFloat,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
Set: hashLaunchTemplateOverrides,
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we need this one: the default hash function handles pretty much every case.
Is there a need for this one on your side?

Copy link
Author

Choose a reason for hiding this comment

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

I think this is required because we have to add the launch template overrides to it, if they exist:

func setLaunchTemplateOverrides(overrides []*ec2.LaunchTemplateOverrides) *schema.Set {
	overrideSet := &schema.Set{F: hashLaunchTemplateOverrides}
	for _, override := range overrides {
		overrideSet.Add(flattenSpotFleetRequestLaunchTemplateOverrides(override))
	}
	return overrideSet
}

but it's been a super long time since I wrote this and I don't fully remember.

Is there a way to do this with the default hash function?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think with my comment at https://github.com/terraform-providers/terraform-provider-aws/pull/4866/files#diff-cd8c91a4c915b1ae56ba89540a3412dcR1135, we may not need this Hash Function at all. Can you give a try of fixing the code below?
Thanks!

},
},
},
},
// Everything on a spot fleet is ForceNew except target_capacity and excess_capacity_termination_policy,
// see https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#ModifySpotFleetRequestInput
"target_capacity": {
Type: schema.TypeInt,
Required: true,
Expand Down Expand Up @@ -360,6 +444,17 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Set: schema.HashString,
},
},
// A launch template can be created with an id or a name, but once created will have both even though we specify only one.
// If either changes, this makes sure the other (which was specified) will register as changed in order to correctly determine diffs.
// This was taken from the original implementation of launch templates in resource_aws_autoscaling_group.go
CustomizeDiff: customdiff.Sequence(
customdiff.ComputedIf("launch_template_configs.0.launch_template_specification.0.id", func(diff *schema.ResourceDiff, meta interface{}) bool {
Copy link
Contributor

@Ninir Ninir Oct 28, 2018

Choose a reason for hiding this comment

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

Can you add a comment explaining the need for that? just for future readers to better understand

Copy link
Author

@lossanarch lossanarch Nov 11, 2018

Choose a reason for hiding this comment

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

I actually stole this from the launch template implementation in the autoscaling group - I've commented it as how I understand it works, hopefully I'm right. 😬
Edit: Commit reference: 167c38f

Copy link
Contributor

Choose a reason for hiding this comment

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

This wouldn't work since launch_template_configs is of TypeSet and the syntax used here is for TypeList with MaxItems: 1.

In this case, I would advise to iterate over the elements and check for the value using Go instead of HCL-based access. Let me know if that doesn't make sense!

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, since launch_template_specification is Computed and Optional, this seems extraneous, since the differences would never show if not configured

return diff.HasChange("launch_template_configs.0.launch_template_specification.0.name")
}),
customdiff.ComputedIf("launch_template_configs.0.launch_template_specification.0.name", func(diff *schema.ResourceDiff, meta interface{}) bool {
return diff.HasChange("launch_template_configs.0.launch_template_specification.0.id")
}),
),
}
}

Expand Down Expand Up @@ -598,19 +693,93 @@ func buildAwsSpotFleetLaunchSpecifications(
return specs, nil
}

func buildLaunchTemplateConfigs(d *schema.ResourceData) ([]*ec2.LaunchTemplateConfig, error) {
launch_template_cfgs := d.Get("launch_template_configs").(*schema.Set).List()
cfgs := make([]*ec2.LaunchTemplateConfig, len(launch_template_cfgs))

for _, launch_template_cfg := range launch_template_cfgs {

ltc := &ec2.LaunchTemplateConfig{}

ltc_map := launch_template_cfg.(map[string]interface{})

//launch template spec
if v, ok := ltc_map["launch_template_specification"]; ok {
vL := v.([]interface{})
lts := vL[0].(map[string]interface{})

flts := &ec2.FleetLaunchTemplateSpecification{}

if v, ok := lts["id"].(string); ok && v != "" {
flts.LaunchTemplateId = aws.String(v)
}

if v, ok := lts["name"].(string); ok && v != "" {
flts.LaunchTemplateName = aws.String(v)
}

if v, ok := lts["version"].(string); ok && v != "" {
flts.Version = aws.String(v)
}

ltc.LaunchTemplateSpecification = flts

}

overrides := make([]*ec2.LaunchTemplateOverrides, 0)

if v, ok := ltc_map["overrides"]; ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
ors := v.(map[string]interface{})
lto := &ec2.LaunchTemplateOverrides{}

if v, ok := ors["availability_zone"].(string); ok && v != "" {
lto.AvailabilityZone = aws.String(v)
}

if v, ok := ors["instance_type"].(string); ok && v != "" {
lto.InstanceType = aws.String(v)
}

if v, ok := ors["spot_price"].(string); ok && v != "" {
lto.SpotPrice = aws.String(v)
}

if v, ok := ors["subnet_id"].(string); ok && v != "" {
lto.SubnetId = aws.String(v)
}

if v, ok := ors["weighted_capacity"].(float64); ok && v > 0 {
lto.WeightedCapacity = aws.Float64(v)
}

overrides = append(overrides, lto)

}
}
ltc.Overrides = overrides

cfgs = append(cfgs, ltc)
}

return cfgs, nil
}

func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RequestSpotFleet.html
conn := meta.(*AWSClient).ec2conn

launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta)
if err != nil {
return err
_, launchSpecificationOk := d.GetOk("launch_specification")
_, launchTemplateConfigsOk := d.GetOk("launch_template_configs")

if !launchSpecificationOk && !launchTemplateConfigsOk {
return fmt.Errorf("One of `launch_specification` or `launch_template` must be set for a fleet request")
}

// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData
spotFleetConfig := &ec2.SpotFleetRequestConfigData{
IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)),
LaunchSpecifications: launch_specs,
TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))),
ClientToken: aws.String(resource.UniqueId()),
TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
Expand All @@ -619,6 +788,24 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{})
Type: aws.String(d.Get("fleet_type").(string)),
}

var err error

if launchSpecificationOk {
launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta)
if err != nil {
return err
}
spotFleetConfig.LaunchSpecifications = launch_specs
}

if launchTemplateConfigsOk {
launch_templates, err := buildLaunchTemplateConfigs(d)
if err != nil {
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we rewrite this using the same logic as lines 800-806?
We also need to clean the comments if unused.

Copy link
Author

Choose a reason for hiding this comment

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

Cleaned and logic standardised. Commit 127425f

spotFleetConfig.LaunchTemplateConfigs = launch_templates
}

if v, ok := d.GetOk("excess_capacity_termination_policy"); ok {
spotFleetConfig.ExcessCapacityTerminationPolicy = aws.String(v.(string))
}
Expand Down Expand Up @@ -703,10 +890,20 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{})

if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// IAM is eventually consistent :/

if awsErr.Code() == "InvalidSpotFleetRequestConfig" {
return resource.RetryableError(
fmt.Errorf("Error creating Spot fleet request, retrying: %s", err))
switch {
case strings.Contains(awsErr.Message(), "LaunchTemplateSpecification"):
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel we should be more specific about the message erroring, so that we can better handle different kind of errors.
We could rewrite part of this switch for something like:

		if isAWSErr(err, "InvalidSpotFleetRequestConfig", "LaunchTemplateSpecification") {
			return resource.RetryableError(err)
		}

but instead of LaunchTemplateSpecification, having a part of the sentence that matches a given error. Does that make sense?

// AWS is letting us know that the Launch Template has been specified in some invalid way.
// No point in retrying as the template needs to be corrected.
return resource.NonRetryableError(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a comment about why we can't retry on LaunchTemplateSpecification ?

Copy link
Author

Choose a reason for hiding this comment

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

Comment added, commit 95e2fc6

case strings.Contains(awsErr.Message(), "IamFleetRole"):
// IAM is eventually consistent :/
return resource.RetryableError(
fmt.Errorf("[WARN] Error creating Spot fleet request, retrying: %s", err))
default:
return resource.NonRetryableError(err)
}
}
}
return resource.NonRetryableError(err)
Expand All @@ -726,7 +923,7 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{})
Pending: []string{"submitted"},
Target: []string{"active"},
Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta),
Timeout: 10 * time.Minute,
Timeout: d.Timeout(schema.TimeoutCreate), //10 * time.Minute,
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second,
}
Expand Down Expand Up @@ -934,9 +1131,49 @@ func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) e
d.Set("fleet_type", config.Type)
d.Set("launch_specification", launchSpecsToSet(config.LaunchSpecifications, conn))

if len(config.LaunchTemplateConfigs) > 0 {
d.Set("launch_template_configs.0.launch_template_specification.0", flattenFleetLaunchTemplateSpecification(config.LaunchTemplateConfigs[0].LaunchTemplateSpecification))
Copy link
Contributor

Choose a reason for hiding this comment

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

After thinking about it, we definitely need to check the output of this Set function here, and I would advise to switch to a more common approach that is more standard from a TF schema perspective:

Suggested change
d.Set("launch_template_configs.0.launch_template_specification.0", flattenFleetLaunchTemplateSpecification(config.LaunchTemplateConfigs[0].LaunchTemplateSpecification))
if err := d.Set("launch_template_configs", flattenLaunchTemplateConfigs(config.LaunchTemplateConfigs)); err != nil {
return fmt.Errorf("error setting launch_template_configs: %s", err)
}

What that means is flattenLaunchTemplateConfigs would be a function relying on flattenFleetLaunchTemplateSpecification and the config.LaunchTemplateConfigs[0].Overrides existence and value.

flattenLaunchTemplateConfigs would also handle the nil case line 1138, thus there wouldn't be a need for this check at all.

In the end, it could be something like:

		if err := d.Set("launch_template_configs", flattenLaunchTemplateConfigs(config.LaunchTemplateConfigs)); err != nil {
			return fmt.Errorf("error setting launch_template_configs: %s", err)
}

instead of:

	if len(config.LaunchTemplateConfigs) > 0 {
		d.Set("launch_template_configs.0.launch_template_specification.0", flattenFleetLaunchTemplateSpecification(config.LaunchTemplateConfigs[0].LaunchTemplateSpecification))
		d.Set("launch_template_configs.0.overrides", setLaunchTemplateOverrides(config.LaunchTemplateConfigs[0].Overrides))
	} else {
		d.Set("launch_template_configs.0.launch_template_specification.0", nil)
	}

There are several examples of that in structure.go, with an example

d.Set("launch_template_configs.0.overrides", setLaunchTemplateOverrides(config.LaunchTemplateConfigs[0].Overrides))
} else {
d.Set("launch_template_configs.0.launch_template_specification.0", nil)
}

return nil
}

func setLaunchTemplateOverrides(overrides []*ec2.LaunchTemplateOverrides) *schema.Set {
overrideSet := &schema.Set{F: hashLaunchTemplateOverrides}
for _, override := range overrides {
overrideSet.Add(flattenSpotFleetRequestLaunchTemplateOverrides(override))
}
return overrideSet
}

func flattenSpotFleetRequestLaunchTemplateOverrides(override *ec2.LaunchTemplateOverrides) map[string]interface{} {
m := make(map[string]interface{})

if override.AvailabilityZone != nil {
m["availability_zone"] = aws.StringValue(override.AvailabilityZone)
}
if override.InstanceType != nil {
m["instance_type"] = aws.StringValue(override.InstanceType)
}

if override.SpotPrice != nil {
m["spot_price"] = aws.StringValue(override.SpotPrice)
}

if override.SubnetId != nil {
m["subnet_id"] = aws.StringValue(override.SubnetId)
}

if override.WeightedCapacity != nil {
m["weighted_capacity"] = aws.Float64Value(override.WeightedCapacity)
}

return m
}

func launchSpecsToSet(launchSpecs []*ec2.SpotFleetLaunchSpecification, conn *ec2.EC2) *schema.Set {
specSet := &schema.Set{F: hashLaunchSpecification}
for _, spec := range launchSpecs {
Expand Down Expand Up @@ -1235,6 +1472,27 @@ func hashLaunchSpecification(v interface{}) int {
return hashcode.String(buf.String())
}

func hashLaunchTemplateOverrides(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
if m["availability_zone"] != nil {
buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string)))
}
if m["subnet_id"] != nil {
buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string)))
}
if m["spot_price"] != nil {
buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string)))
}
if m["instance_type"] != nil {
buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string)))
}
if m["weighted_capacity"] != nil {
buf.WriteString(fmt.Sprintf("%f-", m["weighted_capacity"].(float64)))
}
return hashcode.String(buf.String())
}

func hashEbsBlockDevice(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
Expand Down
Loading