diff --git a/builder/amazon/common/interpolate_build_info_test.go b/builder/amazon/common/interpolate_build_info_test.go index a09490b4f..6a5cc17b3 100644 --- a/builder/amazon/common/interpolate_build_info_test.go +++ b/builder/amazon/common/interpolate_build_info_test.go @@ -17,6 +17,7 @@ func testImage() *ec2.Image { Name: aws.String("ami_test_name"), OwnerId: aws.String("ami_test_owner_id"), ImageOwnerAlias: aws.String("ami_test_owner_alias"), + RootDeviceType: aws.String("ebs"), Tags: []*ec2.Tag{ { Key: aws.String("key-1"), diff --git a/builder/amazon/common/step_run_spot_instance.go b/builder/amazon/common/step_run_spot_instance.go index 75ec401e9..dc196817a 100644 --- a/builder/amazon/common/step_run_spot_instance.go +++ b/builder/amazon/common/step_run_spot_instance.go @@ -11,6 +11,7 @@ import ( "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/builder/amazon/common/awserrors" "github.com/hashicorp/packer/common/random" "github.com/hashicorp/packer/common/retry" @@ -35,6 +36,7 @@ type StepRunSpotInstance struct { ExpectedRootDevice string InstanceInitiatedShutdownBehavior string InstanceType string + Region string SourceAMI string SpotPrice string SpotTags map[string]string @@ -158,7 +160,7 @@ func (s *StepRunSpotInstance) LoadUserData() (string, error) { } func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - ec2conn := state.Get("ec2").(*ec2.EC2) + ec2conn := state.Get("ec2").(ec2iface.EC2API) ui := state.Get("ui").(packer.Ui) ui.Say("Launching a spot AWS instance...") @@ -197,7 +199,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) } // Convert tags from the tag map provided by the user into *ec2.Tag s - ec2Tags, err := TagMap(s.Tags).EC2Tags(s.Ctx, *ec2conn.Config.Region, state) + ec2Tags, err := TagMap(s.Tags).EC2Tags(s.Ctx, s.Region, state) if err != nil { err := fmt.Errorf("Error generating tags for source instance: %s", err) state.Put("error", err) @@ -221,6 +223,14 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) } marketOptions.SetMarketType(ec2.MarketTypeSpot) + spotTags, err := TagMap(s.SpotTags).EC2Tags(s.Ctx, s.Region, state) + if err != nil { + err := fmt.Errorf("Error generating tags for spot request: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + // Create a launch template for the instance ui.Message("Loading User Data File...") @@ -243,6 +253,14 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) LaunchTemplateName: aws.String(launchTemplateName), VersionDescription: aws.String("template generated by packer for launching spot instances"), } + if len(spotTags) > 0 { + launchTemplate.TagSpecifications = []*ec2.TagSpecification{ + { + ResourceType: aws.String("launch-template"), + Tags: spotTags, + }, + } + } // Tell EC2 to create the template _, err = ec2conn.CreateLaunchTemplate(launchTemplate) @@ -361,14 +379,6 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) instance := describeOutput.Reservations[0].Instances[0] // Tag the spot instance request (not the eventual spot instance) - spotTags, err := TagMap(s.SpotTags).EC2Tags(s.Ctx, *ec2conn.Config.Region, state) - if err != nil { - err := fmt.Errorf("Error generating tags for spot request: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - if len(spotTags) > 0 && len(s.SpotTags) > 0 { spotTags.Report(ui) // Use the instance ID to find out the SIR, so that we can tag the spot @@ -428,7 +438,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) if len(volumeIds) > 0 && len(s.VolumeTags) > 0 { ui.Say("Adding tags to source EBS Volumes") - volumeTags, err := TagMap(s.VolumeTags).EC2Tags(s.Ctx, *ec2conn.Config.Region, state) + volumeTags, err := TagMap(s.VolumeTags).EC2Tags(s.Ctx, s.Region, state) if err != nil { err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err) state.Put("error", err) diff --git a/builder/amazon/common/step_run_spot_instance_test.go b/builder/amazon/common/step_run_spot_instance_test.go index 89fa53f01..d00bf74dd 100644 --- a/builder/amazon/common/step_run_spot_instance_test.go +++ b/builder/amazon/common/step_run_spot_instance_test.go @@ -2,11 +2,13 @@ package common import ( "bytes" + "context" "fmt" "testing" "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" @@ -43,6 +45,7 @@ func getBasicStep() *StepRunSpotInstance { ExpectedRootDevice: "ebs", InstanceInitiatedShutdownBehavior: "stop", InstanceType: "t2.micro", + Region: "us-east-1", SourceAMI: "", SpotPrice: "auto", SpotTags: nil, @@ -134,3 +137,226 @@ func TestCreateTemplateData_NoEphemeral(t *testing.T) { // t.Fatalf("Should have created 26 mappings to keep ephemeral drives from appearing.") // } } + +type runSpotEC2ConnMock struct { + ec2iface.EC2API + + CreateLaunchTemplateParams []*ec2.CreateLaunchTemplateInput + CreateLaunchTemplateFn func(*ec2.CreateLaunchTemplateInput) (*ec2.CreateLaunchTemplateOutput, error) + + CreateFleetParams []*ec2.CreateFleetInput + CreateFleetFn func(*ec2.CreateFleetInput) (*ec2.CreateFleetOutput, error) + + CreateTagsParams []*ec2.CreateTagsInput + CreateTagsFn func(*ec2.CreateTagsInput) (*ec2.CreateTagsOutput, error) + + DescribeInstancesParams []*ec2.DescribeInstancesInput + DescribeInstancesFn func(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) +} + +func (m *runSpotEC2ConnMock) CreateLaunchTemplate(req *ec2.CreateLaunchTemplateInput) (*ec2.CreateLaunchTemplateOutput, error) { + m.CreateLaunchTemplateParams = append(m.CreateLaunchTemplateParams, req) + resp, err := m.CreateLaunchTemplateFn(req) + return resp, err +} + +func (m *runSpotEC2ConnMock) CreateFleet(req *ec2.CreateFleetInput) (*ec2.CreateFleetOutput, error) { + m.CreateFleetParams = append(m.CreateFleetParams, req) + if m.CreateFleetFn != nil { + resp, err := m.CreateFleetFn(req) + return resp, err + } else { + return nil, nil + } +} + +func (m *runSpotEC2ConnMock) DescribeInstances(req *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + m.DescribeInstancesParams = append(m.DescribeInstancesParams, req) + if m.DescribeInstancesFn != nil { + resp, err := m.DescribeInstancesFn(req) + return resp, err + } else { + return nil, nil + } +} + +func (m *runSpotEC2ConnMock) CreateTags(req *ec2.CreateTagsInput) (*ec2.CreateTagsOutput, error) { + m.CreateTagsParams = append(m.CreateTagsParams, req) + if m.CreateTagsFn != nil { + resp, err := m.CreateTagsFn(req) + return resp, err + } else { + return nil, nil + } +} + +func defaultEc2Mock(instanceId, spotRequestId, volumeId *string) *runSpotEC2ConnMock { + instance := &ec2.Instance{ + InstanceId: instanceId, + SpotInstanceRequestId: spotRequestId, + BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{ + { + Ebs: &ec2.EbsInstanceBlockDevice{ + VolumeId: volumeId, + }, + }, + }, + } + return &runSpotEC2ConnMock{ + CreateLaunchTemplateFn: func(in *ec2.CreateLaunchTemplateInput) (*ec2.CreateLaunchTemplateOutput, error) { + return &ec2.CreateLaunchTemplateOutput{ + LaunchTemplate: nil, + Warning: nil, + }, nil + }, + CreateFleetFn: func(*ec2.CreateFleetInput) (*ec2.CreateFleetOutput, error) { + return &ec2.CreateFleetOutput{ + Errors: nil, + FleetId: nil, + Instances: []*ec2.CreateFleetInstance{ + { + InstanceIds: []*string{instanceId}, + }, + }, + }, nil + }, + DescribeInstancesFn: func(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { + return &ec2.DescribeInstancesOutput{ + NextToken: nil, + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{instance}, + }, + }, + }, nil + }, + } +} + +func TestRun(t *testing.T) { + instanceId := aws.String("test-instance-id") + spotRequestId := aws.String("spot-id") + volumeId := aws.String("volume-id") + ec2Mock := defaultEc2Mock(instanceId, spotRequestId, volumeId) + + uiMock := packer.TestUi(t) + + state := tStateSpot() + state.Put("ec2", ec2Mock) + state.Put("ui", uiMock) + state.Put("source_image", testImage()) + + stepRunSpotInstance := getBasicStep() + stepRunSpotInstance.Tags["Name"] = "Packer Builder" + stepRunSpotInstance.Tags["test-tag"] = "test-value" + stepRunSpotInstance.SpotTags = map[string]string{ + "spot-tag": "spot-tag-value", + } + stepRunSpotInstance.VolumeTags = map[string]string{ + "volume-tag": "volume-tag-value", + } + + ctx := context.TODO() + action := stepRunSpotInstance.Run(ctx, state) + + if err := state.Get("error"); err != nil { + t.Fatalf("should not error, but: %v", err) + } + + if action != multistep.ActionContinue { + t.Fatalf("shoul continue, but: %v", action) + } + + if len(ec2Mock.CreateLaunchTemplateParams) != 1 { + t.Fatalf("createLaunchTemplate should be invoked once, but invoked %v", len(ec2Mock.CreateLaunchTemplateParams)) + } + launchTemplateName := ec2Mock.CreateLaunchTemplateParams[0].LaunchTemplateName + + if len(ec2Mock.CreateLaunchTemplateParams[0].TagSpecifications) != 1 { + t.Fatalf("exactly one launch template tag specification expected") + } + if *ec2Mock.CreateLaunchTemplateParams[0].TagSpecifications[0].ResourceType != "launch-template" { + t.Fatalf("resource type 'launch-template' expected") + } + if len(ec2Mock.CreateLaunchTemplateParams[0].TagSpecifications[0].Tags) != 1 { + t.Fatalf("1 launch template tag expected") + } + + nameTag := ec2Mock.CreateLaunchTemplateParams[0].TagSpecifications[0].Tags[0] + if *nameTag.Key != "spot-tag" || *nameTag.Value != "spot-tag-value" { + t.Fatalf("expected spot-tag: spot-tag-value") + } + + if len(ec2Mock.CreateFleetParams) != 1 { + t.Fatalf("createFleet should be invoked once, but invoked %v", len(ec2Mock.CreateLaunchTemplateParams)) + } + if *ec2Mock.CreateFleetParams[0].TargetCapacitySpecification.DefaultTargetCapacityType != "spot" { + t.Fatalf("capacity type should be spot") + } + if *ec2Mock.CreateFleetParams[0].TargetCapacitySpecification.TotalTargetCapacity != 1 { + t.Fatalf("target capacity should be 1") + } + if len(ec2Mock.CreateFleetParams[0].LaunchTemplateConfigs) != 1 { + t.Fatalf("exactly one launch config template expected") + } + if *ec2Mock.CreateFleetParams[0].LaunchTemplateConfigs[0].LaunchTemplateSpecification.LaunchTemplateName != *launchTemplateName { + t.Fatalf("launchTemplateName should match in createLaunchTemplate and createFleet requests") + } + + if len(ec2Mock.DescribeInstancesParams) != 1 { + t.Fatalf("describeInstancesParams should be invoked once, but invoked %v", len(ec2Mock.DescribeInstancesParams)) + } + if *ec2Mock.DescribeInstancesParams[0].InstanceIds[0] != *instanceId { + t.Fatalf("instanceId should match from createFleet response") + } + + uiMock.Say(fmt.Sprintf("%v", ec2Mock.CreateTagsParams)) + if len(ec2Mock.CreateTagsParams) != 3 { + t.Fatalf("createTags should be invoked 3 times") + } + if len(ec2Mock.CreateTagsParams[0].Resources) != 1 || *ec2Mock.CreateTagsParams[0].Resources[0] != *spotRequestId { + t.Fatalf("should create tags for spot request") + } + if len(ec2Mock.CreateTagsParams[1].Resources) != 1 || *ec2Mock.CreateTagsParams[1].Resources[0] != *instanceId { + t.Fatalf("should create tags for instance") + } + if len(ec2Mock.CreateTagsParams[2].Resources) != 1 || ec2Mock.CreateTagsParams[2].Resources[0] != volumeId { + t.Fatalf("should create tags for volume") + } +} + +func TestRun_NoSpotTags(t *testing.T) { + instanceId := aws.String("test-instance-id") + spotRequestId := aws.String("spot-id") + volumeId := aws.String("volume-id") + ec2Mock := defaultEc2Mock(instanceId, spotRequestId, volumeId) + + uiMock := packer.TestUi(t) + + state := tStateSpot() + state.Put("ec2", ec2Mock) + state.Put("ui", uiMock) + state.Put("source_image", testImage()) + + stepRunSpotInstance := getBasicStep() + stepRunSpotInstance.Tags["Name"] = "Packer Builder" + stepRunSpotInstance.Tags["test-tag"] = "test-value" + stepRunSpotInstance.VolumeTags = map[string]string{ + "volume-tag": "volume-tag-value", + } + + ctx := context.TODO() + action := stepRunSpotInstance.Run(ctx, state) + + if err := state.Get("error"); err != nil { + t.Fatalf("should not error, but: %v", err) + } + + if action != multistep.ActionContinue { + t.Fatalf("shoul continue, but: %v", action) + } + + if len(ec2Mock.CreateLaunchTemplateParams[0].TagSpecifications) != 0 { + t.Fatalf("0 launch template tags expected") + } +} diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 05f585113..f3d099042 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -186,6 +186,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack ExpectedRootDevice: "ebs", InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior, InstanceType: b.config.InstanceType, + Region: *ec2conn.Config.Region, SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, SpotTags: b.config.SpotTags, diff --git a/builder/amazon/ebssurrogate/builder.go b/builder/amazon/ebssurrogate/builder.go index aa27520fc..a351bfe89 100644 --- a/builder/amazon/ebssurrogate/builder.go +++ b/builder/amazon/ebssurrogate/builder.go @@ -209,6 +209,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack ExpectedRootDevice: "ebs", InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior, InstanceType: b.config.InstanceType, + Region: *ec2conn.Config.Region, SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, SpotInstanceTypes: b.config.SpotInstanceTypes, diff --git a/builder/amazon/ebsvolume/builder.go b/builder/amazon/ebsvolume/builder.go index 68ac53f41..ac31f94f7 100644 --- a/builder/amazon/ebsvolume/builder.go +++ b/builder/amazon/ebsvolume/builder.go @@ -197,6 +197,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack ExpectedRootDevice: "ebs", InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior, InstanceType: b.config.InstanceType, + Region: *ec2conn.Config.Region, SourceAMI: b.config.SourceAmi, SpotInstanceTypes: b.config.SpotInstanceTypes, SpotPrice: b.config.SpotPrice, diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 57b42d977..d07e26150 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -265,6 +265,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack Debug: b.config.PackerDebug, EbsOptimized: b.config.EbsOptimized, InstanceType: b.config.InstanceType, + Region: *ec2conn.Config.Region, SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, SpotInstanceTypes: b.config.SpotInstanceTypes,