diff --git a/cmd/launch.go b/cmd/launch.go index 51a90ca..05b068e 100644 --- a/cmd/launch.go +++ b/cmd/launch.go @@ -152,7 +152,6 @@ func launchInteractive(h *ec2helper.EC2Helper) { // Ask for confirmation or modification. Keep asking until the config is confirmed or denied var detailedConfig *config.DetailedInfo var confirmation string - var capacityTypeAnswer string for { // Parse config first detailedConfig, err = h.ParseConfig(simpleConfig) @@ -161,8 +160,7 @@ func launchInteractive(h *ec2helper.EC2Helper) { } // Ask for and set the capacity type - capacityTypeAnswer = question.AskCapacityType() - simpleConfig.CapacityType = capacityTypeAnswer + simpleConfig.CapacityType = question.AskCapacityType() // Ask for confirmation or modification confirmation = question.AskConfirmationWithInput(simpleConfig, detailedConfig, true) @@ -279,7 +277,7 @@ func LaunchCapacityInstance(h *ec2helper.EC2Helper, simpleConfig *config.SimpleI if simpleConfig.CapacityType == question.DefaultCapacityTypeText.OnDemand { _, err = h.LaunchInstance(simpleConfig, detailedConfig, confirmation == cli.ResponseYes) } else { - err = h.LaunchSpotInstance(simpleConfig, detailedConfig, confirmation) + err = h.LaunchSpotInstance(simpleConfig, detailedConfig, confirmation, nil) } return } @@ -337,13 +335,14 @@ func UseLaunchTemplateWithConfig(h *ec2helper.EC2Helper, simpleConfig *config.Si // Launch an instance with a launch template func LaunchWithLaunchTemplate(h *ec2helper.EC2Helper, simpleConfig *config.SimpleInfo) { + simpleConfig.CapacityType = question.AskCapacityType() confirmation, err := question.AskConfirmationWithTemplate(h, simpleConfig) if cli.ShowError(err, "Asking confirmation with launch template failed") { return } // Launch the instance. - _, err = h.LaunchInstance(simpleConfig, nil, *confirmation == cli.ResponseYes) + err = LaunchCapacityInstance(h, simpleConfig, nil, *confirmation) if cli.ShowError(err, "Launching instance failed") { return } diff --git a/go.mod b/go.mod index 015c753..b60812d 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/fatih/color v1.13.0 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect diff --git a/go.sum b/go.sum index 80487df..c3b5d6f 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= diff --git a/pkg/ec2helper/ec2helper.go b/pkg/ec2helper/ec2helper.go index fcd7a43..3ee983a 100644 --- a/pkg/ec2helper/ec2helper.go +++ b/pkg/ec2helper/ec2helper.go @@ -31,8 +31,10 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/google/uuid" ) const DefaultRegion = "us-east-2" @@ -1164,9 +1166,24 @@ func (h *EC2Helper) LaunchInstance(simpleConfig *config.SimpleInfo, detailedConf } } -func (h *EC2Helper) LaunchSpotInstance(simpleConfig *config.SimpleInfo, detailedConfig *config.DetailedInfo, confirmation string) (err error) { +func (h *EC2Helper) LaunchSpotInstance(simpleConfig *config.SimpleInfo, detailedConfig *config.DetailedInfo, confirmation string, template *ec2.LaunchTemplate) (err error) { fmt.Println("Spot Instance Testing") - _, err = h.LaunchInstance(simpleConfig, detailedConfig, confirmation == cli.ResponseYes) + if template != nil { + _, err = h.LaunchInstance(simpleConfig, detailedConfig, confirmation == cli.ResponseYes) // Replace with CreateFleet + } else { + template, err = h.CreateLaunchTemplate(simpleConfig) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + fmt.Println(aerr.Error()) + } else { + fmt.Println(err.Error()) + } + return + } + _, err = h.LaunchInstance(simpleConfig, detailedConfig, confirmation == cli.ResponseYes) // Replace with CreateFleet + err = h.DeleteLaunchTemplate(template.LaunchTemplateId) + } + return } @@ -1339,3 +1356,50 @@ func HasEbsVolume(image *ec2.Image) bool { return false } + +func (h *EC2Helper) CreateLaunchTemplate(simpleConfig *config.SimpleInfo) (template *ec2.LaunchTemplate, err error) { + launchIdentifier := uuid.New() + + fmt.Println("Creating Launch Template...") + input := &ec2.CreateLaunchTemplateInput{ + LaunchTemplateData: &ec2.RequestLaunchTemplateData{ + ImageId: &simpleConfig.ImageId, + InstanceType: &simpleConfig.InstanceType, + NetworkInterfaces: []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{ + { + AssociatePublicIpAddress: aws.Bool(true), + DeviceIndex: aws.Int64(0), + Ipv6AddressCount: aws.Int64(1), + SubnetId: aws.String(simpleConfig.SubnetId), + }, + }, + }, + TagSpecifications: []*ec2.TagSpecification{ + { + ResourceType: aws.String("launch-template"), + Tags: []*ec2.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("Ec2-LaunchTemplate"), + }, + }, + }, + }, + LaunchTemplateName: aws.String(fmt.Sprintf("SimpleEC2LaunchTemplate-%s", launchIdentifier)), + VersionDescription: aws.String(fmt.Sprintf("Lauch Template %s", launchIdentifier)), + } + + result, err := h.Svc.CreateLaunchTemplate(input) + template = result.LaunchTemplate + return +} + +func (h *EC2Helper) DeleteLaunchTemplate(templateId *string) (err error) { + fmt.Println("Deleting Launch Template...") + input := &ec2.DeleteLaunchTemplateInput{ + LaunchTemplateId: templateId, + } + + _, err = h.Svc.DeleteLaunchTemplate(input) + return +} diff --git a/pkg/ec2helper/ec2helper_test.go b/pkg/ec2helper/ec2helper_test.go index 331d6f7..5d32f47 100644 --- a/pkg/ec2helper/ec2helper_test.go +++ b/pkg/ec2helper/ec2helper_test.go @@ -152,6 +152,8 @@ func TestGetAvailableAvailabilityZones_NoResult(t *testing.T) { Launch Template Tests */ +var testLaunchId = "lt-12345" + func TestGetLaunchTemplatesInRegion_Success(t *testing.T) { expectedTemplates := []*ec2.LaunchTemplate{ { @@ -225,6 +227,34 @@ func TestGetLaunchTemplateById_DescribeLaunchTemplatesPagesError(t *testing.T) { th.Nok(t, err) } +func TestCreateLaunchTemplate(t *testing.T) { + config := config.NewSimpleInfo() + config.ImageId = "ami-12345" + config.InstanceType = "t2.micro" + config.SubnetId = "subnet-12345" + testEC2.Svc = &th.MockedEC2Svc{} + testEC2.CreateLaunchTemplate(config) + + launchTemplatesOutput, _ := testEC2.Svc.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{}) + templates := launchTemplatesOutput.LaunchTemplates + th.Equals(t, 1, len(templates)) + th.Equals(t, testLaunchId, *templates[0].LaunchTemplateId) +} + +func TestDeleteLaunchTemplate(t *testing.T) { + testEC2.Svc = &th.MockedEC2Svc{ + LaunchTemplates: []*ec2.LaunchTemplate{ + {LaunchTemplateId: &testLaunchId}, + }, + } + testEC2.DeleteLaunchTemplate(&testLaunchId) + + launchTemplatesOutput, _ := testEC2.Svc.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{}) + templates := launchTemplatesOutput.LaunchTemplates + + th.Equals(t, 0, len(templates)) +} + /* Launch Template Version Tests */ diff --git a/pkg/ec2helper/types.go b/pkg/ec2helper/types.go index f9dcd42..d566a34 100644 --- a/pkg/ec2helper/types.go +++ b/pkg/ec2helper/types.go @@ -37,6 +37,9 @@ type EC2Svc interface { RunInstances(input *ec2.RunInstancesInput) (*ec2.Reservation, error) TerminateInstances(input *ec2.TerminateInstancesInput) (*ec2.TerminateInstancesOutput, error) DeleteSecurityGroup(input *ec2.DeleteSecurityGroupInput) (*ec2.DeleteSecurityGroupOutput, error) + CreateLaunchTemplate(input *ec2.CreateLaunchTemplateInput) (*ec2.CreateLaunchTemplateOutput, error) + DeleteLaunchTemplate(input *ec2.DeleteLaunchTemplateInput) (*ec2.DeleteLaunchTemplateOutput, error) + DescribeLaunchTemplates(input *ec2.DescribeLaunchTemplatesInput) (*ec2.DescribeLaunchTemplatesOutput, error) } type EC2Helper struct { diff --git a/test/testhelper/ec2helper_mock.go b/test/testhelper/ec2helper_mock.go index 42e3021..e8a1289 100644 --- a/test/testhelper/ec2helper_mock.go +++ b/test/testhelper/ec2helper_mock.go @@ -374,6 +374,33 @@ func findFilter(filters []*ec2.Filter, name string) []*string { return nil } +func (e *MockedEC2Svc) CreateLaunchTemplate(input *ec2.CreateLaunchTemplateInput) (*ec2.CreateLaunchTemplateOutput, error) { + output := &ec2.CreateLaunchTemplateOutput{ + LaunchTemplate: &ec2.LaunchTemplate{ + LaunchTemplateId: aws.String("lt-12345"), + }, + } + e.LaunchTemplates = append(e.LaunchTemplates, output.LaunchTemplate) + return output, nil +} + +func (e *MockedEC2Svc) DeleteLaunchTemplate(input *ec2.DeleteLaunchTemplateInput) (*ec2.DeleteLaunchTemplateOutput, error) { + for index, template := range e.LaunchTemplates { + if *template.LaunchTemplateId == "lt-12345" { + e.LaunchTemplates = append(e.LaunchTemplates[:index], e.LaunchTemplates[index+1:]...) + return nil, nil + } + } + return nil, nil +} + +func (e *MockedEC2Svc) DescribeLaunchTemplates(input *ec2.DescribeLaunchTemplatesInput) (*ec2.DescribeLaunchTemplatesOutput, error) { + output := &ec2.DescribeLaunchTemplatesOutput{ + LaunchTemplates: e.LaunchTemplates, + } + return output, nil +} + // Placeholder functions func (e *MockedEC2Svc) DeleteSecurityGroup(input *ec2.DeleteSecurityGroupInput) (*ec2.DeleteSecurityGroupOutput, error) { return nil, nil