commit
65bd7207e8
|
@ -73,6 +73,7 @@ type RunConfig struct {
|
|||
SecurityGroupIds []string `mapstructure:"security_group_ids"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"`
|
||||
SpotInstanceTypes []string `mapstructure:"spot_instance_types"`
|
||||
SpotPrice string `mapstructure:"spot_price"`
|
||||
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
|
||||
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."))
|
||||
}
|
||||
|
||||
if c.InstanceType == "" {
|
||||
errs = append(errs, fmt.Errorf("An instance_type 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"))
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -14,10 +14,6 @@ import (
|
|||
"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.
|
||||
type mockEC2Conn struct {
|
||||
ec2iface.EC2API
|
||||
|
@ -120,7 +116,7 @@ func TestStepAmiRegionCopy_false_encryption(t *testing.T) {
|
|||
Regions: make([]string, 0),
|
||||
AMIKmsKeyId: "",
|
||||
RegionKeyIds: make(map[string]string),
|
||||
EncryptBootVolume: boolPointer(false),
|
||||
EncryptBootVolume: aws.Bool(false),
|
||||
Name: "fake-ami-name",
|
||||
OriginalRegion: "us-east-1",
|
||||
}
|
||||
|
@ -145,7 +141,7 @@ func TestStepAmiRegionCopy_true_encryption(t *testing.T) {
|
|||
Regions: make([]string, 0),
|
||||
AMIKmsKeyId: "",
|
||||
RegionKeyIds: make(map[string]string),
|
||||
EncryptBootVolume: boolPointer(true),
|
||||
EncryptBootVolume: aws.Bool(true),
|
||||
Name: "fake-ami-name",
|
||||
OriginalRegion: "us-east-1",
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"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/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
|
@ -35,6 +36,7 @@ type StepRunSpotInstance struct {
|
|||
SpotPrice string
|
||||
SpotPriceProduct string
|
||||
SpotTags TagMap
|
||||
SpotInstanceTypes []string
|
||||
Tags TagMap
|
||||
VolumeTags TagMap
|
||||
UserData string
|
||||
|
@ -45,17 +47,130 @@ type StepRunSpotInstance struct {
|
|||
spotRequest *ec2.SpotInstanceRequest
|
||||
}
|
||||
|
||||
func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||
securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
func (s *StepRunSpotInstance) CalculateSpotPrice(az string, ec2conn ec2iface.EC2API) (string, error) {
|
||||
// Calculate the spot price for a given availability zone
|
||||
spotPrice := s.SpotPrice
|
||||
|
||||
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
|
||||
if s.UserDataFile != "" {
|
||||
contents, err := ioutil.ReadFile(s.UserDataFile)
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
|
||||
return multistep.ActionHalt
|
||||
return "", fmt.Errorf("Problem reading user data file: %s", err)
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
|
@ -66,8 +181,16 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
|
|||
log.Printf("[DEBUG] base64 encoding user data...")
|
||||
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)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
spotPrice := s.SpotPrice
|
||||
azConfig := ""
|
||||
if azRaw, ok := state.GetOk("availability_zone"); ok {
|
||||
azConfig = azRaw.(string)
|
||||
}
|
||||
az := azConfig
|
||||
|
||||
if spotPrice == "auto" {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Finding spot price for %s %s...",
|
||||
s.SpotPriceProduct, s.InstanceType))
|
||||
|
||||
// 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 {
|
||||
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("Finding spot price for %s %s...",
|
||||
s.SpotPriceProduct, s.InstanceType))
|
||||
spotPrice, err := s.CalculateSpotPrice(az, ec2conn)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Determined spot instance price of: %s.", spotPrice))
|
||||
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
// This prints the tags to the ui; it doesn't actually add them to the
|
||||
// instance yet
|
||||
ec2Tags.Report(ui)
|
||||
|
||||
ui.Message(fmt.Sprintf(
|
||||
"Requesting spot instance '%s' for: %s",
|
||||
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,
|
||||
spotOptions := ec2.LaunchTemplateSpotMarketOptionsRequest{
|
||||
MaxPrice: &s.SpotPrice,
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.spotRequest = runSpotResp.SpotInstanceRequests[0]
|
||||
|
||||
spotRequestId := s.spotRequest.SpotInstanceRequestId
|
||||
ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
|
||||
err = WaitUntilSpotRequestFulfilled(ctx, ec2conn, *spotRequestId)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
// Add overrides for each user-provided instance type
|
||||
var overrides []*ec2.FleetLaunchTemplateOverridesRequest
|
||||
for _, instanceType := range s.SpotInstanceTypes {
|
||||
override := ec2.FleetLaunchTemplateOverridesRequest{
|
||||
InstanceType: aws.String(instanceType),
|
||||
}
|
||||
overrides = append(overrides, &override)
|
||||
}
|
||||
|
||||
spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
|
||||
SpotInstanceRequestIds: []*string{spotRequestId},
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
createFleetInput := &ec2.CreateFleetInput{
|
||||
LaunchTemplateConfigs: []*ec2.FleetLaunchTemplateConfigRequest{
|
||||
{
|
||||
LaunchTemplateSpecification: &ec2.FleetLaunchTemplateSpecificationRequest{
|
||||
LaunchTemplateName: aws.String("packer-fleet-launch-template"),
|
||||
Version: aws.String("1"),
|
||||
},
|
||||
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)
|
||||
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)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
|
@ -248,7 +330,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
|
|||
}.Run(ctx, func(ctx context.Context) error {
|
||||
_, err := ec2conn.CreateTags(&ec2.CreateTagsInput{
|
||||
Tags: spotTags,
|
||||
Resources: []*string{spotRequestId},
|
||||
Resources: []*string{aws.String(req.RequestID)},
|
||||
})
|
||||
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
|
||||
s.instanceId = instanceId
|
||||
|
||||
ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
|
||||
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)
|
||||
// Actually send the spot connection request.
|
||||
err = req.Send()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", req.RequestID, err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
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{
|
||||
InstanceIds: []*string{aws.String(instanceId)},
|
||||
})
|
||||
|
@ -401,4 +491,12 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -124,6 +124,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
SpotTags: b.config.SpotTags,
|
||||
Tags: b.config.RunTags,
|
||||
SpotInstanceTypes: b.config.SpotInstanceTypes,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
VolumeTags: b.config.VolumeRunTags,
|
||||
|
|
|
@ -152,6 +152,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
SourceAMI: b.config.SourceAmi,
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
SpotInstanceTypes: b.config.SpotInstanceTypes,
|
||||
SpotTags: b.config.SpotTags,
|
||||
Tags: b.config.RunTags,
|
||||
UserData: b.config.UserData,
|
||||
|
|
|
@ -122,6 +122,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
SourceAMI: b.config.SourceAmi,
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
SpotInstanceTypes: b.config.SpotInstanceTypes,
|
||||
SpotTags: b.config.SpotTags,
|
||||
Tags: b.config.RunTags,
|
||||
UserData: b.config.UserData,
|
||||
|
|
|
@ -202,6 +202,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
InstanceType: b.config.InstanceType,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
SpotPrice: b.config.SpotPrice,
|
||||
SpotInstanceTypes: b.config.SpotInstanceTypes,
|
||||
SpotPriceProduct: b.config.SpotPriceAutoProduct,
|
||||
Tags: b.config.RunTags,
|
||||
SpotTags: b.config.SpotTags,
|
||||
|
|
|
@ -112,8 +112,8 @@ builder.
|
|||
of `source_ami`. Can be `paravirtual` or `hvm`.
|
||||
|
||||
- `associate_public_ip_address` (boolean) - If using a non-default VPC,
|
||||
public IP addresses are not provided by default. If this is toggled, your
|
||||
new instance will get a Public IP.
|
||||
public IP addresses are not provided by default. If this is `true`, your
|
||||
new instance will get a Public IP. default: `false`
|
||||
|
||||
- `availability_zone` (string) - Destination availability zone to launch
|
||||
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
|
||||
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
|
||||
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.
|
||||
<%= partial "partials/builders/aws-spot-docs" %>
|
||||
|
||||
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
|
||||
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`
|
||||
|
|
|
@ -104,8 +104,8 @@ builder.
|
|||
of `source_ami`. Can be `paravirtual` or `hvm`.
|
||||
|
||||
- `associate_public_ip_address` (boolean) - If using a non-default VPC,
|
||||
public IP addresses are not provided by default. If this is toggled, your
|
||||
new instance will get a Public IP.
|
||||
public IP addresses are not provided by default. If this is `true`, your
|
||||
new instance will get a Public IP. default: `false`
|
||||
|
||||
- `availability_zone` (string) - Destination availability zone to launch
|
||||
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
|
||||
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
|
||||
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.
|
||||
<%= partial "partials/builders/aws-spot-docs" %>
|
||||
|
||||
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
|
||||
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`
|
||||
|
|
|
@ -107,8 +107,8 @@ builder.
|
|||
data](#build-template-data) for more information.
|
||||
|
||||
- `associate_public_ip_address` (boolean) - If using a non-default VPC,
|
||||
public IP addresses are not provided by default. If this is toggled, your
|
||||
new instance will get a Public IP.
|
||||
public IP addresses are not provided by default. If this is `true`, your
|
||||
new instance will get a Public IP. default: `false`
|
||||
|
||||
- `availability_zone` (string) - Destination availability zone to launch
|
||||
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
|
||||
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
|
||||
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.
|
||||
<%= partial "partials/builders/aws-spot-docs" %>
|
||||
|
||||
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
|
||||
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`
|
||||
|
|
|
@ -131,8 +131,8 @@ builder.
|
|||
`paravirtual` (default) or `hvm`.
|
||||
|
||||
- `associate_public_ip_address` (boolean) - If using a non-default VPC,
|
||||
public IP addresses are not provided by default. If this is toggled, your
|
||||
new instance will get a Public IP.
|
||||
public IP addresses are not provided by default. If this is `true`, your
|
||||
new instance will get a Public IP. default: `false`
|
||||
|
||||
- `availability_zone` (string) - Destination availability zone to launch
|
||||
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.
|
||||
They will override AMI tags if already applied to snapshot.
|
||||
|
||||
- `spot_price` (string) - The maximum hourly price to launch a spot instance
|
||||
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.
|
||||
<%= partial "partials/builders/aws-spot-docs" %>
|
||||
|
||||
- `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but
|
||||
not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute`
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue