367 lines
12 KiB
Go
367 lines
12 KiB
Go
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-plugin-sdk/communicator"
|
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
)
|
|
|
|
// Create statebag for running test
|
|
func tStateSpot() multistep.StateBag {
|
|
state := new(multistep.BasicStateBag)
|
|
state.Put("ui", &packersdk.BasicUi{
|
|
Reader: new(bytes.Buffer),
|
|
Writer: new(bytes.Buffer),
|
|
})
|
|
state.Put("availability_zone", "us-east-1c")
|
|
state.Put("securityGroupIds", []string{"sg-0b8984db72f213dc3"})
|
|
state.Put("iamInstanceProfile", "packer-123")
|
|
state.Put("subnet_id", "subnet-077fde4e")
|
|
state.Put("source_image", "")
|
|
return state
|
|
}
|
|
|
|
func getBasicStep() *StepRunSpotInstance {
|
|
stepRunSpotInstance := StepRunSpotInstance{
|
|
PollingConfig: new(AWSPollingConfig),
|
|
AssociatePublicIpAddress: false,
|
|
LaunchMappings: BlockDevices{},
|
|
BlockDurationMinutes: 0,
|
|
Debug: false,
|
|
Comm: &communicator.Config{
|
|
SSH: communicator.SSH{
|
|
SSHKeyPairName: "foo",
|
|
},
|
|
},
|
|
EbsOptimized: false,
|
|
ExpectedRootDevice: "ebs",
|
|
InstanceInitiatedShutdownBehavior: "stop",
|
|
InstanceType: "t2.micro",
|
|
Region: "us-east-1",
|
|
SourceAMI: "",
|
|
SpotPrice: "auto",
|
|
SpotTags: nil,
|
|
Tags: map[string]string{},
|
|
VolumeTags: nil,
|
|
UserData: "",
|
|
UserDataFile: "",
|
|
}
|
|
|
|
return &stepRunSpotInstance
|
|
}
|
|
|
|
func TestCreateTemplateData(t *testing.T) {
|
|
state := tStateSpot()
|
|
stepRunSpotInstance := getBasicStep()
|
|
template := stepRunSpotInstance.CreateTemplateData(aws.String("userdata"), "az", state,
|
|
&ec2.LaunchTemplateInstanceMarketOptionsRequest{})
|
|
|
|
// expected := []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{
|
|
// &ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{
|
|
// DeleteOnTermination: aws.Bool(true),
|
|
// DeviceIndex: aws.Int64(0),
|
|
// Groups: aws.StringSlice([]string{"sg-0b8984db72f213dc3"}),
|
|
// SubnetId: aws.String("subnet-077fde4e"),
|
|
// },
|
|
// }
|
|
// if expected != template.NetworkInterfaces {
|
|
if template.NetworkInterfaces == nil {
|
|
t.Fatalf("Template should have contained a networkInterface object: recieved %#v", template.NetworkInterfaces)
|
|
}
|
|
|
|
if *template.IamInstanceProfile.Name != state.Get("iamInstanceProfile") {
|
|
t.Fatalf("Template should have contained a InstanceProfile name: recieved %#v", template.IamInstanceProfile.Name)
|
|
}
|
|
|
|
// Rerun, this time testing that we set security group IDs
|
|
state.Put("subnet_id", "")
|
|
template = stepRunSpotInstance.CreateTemplateData(aws.String("userdata"), "az", state,
|
|
&ec2.LaunchTemplateInstanceMarketOptionsRequest{})
|
|
if template.NetworkInterfaces != nil {
|
|
t.Fatalf("Template shouldn't contain network interfaces object if subnet_id is unset.")
|
|
}
|
|
|
|
// Rerun, this time testing that instance doesn't have instance profile is iamInstanceProfile is unset
|
|
state.Put("iamInstanceProfile", "")
|
|
template = stepRunSpotInstance.CreateTemplateData(aws.String("userdata"), "az", state,
|
|
&ec2.LaunchTemplateInstanceMarketOptionsRequest{})
|
|
fmt.Println(template.IamInstanceProfile)
|
|
if *template.IamInstanceProfile.Name != "" {
|
|
t.Fatalf("Template shouldn't contain instance profile if iamInstanceProfile is unset.")
|
|
}
|
|
}
|
|
|
|
func TestCreateTemplateData_NoEphemeral(t *testing.T) {
|
|
state := tStateSpot()
|
|
stepRunSpotInstance := getBasicStep()
|
|
stepRunSpotInstance.NoEphemeral = true
|
|
template := stepRunSpotInstance.CreateTemplateData(aws.String("userdata"), "az", state,
|
|
&ec2.LaunchTemplateInstanceMarketOptionsRequest{})
|
|
if len(template.BlockDeviceMappings) != 26 {
|
|
t.Fatalf("Should have created 26 mappings to keep ephemeral drives from appearing.")
|
|
}
|
|
|
|
// Now check that noEphemeral doesn't mess with the mappings in real life.
|
|
// state = tStateSpot()
|
|
// stepRunSpotInstance = getBasicStep()
|
|
// stepRunSpotInstance.NoEphemeral = true
|
|
// mappings := []*ec2.InstanceBlockDeviceMapping{
|
|
// &ec2.InstanceBlockDeviceMapping{
|
|
// DeviceName: "xvda",
|
|
// Ebs: {
|
|
// DeleteOnTermination: true,
|
|
// Status: "attaching",
|
|
// VolumeId: "vol-044cd49c330f21c05",
|
|
// },
|
|
// },
|
|
// &ec2.InstanceBlockDeviceMapping{
|
|
// DeviceName: "/dev/xvdf",
|
|
// Ebs: {
|
|
// DeleteOnTermination: false,
|
|
// Status: "attaching",
|
|
// VolumeId: "vol-0eefaf2d6ae35827e",
|
|
// },
|
|
// },
|
|
// }
|
|
// template = stepRunSpotInstance.CreateTemplateData(aws.String("userdata"), "az", state,
|
|
// &ec2.LaunchTemplateInstanceMarketOptionsRequest{})
|
|
// if len(*template.BlockDeviceMappings) != 26 {
|
|
// 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, launchTemplateId *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: &ec2.LaunchTemplate{
|
|
LaunchTemplateId: launchTemplateId,
|
|
},
|
|
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")
|
|
launchTemplateId := aws.String("launchTemplateId")
|
|
ec2Mock := defaultEc2Mock(instanceId, spotRequestId, volumeId, launchTemplateId)
|
|
|
|
uiMock := packersdk.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")
|
|
launchTemplateId := aws.String("lt-id")
|
|
ec2Mock := defaultEc2Mock(instanceId, spotRequestId, volumeId, launchTemplateId)
|
|
|
|
uiMock := packersdk.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")
|
|
}
|
|
}
|