Merge pull request #7682 from hashicorp/do_7648

Do 7648
This commit is contained in:
Adrien Delorme 2019-05-24 18:54:49 +02:00 committed by GitHub
commit 65bd7207e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 413 additions and 211 deletions

View File

@ -73,6 +73,7 @@ type RunConfig struct {
SecurityGroupIds []string `mapstructure:"security_group_ids"` SecurityGroupIds []string `mapstructure:"security_group_ids"`
SourceAmi string `mapstructure:"source_ami"` SourceAmi string `mapstructure:"source_ami"`
SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"` SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"`
SpotInstanceTypes []string `mapstructure:"spot_instance_types"`
SpotPrice string `mapstructure:"spot_price"` SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"` SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
SpotTags map[string]string `mapstructure:"spot_tags"` SpotTags map[string]string `mapstructure:"spot_tags"`
@ -137,8 +138,14 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, fmt.Errorf("For security reasons, your source AMI filter must declare an owner.")) errs = append(errs, fmt.Errorf("For security reasons, your source AMI filter must declare an owner."))
} }
if c.InstanceType == "" { if c.InstanceType == "" && len(c.SpotInstanceTypes) == 0 {
errs = append(errs, fmt.Errorf("An instance_type must be specified")) errs = append(errs, fmt.Errorf("either instance_type or "+
"spot_instance_types must be specified"))
}
if c.InstanceType != "" && len(c.SpotInstanceTypes) > 0 {
errs = append(errs, fmt.Errorf("either instance_type or "+
"spot_instance_types must be specified, not both"))
} }
if c.BlockDurationMinutes%60 != 0 { if c.BlockDurationMinutes%60 != 0 {

View File

@ -14,10 +14,6 @@ import (
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
func boolPointer(tf bool) *bool {
return &tf
}
// Define a mock struct to be used in unit tests for common aws steps. // Define a mock struct to be used in unit tests for common aws steps.
type mockEC2Conn struct { type mockEC2Conn struct {
ec2iface.EC2API ec2iface.EC2API
@ -120,7 +116,7 @@ func TestStepAmiRegionCopy_false_encryption(t *testing.T) {
Regions: make([]string, 0), Regions: make([]string, 0),
AMIKmsKeyId: "", AMIKmsKeyId: "",
RegionKeyIds: make(map[string]string), RegionKeyIds: make(map[string]string),
EncryptBootVolume: boolPointer(false), EncryptBootVolume: aws.Bool(false),
Name: "fake-ami-name", Name: "fake-ami-name",
OriginalRegion: "us-east-1", OriginalRegion: "us-east-1",
} }
@ -145,7 +141,7 @@ func TestStepAmiRegionCopy_true_encryption(t *testing.T) {
Regions: make([]string, 0), Regions: make([]string, 0),
AMIKmsKeyId: "", AMIKmsKeyId: "",
RegionKeyIds: make(map[string]string), RegionKeyIds: make(map[string]string),
EncryptBootVolume: boolPointer(true), EncryptBootVolume: aws.Bool(true),
Name: "fake-ami-name", Name: "fake-ami-name",
OriginalRegion: "us-east-1", OriginalRegion: "us-east-1",
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/hashicorp/packer/common/retry" "github.com/hashicorp/packer/common/retry"
"github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
@ -35,6 +36,7 @@ type StepRunSpotInstance struct {
SpotPrice string SpotPrice string
SpotPriceProduct string SpotPriceProduct string
SpotTags TagMap SpotTags TagMap
SpotInstanceTypes []string
Tags TagMap Tags TagMap
VolumeTags TagMap VolumeTags TagMap
UserData string UserData string
@ -45,17 +47,130 @@ type StepRunSpotInstance struct {
spotRequest *ec2.SpotInstanceRequest spotRequest *ec2.SpotInstanceRequest
} }
func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { func (s *StepRunSpotInstance) CalculateSpotPrice(az string, ec2conn ec2iface.EC2API) (string, error) {
ec2conn := state.Get("ec2").(*ec2.EC2) // Calculate the spot price for a given availability zone
securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string)) spotPrice := s.SpotPrice
ui := state.Get("ui").(packer.Ui)
if spotPrice == "auto" {
// Detect the spot price
startTime := time.Now().Add(-1 * time.Hour)
resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
InstanceTypes: []*string{&s.InstanceType},
ProductDescriptions: []*string{&s.SpotPriceProduct},
AvailabilityZone: &az,
StartTime: &startTime,
})
if err != nil {
return "", fmt.Errorf("Error finding spot price: %s", err)
}
var price float64
for _, history := range resp.SpotPriceHistory {
log.Printf("[INFO] Candidate spot price: %s", *history.SpotPrice)
current, err := strconv.ParseFloat(*history.SpotPrice, 64)
if err != nil {
log.Printf("[ERR] Error parsing spot price: %s", err)
continue
}
if price == 0 || current < price {
price = current
if az == "" {
az = *history.AvailabilityZone
}
}
}
if price == 0 {
return "", fmt.Errorf("No candidate spot prices found!")
} else {
// Add 0.5 cents to minimum spot bid to ensure capacity will be available
// Avoids price-too-low error in active markets which can fluctuate
price = price + 0.005
}
spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
}
s.SpotPrice = spotPrice
return spotPrice, nil
}
func (s *StepRunSpotInstance) CreateTemplateData(userData *string, az string,
state multistep.StateBag, marketOptions *ec2.LaunchTemplateInstanceMarketOptionsRequest) *ec2.RequestLaunchTemplateData {
// Convert the BlockDeviceMapping into a
// LaunchTemplateBlockDeviceMappingRequest. These structs are identical,
// except for the EBS field -- on one, that field contains a
// LaunchTemplateEbsBlockDeviceRequest, and on the other, it contains an
// EbsBlockDevice. The EbsBlockDevice and
// LaunchTemplateEbsBlockDeviceRequest structs are themselves
// identical except for the struct's name, so you can cast one directly
// into the other.
blockDeviceMappings := s.BlockDevices.BuildLaunchDevices()
var launchMappingRequests []*ec2.LaunchTemplateBlockDeviceMappingRequest
for _, mapping := range blockDeviceMappings {
launchRequest := &ec2.LaunchTemplateBlockDeviceMappingRequest{
DeviceName: mapping.DeviceName,
Ebs: (*ec2.LaunchTemplateEbsBlockDeviceRequest)(mapping.Ebs),
NoDevice: mapping.NoDevice,
VirtualName: mapping.VirtualName,
}
launchMappingRequests = append(launchMappingRequests, launchRequest)
}
// Create a launch template.
templateData := ec2.RequestLaunchTemplateData{
BlockDeviceMappings: launchMappingRequests,
DisableApiTermination: aws.Bool(false),
EbsOptimized: &s.EbsOptimized,
IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{Name: &s.IamInstanceProfile},
ImageId: &s.SourceAMI,
InstanceMarketOptions: marketOptions,
Placement: &ec2.LaunchTemplatePlacementRequest{
AvailabilityZone: &az,
},
UserData: userData,
}
// Create a network interface
securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string))
subnetId := state.Get("subnet_id").(string)
if subnetId != "" {
// Set up a full network interface
networkInterface := ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{
Groups: securityGroupIds,
DeleteOnTermination: aws.Bool(true),
DeviceIndex: aws.Int64(0),
SubnetId: aws.String(subnetId),
}
if s.AssociatePublicIpAddress {
networkInterface.SetAssociatePublicIpAddress(s.AssociatePublicIpAddress)
}
templateData.SetNetworkInterfaces([]*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{&networkInterface})
} else {
templateData.SetSecurityGroupIds(securityGroupIds)
}
// If instance type is not set, we'll just pick the lowest priced instance
// available.
if s.InstanceType != "" {
templateData.SetInstanceType(s.InstanceType)
}
if s.Comm.SSHKeyPairName != "" {
templateData.SetKeyName(s.Comm.SSHKeyPairName)
}
return &templateData
}
func (s *StepRunSpotInstance) LoadUserData() (string, error) {
userData := s.UserData userData := s.UserData
if s.UserDataFile != "" { if s.UserDataFile != "" {
contents, err := ioutil.ReadFile(s.UserDataFile) contents, err := ioutil.ReadFile(s.UserDataFile)
if err != nil { if err != nil {
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err)) return "", fmt.Errorf("Problem reading user data file: %s", err)
return multistep.ActionHalt
} }
userData = string(contents) userData = string(contents)
@ -66,8 +181,16 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
log.Printf("[DEBUG] base64 encoding user data...") log.Printf("[DEBUG] base64 encoding user data...")
userData = base64.StdEncoding.EncodeToString([]byte(userData)) userData = base64.StdEncoding.EncodeToString([]byte(userData))
} }
return userData, nil
}
ui.Say("Launching a source AWS instance...") func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Launching a spot AWS instance...")
// Get and validate the source AMI
image, ok := state.Get("source_image").(*ec2.Image) image, ok := state.Get("source_image").(*ec2.Image)
if !ok { if !ok {
state.Put("error", fmt.Errorf("source_image type assertion failed")) state.Put("error", fmt.Errorf("source_image type assertion failed"))
@ -83,157 +206,116 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt return multistep.ActionHalt
} }
spotPrice := s.SpotPrice
azConfig := "" azConfig := ""
if azRaw, ok := state.GetOk("availability_zone"); ok { if azRaw, ok := state.GetOk("availability_zone"); ok {
azConfig = azRaw.(string) azConfig = azRaw.(string)
} }
az := azConfig az := azConfig
if spotPrice == "auto" { ui.Message(fmt.Sprintf("Finding spot price for %s %s...",
ui.Message(fmt.Sprintf( s.SpotPriceProduct, s.InstanceType))
"Finding spot price for %s %s...", spotPrice, err := s.CalculateSpotPrice(az, ec2conn)
s.SpotPriceProduct, s.InstanceType)) if err != nil {
state.Put("error", err)
// Detect the spot price ui.Error(err.Error())
startTime := time.Now().Add(-1 * time.Hour) return multistep.ActionHalt
resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
InstanceTypes: []*string{&s.InstanceType},
ProductDescriptions: []*string{&s.SpotPriceProduct},
AvailabilityZone: &az,
StartTime: &startTime,
})
if err != nil {
err := fmt.Errorf("Error finding spot price: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
var price float64
for _, history := range resp.SpotPriceHistory {
log.Printf("[INFO] Candidate spot price: %s", *history.SpotPrice)
current, err := strconv.ParseFloat(*history.SpotPrice, 64)
if err != nil {
log.Printf("[ERR] Error parsing spot price: %s", err)
continue
}
if price == 0 || current < price {
price = current
if azConfig == "" {
az = *history.AvailabilityZone
}
}
}
if price == 0 {
err := fmt.Errorf("No candidate spot prices found!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else {
// Add 0.5 cents to minimum spot bid to ensure capacity will be available
// Avoids price-too-low error in active markets which can fluctuate
price = price + 0.005
}
spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
} }
ui.Message(fmt.Sprintf("Determined spot instance price of: %s.", spotPrice))
var instanceId string var instanceId string
ui.Say("Adding tags to source instance") ui.Say("Interpolating tags for spot instance...")
// s.Tags will tag the eventually launched instance
// s.SpotTags apply to the spot request itself, and do not automatically
// get applied to the spot instance that is launched once the request is
// fulfilled
if _, exists := s.Tags["Name"]; !exists { if _, exists := s.Tags["Name"]; !exists {
s.Tags["Name"] = "Packer Builder" s.Tags["Name"] = "Packer Builder"
} }
// Convert tags from the tag map provided by the user into *ec2.Tag s
ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
if err != nil { if err != nil {
err := fmt.Errorf("Error tagging source instance: %s", err) err := fmt.Errorf("Error generating tags for source instance: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
// This prints the tags to the ui; it doesn't actually add them to the
// instance yet
ec2Tags.Report(ui) ec2Tags.Report(ui)
ui.Message(fmt.Sprintf( spotOptions := ec2.LaunchTemplateSpotMarketOptionsRequest{
"Requesting spot instance '%s' for: %s", MaxPrice: &s.SpotPrice,
s.InstanceType, spotPrice))
runOpts := &ec2.RequestSpotLaunchSpecification{
ImageId: &s.SourceAMI,
InstanceType: &s.InstanceType,
UserData: &userData,
IamInstanceProfile: &ec2.IamInstanceProfileSpecification{Name: &s.IamInstanceProfile},
Placement: &ec2.SpotPlacement{
AvailabilityZone: &az,
},
BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
EbsOptimized: &s.EbsOptimized,
}
subnetId := state.Get("subnet_id").(string)
if subnetId != "" && s.AssociatePublicIpAddress {
runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
{
DeviceIndex: aws.Int64(0),
AssociatePublicIpAddress: &s.AssociatePublicIpAddress,
SubnetId: &subnetId,
Groups: securityGroupIds,
DeleteOnTermination: aws.Bool(true),
},
}
} else {
runOpts.SubnetId = &subnetId
runOpts.SecurityGroupIds = securityGroupIds
}
if s.Comm.SSHKeyPairName != "" {
runOpts.KeyName = &s.Comm.SSHKeyPairName
}
spotInstanceInput := &ec2.RequestSpotInstancesInput{
LaunchSpecification: runOpts,
SpotPrice: &spotPrice,
} }
if s.BlockDurationMinutes != 0 { if s.BlockDurationMinutes != 0 {
spotInstanceInput.BlockDurationMinutes = &s.BlockDurationMinutes spotOptions.BlockDurationMinutes = &s.BlockDurationMinutes
}
marketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{
SpotOptions: &spotOptions,
}
marketOptions.SetMarketType(ec2.MarketTypeSpot)
// Create a launch template for the instance
ui.Message("Loading User Data File...")
userData, err := s.LoadUserData()
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message("Creating Spot Fleet launch template...")
templateData := s.CreateTemplateData(&userData, az, state, marketOptions)
launchTemplate := &ec2.CreateLaunchTemplateInput{
LaunchTemplateData: templateData,
LaunchTemplateName: aws.String("packer-fleet-launch-template"),
VersionDescription: aws.String("template generated by packer for launching spot instances"),
} }
runSpotResp, err := ec2conn.RequestSpotInstances(spotInstanceInput) // Tell EC2 to create the template
_, err = ec2conn.CreateLaunchTemplate(launchTemplate)
if err != nil { if err != nil {
err := fmt.Errorf("Error launching source spot instance: %s", err) err := fmt.Errorf("Error creating launch template for spot instance: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
s.spotRequest = runSpotResp.SpotInstanceRequests[0] // Add overrides for each user-provided instance type
var overrides []*ec2.FleetLaunchTemplateOverridesRequest
spotRequestId := s.spotRequest.SpotInstanceRequestId for _, instanceType := range s.SpotInstanceTypes {
ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId)) override := ec2.FleetLaunchTemplateOverridesRequest{
err = WaitUntilSpotRequestFulfilled(ctx, ec2conn, *spotRequestId) InstanceType: aws.String(instanceType),
if err != nil { }
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err) overrides = append(overrides, &override)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} }
spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ createFleetInput := &ec2.CreateFleetInput{
SpotInstanceRequestIds: []*string{spotRequestId}, LaunchTemplateConfigs: []*ec2.FleetLaunchTemplateConfigRequest{
}) {
if err != nil { LaunchTemplateSpecification: &ec2.FleetLaunchTemplateSpecificationRequest{
err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err) LaunchTemplateName: aws.String("packer-fleet-launch-template"),
state.Put("error", err) Version: aws.String("1"),
ui.Error(err.Error()) },
return multistep.ActionHalt Overrides: overrides,
},
},
ReplaceUnhealthyInstances: aws.Bool(false),
TargetCapacitySpecification: &ec2.TargetCapacitySpecificationRequest{
TotalTargetCapacity: aws.Int64(1),
DefaultTargetCapacityType: aws.String("spot"),
},
Type: aws.String("instant"),
} }
instanceId = *spotResp.SpotInstanceRequests[0].InstanceId
// Tag spot instance request // Create the request for the spot instance.
req, createOutput := ec2conn.CreateFleetRequest(createFleetInput)
ui.Message(fmt.Sprintf("Sending spot request (%s)...", req.RequestID))
// Tag the spot instance request (not the eventual spot instance)
spotTags, err := s.SpotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) spotTags, err := s.SpotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
if err != nil { if err != nil {
err := fmt.Errorf("Error tagging spot request: %s", err) err := fmt.Errorf("Error generating tags for spot request: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
@ -248,7 +330,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
}.Run(ctx, func(ctx context.Context) error { }.Run(ctx, func(ctx context.Context) error {
_, err := ec2conn.CreateTags(&ec2.CreateTagsInput{ _, err := ec2conn.CreateTags(&ec2.CreateTagsInput{
Tags: spotTags, Tags: spotTags,
Resources: []*string{spotRequestId}, Resources: []*string{aws.String(req.RequestID)},
}) })
return err return err
}) })
@ -260,21 +342,29 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
} }
} }
// Set the instance ID so that the cleanup works properly // Actually send the spot connection request.
s.instanceId = instanceId err = req.Send()
if err != nil {
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId)) err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", req.RequestID, err)
ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
describeInstance := &ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(instanceId)},
}
if err := ec2conn.WaitUntilInstanceRunningWithContext(ctx, describeInstance); err != nil {
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
if len(createOutput.Errors) > 0 {
err := fmt.Errorf("error sending spot request: %s", *createOutput.Errors[0])
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceId = *createOutput.Instances[0].InstanceIds[0]
// Set the instance ID so that the cleanup works properly
s.instanceId = instanceId
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
r, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{ r, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(instanceId)}, InstanceIds: []*string{aws.String(instanceId)},
}) })
@ -401,4 +491,12 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
ui.Error(err.Error()) ui.Error(err.Error())
} }
} }
// Delete the launch template used to create the spot fleet
deleteInput := &ec2.DeleteLaunchTemplateInput{
LaunchTemplateName: aws.String("packer-fleet-launch-template"),
}
if _, err := ec2conn.DeleteLaunchTemplate(deleteInput); err != nil {
ui.Error(err.Error())
}
} }

View File

@ -0,0 +1,131 @@
package common
import (
"bytes"
"strconv"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// Define a mock struct to be used in unit tests for common aws steps.
type mockEC2ConnSpot struct {
ec2iface.EC2API
Config *aws.Config
// Counters to figure out what code path was taken
describeSpotPriceHistoryCount int
}
// Generates fake SpotPriceHistory data and returns it in the expected output
// format. Also increments a
func (m *mockEC2ConnSpot) DescribeSpotPriceHistory(copyInput *ec2.DescribeSpotPriceHistoryInput) (*ec2.DescribeSpotPriceHistoryOutput, error) {
m.describeSpotPriceHistoryCount++
testTime := time.Now().Add(-1 * time.Hour)
sp := []*ec2.SpotPrice{
{
AvailabilityZone: aws.String("us-east-1c"),
InstanceType: aws.String("t2.micro"),
ProductDescription: aws.String("Linux/UNIX"),
SpotPrice: aws.String("0.003500"),
Timestamp: &testTime,
},
{
AvailabilityZone: aws.String("us-east-1f"),
InstanceType: aws.String("t2.micro"),
ProductDescription: aws.String("Linux/UNIX"),
SpotPrice: aws.String("0.003500"),
Timestamp: &testTime,
},
{
AvailabilityZone: aws.String("us-east-1b"),
InstanceType: aws.String("t2.micro"),
ProductDescription: aws.String("Linux/UNIX"),
SpotPrice: aws.String("0.003500"),
Timestamp: &testTime,
},
}
output := &ec2.DescribeSpotPriceHistoryOutput{SpotPriceHistory: sp}
return output, nil
}
func getMockConnSpot() ec2iface.EC2API {
mockConn := &mockEC2ConnSpot{
Config: aws.NewConfig(),
}
return mockConn
}
// Create statebag for running test
func tStateSpot() multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
state.Put("availability_zone", "us-east-1c")
state.Put("securityGroupIds", []string{"sg-0b8984db72f213dc3"})
state.Put("subnet_id", "subnet-077fde4e")
state.Put("source_image", "")
return state
}
func getBasicStep() *StepRunSpotInstance {
stepRunSpotInstance := StepRunSpotInstance{
AssociatePublicIpAddress: false,
BlockDevices: BlockDevices{
AMIBlockDevices: AMIBlockDevices{
AMIMappings: []BlockDevice(nil),
},
LaunchBlockDevices: LaunchBlockDevices{
LaunchMappings: []BlockDevice(nil),
},
},
BlockDurationMinutes: 0,
Debug: false,
Comm: &communicator.Config{
SSHKeyPairName: "foo",
},
EbsOptimized: false,
ExpectedRootDevice: "ebs",
IamInstanceProfile: "",
InstanceInitiatedShutdownBehavior: "stop",
InstanceType: "t2.micro",
SourceAMI: "",
SpotPrice: "auto",
SpotPriceProduct: "Linux/UNIX",
SpotTags: TagMap(nil),
Tags: TagMap{},
VolumeTags: TagMap(nil),
UserData: "",
UserDataFile: "",
}
return &stepRunSpotInstance
}
func TestCalculateSpotPrice(t *testing.T) {
stepRunSpotInstance := getBasicStep()
// Set spot price and spot price product
stepRunSpotInstance.SpotPrice = "auto"
stepRunSpotInstance.SpotPriceProduct = "Linux/UNIX"
ec2conn := getMockConnSpot()
// state := tStateSpot()
spotPrice, err := stepRunSpotInstance.CalculateSpotPrice("", ec2conn)
if err != nil {
t.Fatalf("Should not have had an error calculating spot price")
}
sp, _ := strconv.ParseFloat(spotPrice, 64)
expected := 0.008500
if sp != expected { // 0.003500 (from spot history) + .005
t.Fatalf("Expected spot price of \"0.008500\", not %s", spotPrice)
}
}

View File

@ -124,6 +124,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
SpotPriceProduct: b.config.SpotPriceAutoProduct, SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotTags: b.config.SpotTags, SpotTags: b.config.SpotTags,
Tags: b.config.RunTags, Tags: b.config.RunTags,
SpotInstanceTypes: b.config.SpotInstanceTypes,
UserData: b.config.UserData, UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile, UserDataFile: b.config.UserDataFile,
VolumeTags: b.config.VolumeRunTags, VolumeTags: b.config.VolumeRunTags,

View File

@ -152,6 +152,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
SourceAMI: b.config.SourceAmi, SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice, SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct, SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotInstanceTypes: b.config.SpotInstanceTypes,
SpotTags: b.config.SpotTags, SpotTags: b.config.SpotTags,
Tags: b.config.RunTags, Tags: b.config.RunTags,
UserData: b.config.UserData, UserData: b.config.UserData,

View File

@ -122,6 +122,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
SourceAMI: b.config.SourceAmi, SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice, SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct, SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotInstanceTypes: b.config.SpotInstanceTypes,
SpotTags: b.config.SpotTags, SpotTags: b.config.SpotTags,
Tags: b.config.RunTags, Tags: b.config.RunTags,
UserData: b.config.UserData, UserData: b.config.UserData,

View File

@ -202,6 +202,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
InstanceType: b.config.InstanceType, InstanceType: b.config.InstanceType,
SourceAMI: b.config.SourceAmi, SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice, SpotPrice: b.config.SpotPrice,
SpotInstanceTypes: b.config.SpotInstanceTypes,
SpotPriceProduct: b.config.SpotPriceAutoProduct, SpotPriceProduct: b.config.SpotPriceAutoProduct,
Tags: b.config.RunTags, Tags: b.config.RunTags,
SpotTags: b.config.SpotTags, SpotTags: b.config.SpotTags,

View File

@ -112,8 +112,8 @@ builder.
of `source_ami`. Can be `paravirtual` or `hvm`. of `source_ami`. Can be `paravirtual` or `hvm`.
- `associate_public_ip_address` (boolean) - If using a non-default VPC, - `associate_public_ip_address` (boolean) - If using a non-default VPC,
public IP addresses are not provided by default. If this is toggled, your public IP addresses are not provided by default. If this is `true`, your
new instance will get a Public IP. new instance will get a Public IP. default: `false`
- `availability_zone` (string) - Destination availability zone to launch - `availability_zone` (string) - Destination availability zone to launch
instance in. Leave this empty to allow Amazon to auto-assign. instance in. Leave this empty to allow Amazon to auto-assign.
@ -352,22 +352,7 @@ builder.
criteria provided in `source_ami_filter`; this pins the AMI returned by the criteria provided in `source_ami_filter`; this pins the AMI returned by the
filter, but will cause Packer to fail if the `source_ami` does not exist. filter, but will cause Packer to fail if the `source_ami` does not exist.
- `spot_price` (string) - The maximum hourly price to pay for a spot instance <%= partial "partials/builders/aws-spot-docs" %>
to create the AMI. Spot instances are a type of instance that EC2 starts
when the current spot price is less than the maximum price you specify.
Spot price will be updated based on available spot instance capacity and
current spot instance requests. It may save you some costs. You can set
this to `auto` for Packer to automatically discover the best spot price or
to "0" to use an on demand instance (default).
- `spot_price_auto_product` (string) - Required if `spot_price` is set to
`auto`. This tells Packer what sort of AMI you're launching to find the
best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`,
`Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`,
`Windows (Amazon VPC)`
- `spot_tags` (object of key/value strings) - Requires `spot_price` to be
set. This tells Packer to apply tags to the spot request that is issued.
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`

View File

@ -104,8 +104,8 @@ builder.
of `source_ami`. Can be `paravirtual` or `hvm`. of `source_ami`. Can be `paravirtual` or `hvm`.
- `associate_public_ip_address` (boolean) - If using a non-default VPC, - `associate_public_ip_address` (boolean) - If using a non-default VPC,
public IP addresses are not provided by default. If this is toggled, your public IP addresses are not provided by default. If this is `true`, your
new instance will get a Public IP. new instance will get a Public IP. default: `false`
- `availability_zone` (string) - Destination availability zone to launch - `availability_zone` (string) - Destination availability zone to launch
instance in. Leave this empty to allow Amazon to auto-assign. instance in. Leave this empty to allow Amazon to auto-assign.
@ -353,22 +353,7 @@ builder.
criteria provided in `source_ami_filter`; this pins the AMI returned by the criteria provided in `source_ami_filter`; this pins the AMI returned by the
filter, but will cause Packer to fail if the `source_ami` does not exist. filter, but will cause Packer to fail if the `source_ami` does not exist.
- `spot_price` (string) - The maximum hourly price to pay for a spot instance <%= partial "partials/builders/aws-spot-docs" %>
to create the AMI. Spot instances are a type of instance that EC2 starts
when the current spot price is less than the maximum price you specify.
Spot price will be updated based on available spot instance capacity and
current spot instance requests. It may save you some costs. You can set
this to `auto` for Packer to automatically discover the best spot price or
to "0" to use an on demand instance (default).
- `spot_price_auto_product` (string) - Required if `spot_price` is set to
`auto`. This tells Packer what sort of AMI you're launching to find the
best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`,
`Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`,
`Windows (Amazon VPC)`
- `spot_tags` (object of key/value strings) - Requires `spot_price` to be
set. This tells Packer to apply tags to the spot request that is issued.
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`

View File

@ -107,8 +107,8 @@ builder.
data](#build-template-data) for more information. data](#build-template-data) for more information.
- `associate_public_ip_address` (boolean) - If using a non-default VPC, - `associate_public_ip_address` (boolean) - If using a non-default VPC,
public IP addresses are not provided by default. If this is toggled, your public IP addresses are not provided by default. If this is `true`, your
new instance will get a Public IP. new instance will get a Public IP. default: `false`
- `availability_zone` (string) - Destination availability zone to launch - `availability_zone` (string) - Destination availability zone to launch
instance in. Leave this empty to allow Amazon to auto-assign. instance in. Leave this empty to allow Amazon to auto-assign.
@ -302,22 +302,7 @@ builder.
criteria provided in `source_ami_filter`; this pins the AMI returned by the criteria provided in `source_ami_filter`; this pins the AMI returned by the
filter, but will cause Packer to fail if the `source_ami` does not exist. filter, but will cause Packer to fail if the `source_ami` does not exist.
- `spot_price` (string) - The maximum hourly price to pay for a spot instance <%= partial "partials/builders/aws-spot-docs" %>
to create the AMI. Spot instances are a type of instance that EC2 starts
when the current spot price is less than the maximum price you specify.
Spot price will be updated based on available spot instance capacity and
current spot instance requests. It may save you some costs. You can set
this to `auto` for Packer to automatically discover the best spot price or
to `0` to use an on-demand instance (default).
- `spot_price_auto_product` (string) - Required if `spot_price` is set to
`auto`. This tells Packer what sort of AMI you're launching to find the
best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`,
`Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)` or
`Windows (Amazon VPC)`
- `spot_tags` (object of key/value strings) - Requires `spot_price` to be
set. This tells Packer to apply tags to the spot request that is issued.
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`

View File

@ -131,8 +131,8 @@ builder.
`paravirtual` (default) or `hvm`. `paravirtual` (default) or `hvm`.
- `associate_public_ip_address` (boolean) - If using a non-default VPC, - `associate_public_ip_address` (boolean) - If using a non-default VPC,
public IP addresses are not provided by default. If this is toggled, your public IP addresses are not provided by default. If this is `true`, your
new instance will get a Public IP. new instance will get a Public IP. default: `false`
- `availability_zone` (string) - Destination availability zone to launch - `availability_zone` (string) - Destination availability zone to launch
instance in. Leave this empty to allow Amazon to auto-assign. instance in. Leave this empty to allow Amazon to auto-assign.
@ -343,22 +343,7 @@ builder.
- `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot. - `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot.
They will override AMI tags if already applied to snapshot. They will override AMI tags if already applied to snapshot.
- `spot_price` (string) - The maximum hourly price to launch a spot instance <%= partial "partials/builders/aws-spot-docs" %>
to create the AMI. It is a type of instances that EC2 starts when the
maximum price that you specify exceeds the current spot price. Spot price
will be updated based on available spot instance capacity and current spot
Instance requests. It may save you some costs. You can set this to `auto`
for Packer to automatically discover the best spot price or to `0` to use
an on-demand instance (default).
- `spot_price_auto_product` (string) - Required if `spot_price` is set to
`auto`. This tells Packer what sort of AMI you're launching to find the
best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`,
`Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`,
`Windows (Amazon VPC)`
- `spot_tags` (object of key/value strings) - Requires `spot_price` to be
set. This tells Packer to apply tags to the spot request that is issued.
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`

View File

@ -0,0 +1,26 @@
- `spot_instance_types` (array of strings) - a list of acceptable instance
types to run your build on. We will request a spot instance using the max
price of `spot_price` and the allocation strategy of "lowest price".
Your instance will be launched on an instance type of the lowest available
price that you have in your list. This is used in place of instance_type.
You may only set either spot_instance_types or instance_type, not both.
This feature exists to help prevent situations where a Packer build fails
because a particular availability zone does not have capacity for the
specific instance_type requested in instance_type.
- `spot_price` (string) - The maximum hourly price to pay for a spot instance
to create the AMI. Spot instances are a type of instance that EC2 starts
when the current spot price is less than the maximum price you specify.
Spot price will be updated based on available spot instance capacity and
current spot instance requests. It may save you some costs. You can set
this to `auto` for Packer to automatically discover the best spot price or
to "0" to use an on demand instance (default).
- `spot_price_auto_product` (string) - Required if `spot_price` is set to
`auto`. This tells Packer what sort of AMI you're launching to find the
best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`,
`Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`,
`Windows (Amazon VPC)`
- `spot_tags` (object of key/value strings) - Requires `spot_price` to be
set. This tells Packer to apply tags to the spot request that is issued.