From 6c596cdb8d2830520b4670108f6c08dc62f65158 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 7 Jul 2022 17:52:40 -0500 Subject: [PATCH 01/12] added instance type sorting --- cmd/examples/example1.go | 70 ---- cmd/main.go | 43 ++- pkg/selector/selector.go | 139 +++++++- pkg/selector/selector_test.go | 626 ---------------------------------- 4 files changed, 163 insertions(+), 715 deletions(-) delete mode 100644 cmd/examples/example1.go delete mode 100644 pkg/selector/selector_test.go diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go deleted file mode 100644 index ef0200b..0000000 --- a/cmd/examples/example1.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" -) - -func main() { - // Load an AWS session by looking at shared credentials or environment variables - // https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ - sess, err := session.NewSession(&aws.Config{ - Region: aws.String("us-east-2"), - }) - if err != nil { - fmt.Printf("Oh no, AWS session credentials cannot be found: %v", err) - return - } - - // Instantiate a new instance of a selector with the AWS session - instanceSelector := selector.New(sess) - - // Instantiate an int range filter to specify min and max vcpus - vcpusRange := selector.IntRangeFilter{ - LowerBound: 2, - UpperBound: 4, - } - // Instantiate a byte quantity range filter to specify min and max memory in GiB - memoryRange := selector.ByteQuantityRangeFilter{ - LowerBound: bytequantity.FromGiB(2), - UpperBound: bytequantity.FromGiB(4), - } - // Create a string for the CPU Architecture so that it can be passed as a pointer - // when creating the Filter struct - cpuArch := "x86_64" - - // Create a Filter struct with criteria you would like to filter - // The full struct definition can be found here for all of the supported filters: - // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go - filters := selector.Filters{ - VCpusRange: &vcpusRange, - MemoryRange: &memoryRange, - CPUArchitecture: &cpuArch, - } - - // Pass the Filter struct to the FilteredInstanceTypes function of your - // selector instance to get a list of filtered instance types and their details - instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters) - if err != nil { - fmt.Printf("Oh no, there was an error getting instance types: %v", err) - return - } - - // Pass in your list of instance type details to the appropriate output function - // in order to format the instance types as printable strings. - maxResults := 100 - instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) - if err != nil { - fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) - return - } - instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) - - // Print the returned instance types slice - fmt.Println(instanceTypes) -} diff --git a/cmd/main.go b/cmd/main.go index 2b526ba..8fa586c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -100,15 +100,17 @@ const ( // Configuration Flag Constants const ( - maxResults = "max-results" - profile = "profile" - help = "help" - verbose = "verbose" - version = "version" - region = "region" - output = "output" - cacheTTL = "cache-ttl" - cacheDir = "cache-dir" + maxResults = "max-results" + profile = "profile" + help = "help" + verbose = "verbose" + version = "version" + region = "region" + output = "output" + cacheTTL = "cache-ttl" + cacheDir = "cache-dir" + sortDirection = "sort-direction" + sortFilter = "sort-filter" // Output constants @@ -147,6 +149,19 @@ Full docs can be found at github.com/aws/amazon-` + binName simpleOutput, } + cliSortCriteria := []string{ + selector.ODPriceSortFlag, + selector.SpotPriceSortFlag, + selector.VcpuSortFlag, + selector.MemorySortFlag, + selector.NameSortFlag, + } + + cliSortDirections := []string{ + selector.SortAscendingFlag, + selector.SortDescendingFlag, + } + // Registers flags with specific input types from the cli pkg // Filter Flags - These will be grouped at the top of the help flags @@ -211,6 +226,8 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") + cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(selector.SortAscendingFlag), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) + cli.ConfigStringOptionsFlag(sortFilter, nil, cli.StringMe(selector.NameSortFlag), fmt.Sprintf("Specify the field to sort by (%s)", strings.Join(cliSortCriteria, ", ")), cliSortCriteria) // Parses the user input with the registered flags and runs type specific validation on the user input flags, err := cli.ParseAndValidateFlags() @@ -335,6 +352,14 @@ Full docs can be found at github.com/aws/amazon-` + binName os.Exit(1) } + // sort instance types + sortFilterFlag := cli.StringMe(flags[sortFilter]) + sortDirectionFlag := cli.StringMe(flags[sortDirection]) + instanceTypeDetails, err = instanceSelector.SortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) + if err != nil { + fmt.Printf("An error occurred when sorting instance types: %v", err) + } + // format instance types as strings maxOutputResults := cli.IntMe(flags[maxResults]) instanceTypes, itemsTruncated, err := formatInstanceTypes(instanceTypeDetails, maxOutputResults, outputFlag) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index f20d8f2..92bf4e5 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -99,6 +99,17 @@ const ( virtualizationTypePV = "pv" pricePerHour = "pricePerHour" + + // Sorting constants + + ODPriceSortFlag = "on-demand-price" + SpotPriceSortFlag = "spot-price" + VcpuSortFlag = "vcpu" + MemorySortFlag = "memory" + NameSortFlag = "instance-type-name" + + SortAscendingFlag = "ascending" + SortDescendingFlag = "descending" ) // New creates an instance of Selector provided an aws session @@ -201,20 +212,128 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai filteredInstanceTypes = append(filteredInstanceTypes, it) } - return sortInstanceTypeInfo(filteredInstanceTypes), nil + return filteredInstanceTypes, nil } -// sortInstanceTypeInfo will sort based on instance type info alpha-numerically -func sortInstanceTypeInfo(instanceTypeInfoSlice []*instancetypes.Details) []*instancetypes.Details { - if len(instanceTypeInfoSlice) < 2 { - return instanceTypeInfoSlice +// SortInstanceTypes acepts a list of instance type details, a sort filter flag, and a sort direction flag and +// returns a sorted list of instance type details sorted based on the sort filter and sort direction. +// Accepted sort filter flags: "on-demand-price", "spot-price", "vcpu", "memory" , "instance-type-name". +// Accepted sort direction flags: "ascending", "descending". +func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, sortFilterFlag *string, sortDirectionFlag *string) ([]*instancetypes.Details, error) { + if len(instanceTypes) <= 1 { + return instanceTypes, nil + } + + var isDescending bool + if sortDirectionFlag != nil { + switch *sortDirectionFlag { + case SortDescendingFlag: + isDescending = true + case SortAscendingFlag: + isDescending = false + default: + return nil, fmt.Errorf("invalid sort direction flag: %s", *sortDirectionFlag) + } + } else { + return nil, fmt.Errorf("sort direction flag is nil") + } + + // if sorting based on either on demand or spot price, ensure the appropriate cache + // has been refreshed. + if sortFilterFlag != nil { + if *sortFilterFlag == ODPriceSortFlag { + if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) + } + } else if *sortFilterFlag == SpotPriceSortFlag { + if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) + } + } } - sort.Slice(instanceTypeInfoSlice, func(i, j int) bool { - iInstanceInfo := instanceTypeInfoSlice[i] - jInstanceInfo := instanceTypeInfoSlice[j] - return strings.Compare(aws.StringValue(iInstanceInfo.InstanceType), aws.StringValue(jInstanceInfo.InstanceType)) <= 0 + + // sort instance types based on filter flag and isDescending + isSortingFlagValid := true + sort.Slice(instanceTypes, func(i, j int) bool { + firstType := instanceTypes[i] + secondType := instanceTypes[j] + + // Determine which value to sort by. + // Handle nil values by making non nil values always less than the nil values. That way the + // nil values can be bubbled up to the end of the list. + switch *sortFilterFlag { + case ODPriceSortFlag: + if firstType.OndemandPricePerHour == nil { + return false + } else if secondType.OndemandPricePerHour == nil { + return true + } + + if isDescending { + return *firstType.OndemandPricePerHour > *secondType.OndemandPricePerHour + } else { + return *firstType.OndemandPricePerHour <= *secondType.OndemandPricePerHour + } + case SpotPriceSortFlag: + if firstType.SpotPrice == nil { + return false + } else if secondType.SpotPrice == nil { + return true + } + + if isDescending { + return *firstType.SpotPrice > *secondType.SpotPrice + } else { + return *firstType.SpotPrice <= *secondType.SpotPrice + } + case VcpuSortFlag: + if firstType.VCpuInfo == nil || firstType.VCpuInfo.DefaultVCpus == nil { + return false + } else if secondType.VCpuInfo == nil || secondType.VCpuInfo.DefaultVCpus == nil { + return true + } + + if isDescending { + return *firstType.VCpuInfo.DefaultVCpus > *secondType.VCpuInfo.DefaultVCpus + } else { + return *firstType.VCpuInfo.DefaultVCpus <= *secondType.VCpuInfo.DefaultVCpus + } + case MemorySortFlag: + if firstType.MemoryInfo == nil || firstType.MemoryInfo.SizeInMiB == nil { + return false + } else if secondType.MemoryInfo == nil || secondType.MemoryInfo.SizeInMiB == nil { + return true + } + + if isDescending { + return *firstType.MemoryInfo.SizeInMiB > *secondType.MemoryInfo.SizeInMiB + } else { + return *firstType.MemoryInfo.SizeInMiB <= *secondType.MemoryInfo.SizeInMiB + } + case NameSortFlag: + if firstType.InstanceType == nil { + return false + } else if secondType.InstanceType == nil { + return true + } + + if isDescending { + return strings.Compare(aws.StringValue(secondType.InstanceType), aws.StringValue(firstType.InstanceType)) <= 0 + } else { + return strings.Compare(aws.StringValue(firstType.InstanceType), aws.StringValue(secondType.InstanceType)) <= 0 + } + default: + // invalid sorting flag. Do not sort. + isSortingFlagValid = false + return true + } }) - return instanceTypeInfoSlice + + if !isSortingFlagValid { + return nil, fmt.Errorf("invalid sort filter flag: %s", *sortFilterFlag) + } + + return instanceTypes, nil } // AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go deleted file mode 100644 index d77f3a8..0000000 --- a/pkg/selector/selector_test.go +++ /dev/null @@ -1,626 +0,0 @@ -// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file is distributed -// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -package selector_test - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "regexp" - "strconv" - "testing" - - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" - h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/ec2/ec2iface" -) - -const ( - describeInstanceTypesPages = "DescribeInstanceTypesPages" - describeInstanceTypes = "DescribeInstanceTypes" - describeInstanceTypeOfferings = "DescribeInstanceTypeOfferings" - describeAvailabilityZones = "DescribeAvailabilityZones" - mockFilesPath = "../../test/static" -) - -// Mocking helpers - -type itFn = func(page *ec2.DescribeInstanceTypesOutput, lastPage bool) bool -type ioFn = func(page *ec2.DescribeInstanceTypeOfferingsOutput, lastPage bool) bool - -type mockedEC2 struct { - ec2iface.EC2API - DescribeInstanceTypesPagesResp ec2.DescribeInstanceTypesOutput - DescribeInstanceTypesPagesErr error - DescribeInstanceTypesResp ec2.DescribeInstanceTypesOutput - DescribeInstanceTypesErr error - DescribeInstanceTypeOfferingsRespFn func(zone string) *ec2.DescribeInstanceTypeOfferingsOutput - DescribeInstanceTypeOfferingsResp ec2.DescribeInstanceTypeOfferingsOutput - DescribeInstanceTypeOfferingsErr error - DescribeAvailabilityZonesResp ec2.DescribeAvailabilityZonesOutput - DescribeAvailabilityZonesErr error -} - -func (m mockedEC2) DescribeAvailabilityZones(input *ec2.DescribeAvailabilityZonesInput) (*ec2.DescribeAvailabilityZonesOutput, error) { - return &m.DescribeAvailabilityZonesResp, m.DescribeAvailabilityZonesErr -} - -func (m mockedEC2) DescribeInstanceTypes(input *ec2.DescribeInstanceTypesInput) (*ec2.DescribeInstanceTypesOutput, error) { - return &m.DescribeInstanceTypesResp, m.DescribeInstanceTypesErr -} - -func (m mockedEC2) DescribeInstanceTypesPages(input *ec2.DescribeInstanceTypesInput, fn itFn) error { - fn(&m.DescribeInstanceTypesPagesResp, true) - return m.DescribeInstanceTypesPagesErr -} - -func (m mockedEC2) DescribeInstanceTypeOfferingsPages(input *ec2.DescribeInstanceTypeOfferingsInput, fn ioFn) error { - if m.DescribeInstanceTypeOfferingsRespFn != nil { - fn(m.DescribeInstanceTypeOfferingsRespFn(*input.Filters[0].Values[0]), true) - } else { - fn(&m.DescribeInstanceTypeOfferingsResp, true) - } - return m.DescribeInstanceTypeOfferingsErr -} - -func mockMultiRespDescribeInstanceTypesOfferings(t *testing.T, locationToFile map[string]string) mockedEC2 { - api := describeInstanceTypeOfferings - locationToResp := map[string]ec2.DescribeInstanceTypeOfferingsOutput{} - for zone, file := range locationToFile { - mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) - h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) - ditoo := ec2.DescribeInstanceTypeOfferingsOutput{} - err = json.Unmarshal(mockFile, &ditoo) - h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - locationToResp[zone] = ditoo - } - return mockedEC2{ - DescribeInstanceTypeOfferingsRespFn: func(input string) *ec2.DescribeInstanceTypeOfferingsOutput { - resp := locationToResp[input] - return &resp - }, - } -} - -func setupMock(t *testing.T, api string, file string) mockedEC2 { - mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) - h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) - switch api { - case describeInstanceTypes: - dito := ec2.DescribeInstanceTypesOutput{} - err = json.Unmarshal(mockFile, &dito) - h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - return mockedEC2{ - DescribeInstanceTypesResp: dito, - } - case describeInstanceTypesPages: - dito := ec2.DescribeInstanceTypesOutput{} - err = json.Unmarshal(mockFile, &dito) - h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - return mockedEC2{ - DescribeInstanceTypesPagesResp: dito, - } - case describeInstanceTypeOfferings: - ditoo := ec2.DescribeInstanceTypeOfferingsOutput{} - err = json.Unmarshal(mockFile, &ditoo) - h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - return mockedEC2{ - DescribeInstanceTypeOfferingsResp: ditoo, - } - case describeAvailabilityZones: - dazo := ec2.DescribeAvailabilityZonesOutput{} - err = json.Unmarshal(mockFile, &dazo) - h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) - return mockedEC2{ - DescribeAvailabilityZonesResp: dazo, - } - default: - h.Assert(t, false, "Unable to mock the provided API type "+api) - } - return mockedEC2{} -} - -func getSelector(ec2Mock mockedEC2) selector.Selector { - return selector.Selector{ - EC2: ec2Mock, - EC2Pricing: &ec2PricingMock{}, - InstanceTypesProvider: instancetypes.NewProvider("", "us-east-1", 0, ec2Mock), - } -} - -// Tests - -func TestNew(t *testing.T) { - itf := selector.New(session.Must(session.NewSession())) - h.Assert(t, itf != nil, "selector instance created without error") -} - -func TestFilterInstanceTypes(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filter := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, - } - - results, err := itf.FilterInstanceTypes(filter) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 intance type with 2 vcpus") - instanceTypeName := results[0].InstanceType - h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") - h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) -} - -func TestFilterInstanceTypes_NoResults(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 0, "Should return 0 instance type with 4 vcpus") -} - -func TestFilterInstanceTypes_AZFilteredIn(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "t3_micro.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, - } - itf := getSelector(ec2Mock) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, - AvailabilityZones: &[]string{"us-east-2a"}, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus but actually returned "+strconv.Itoa(len(results))) - instanceTypeName := results[0].InstanceType - h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") - h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) -} - -func TestFilterInstanceTypes_AZFilteredOut(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "t3_micro.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a_only_c5d12x.json").DescribeInstanceTypeOfferingsResp, - DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, - } - itf := getSelector(ec2Mock) - filters := selector.Filters{ - AvailabilityZones: &[]string{"us-east-2a"}, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 0, "Should return 0 instance types in us-east-2a but actually returned "+strconv.Itoa(len(results))) -} - -func TestFilterInstanceTypes_AZFilteredErr(t *testing.T) { - itf := getSelector(mockedEC2{}) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, - AvailabilityZones: &[]string{"blah"}, - } - - _, err := itf.FilterInstanceTypes(filters) - - h.Assert(t, err != nil, "Should error since bad zone was passed in") -} - -func TestFilterInstanceTypes_Gpus(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro_and_p3_16xl.json")) - gpuMemory, err := bytequantity.ParseToByteQuantity("128g") - h.Ok(t, err) - filters := selector.Filters{ - GpusRange: &selector.IntRangeFilter{LowerBound: 8, UpperBound: 8}, - GpuMemoryRange: &selector.ByteQuantityRangeFilter{ - LowerBound: gpuMemory, - UpperBound: gpuMemory, - }, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus but actually returned "+strconv.Itoa(len(results))) - instanceTypeName := results[0].InstanceType - h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") - h.Assert(t, *instanceTypeName == "p3.16xlarge", "Should return p3.16xlarge, got %s instead", *results[0].InstanceType) -} - -func TestFilterInstanceTypes_MoreFilters(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, - BareMetal: aws.Bool(false), - CPUArchitecture: aws.String("x86_64"), - Hypervisor: aws.String("nitro"), - EnaSupport: aws.Bool(true), - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus") - instanceTypeName := results[0].InstanceType - h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") - h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) -} - -func TestFilterInstanceTypes_Failure(t *testing.T) { - itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Assert(t, results == nil, "Results should be nil") - h.Assert(t, err != nil, "An error should be returned") -} - -func TestFilterInstanceTypes_InstanceTypeBase(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesResp: setupMock(t, describeInstanceTypes, "c4_large.json").DescribeInstanceTypesResp, - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - } - itf := getSelector(ec2Mock) - c4Large := "c4.large" - filters := selector.Filters{ - InstanceTypeBase: &c4Large, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 3, "c4.large should return 3 similar instance types") -} - -func TestFilterInstanceTypes_AllowList(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - } - itf := getSelector(ec2Mock) - allowRegex, err := regexp.Compile("c4.large") - h.Ok(t, err) - filters := selector.Filters{ - AllowList: allowRegex, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Allow List Regex: 'c4.large' should return 1 instance type") -} - -func TestFilterInstanceTypes_DenyList(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - } - itf := getSelector(ec2Mock) - denyRegex, err := regexp.Compile("c4.large") - h.Ok(t, err) - filters := selector.Filters{ - DenyList: denyRegex, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 24, "Deny List Regex: 'c4.large' should return 24 instance type matching regex but returned %d", len(results)) -} - -func TestFilterInstanceTypes_AllowAndDenyList(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - } - itf := getSelector(ec2Mock) - allowRegex, err := regexp.Compile("c4.*") - h.Ok(t, err) - denyRegex, err := regexp.Compile("c4.large") - h.Ok(t, err) - filters := selector.Filters{ - AllowList: allowRegex, - DenyList: denyRegex, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 4, "Allow/Deny List Regex: 'c4.large' should return 4 instance types matching the regex but returned %d", len(results)) -} - -func TestFilterInstanceTypes_X8664_AMD64(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filters := selector.Filters{ - CPUArchitecture: aws.String("amd64"), - } - results, err := itf.FilterInstanceTypes(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with x86_64/amd64 cpu architecture") - instanceTypeName := results[0].InstanceType - h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") - h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) - -} - -func TestFilterInstanceTypes_VirtType_PV(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "pv_instances.json")) - filters := selector.Filters{ - VirtualizationType: aws.String("pv"), - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: pv") - - filters = selector.Filters{ - VirtualizationType: aws.String("paravirtual"), - } - - results, err = itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") -} - -func TestFilterInstanceTypes_PricePerHour(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - itf.EC2Pricing = &ec2PricingMock{ - GetOndemandInstanceTypeCostResp: 0.0104, - onDemandCacheCount: 1, - } - filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0104, - UpperBound: 0.0104, - }, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} - -func TestFilterInstanceTypes_PricePerHour_NoResults(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - itf.EC2Pricing = &ec2PricingMock{ - GetOndemandInstanceTypeCostResp: 0.0104, - onDemandCacheCount: 1, - } - filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0105, - UpperBound: 0.0105, - }, - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 0, "Should return 0 instance types") -} - -func TestFilterInstanceTypes_PricePerHour_OD(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - itf.EC2Pricing = &ec2PricingMock{ - GetOndemandInstanceTypeCostResp: 0.0104, - onDemandCacheCount: 1, - } - filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0104, - UpperBound: 0.0104, - }, - UsageClass: aws.String("on-demand"), - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} - -func TestFilterInstanceTypes_PricePerHour_Spot(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - itf.EC2Pricing = &ec2PricingMock{ - GetSpotInstanceTypeNDayAvgCostResp: 0.0104, - spotCacheCount: 1, - } - filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0104, - UpperBound: 0.0104, - }, - UsageClass: aws.String("spot"), - } - - results, err := itf.FilterInstanceTypes(filters) - - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} - -func TestRetrieveInstanceTypesSupportedInAZ_WithZoneName(t *testing.T) { - ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") - ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) - h.Ok(t, err) - h.Assert(t, len(results) == 228, "Should return 228 entries in us-east-2a golden file w/ no resource filters applied") -} - -func TestRetrieveInstanceTypesSupportedInAZ_WithZoneID(t *testing.T) { - ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") - ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"use2-az1"}) - h.Ok(t, err) - h.Assert(t, len(results) == 228, "Should return 228 entries in use2-az2 golden file w/ no resource filter applied") -} - -func TestRetrieveInstanceTypesSupportedInAZ_WithRegion(t *testing.T) { - ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") - ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2"}) - h.Ok(t, err) - h.Assert(t, len(results) == 228, "Should return 228 entries in us-east-2 golden file w/ no resource filter applied") -} - -func TestRetrieveInstanceTypesSupportedInAZ_WithBadZone(t *testing.T) { - ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") - ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"blah"}) - h.Assert(t, err != nil, "Should return an error since a bad zone was passed in") - h.Assert(t, results == nil, "Should return nil results due to error") -} - -func TestRetrieveInstanceTypesSupportedInAZ_Error(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypeOfferingsErr: errors.New("error"), - DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, - } - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) - h.Assert(t, err != nil, "Should return an error since ec2 api mock is configured to return an error") - h.Assert(t, results == nil, "Should return nil results due to error") -} - -func TestAggregateFilterTransform(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypes, "g2_2xlarge.json")) - g22Xlarge := "g2.2xlarge" - filters := selector.Filters{ - InstanceTypeBase: &g22Xlarge, - } - filters, err := itf.AggregateFilterTransform(filters) - h.Ok(t, err) - h.Assert(t, filters.GpusRange != nil, "g2.2Xlarge as a base instance type should filter out non-GPU instances") - h.Assert(t, *filters.BareMetal == false, "g2.2Xlarge as a base instance type should filter out bare metal instances") - h.Assert(t, *filters.Fpga == false, "g2.2Xlarge as a base instance type should filter out FPGA instances") - h.Assert(t, *filters.CPUArchitecture == "x86_64", "g2.2Xlarge as a base instance type should only return x86_64 instance types") -} - -func TestAggregateFilterTransform_InvalidInstanceType(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypes, "empty.json")) - t3Micro := "t3.microoon" - filters := selector.Filters{ - InstanceTypeBase: &t3Micro, - } - _, err := itf.AggregateFilterTransform(filters) - h.Nok(t, err) -} - -func TestRetrieveInstanceTypesSupportedInAZs_Intersection(t *testing.T) { - ec2Mock := mockMultiRespDescribeInstanceTypesOfferings(t, map[string]string{ - "us-east-2a": "us-east-2a.json", - "us-east-2b": "us-east-2b.json", - }) - ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a", "us-east-2b"}) - h.Ok(t, err) - h.Assert(t, len(results) == 3, "Should return instance types that are included in both files") - - // Check reversed zones to ensure order does not matter - results, err = itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2b", "us-east-2a"}) - h.Ok(t, err) - h.Assert(t, len(results) == 3, "Should return instance types that are included in both files when passed in reverse order") -} - -func TestRetrieveInstanceTypesSupportedInAZs_Duplicates(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2b.json").DescribeInstanceTypeOfferingsResp, - DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, - } - itf := getSelector(ec2Mock) - results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2b", "us-east-2b"}) - h.Ok(t, err) - h.Assert(t, len(results) == 3, "Should return instance types that are included in both files") -} - -func TestRetrieveInstanceTypesSupportedInAZs_GoodAndBadZone(t *testing.T) { - ec2Mock := mockedEC2{ - DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, - DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, - } - itf := getSelector(ec2Mock) - _, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-weast-2k", "us-east-2a"}) - h.Nok(t, err) -} - -func TestRetrieveInstanceTypesSupportedInAZs_DescribeAZErr(t *testing.T) { - itf := getSelector(mockedEC2{DescribeAvailabilityZonesErr: fmt.Errorf("error")}) - _, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) - h.Nok(t, err) -} - -type ec2PricingMock struct { - GetOndemandInstanceTypeCostResp float64 - GetOndemandInstanceTypeCostErr error - GetSpotInstanceTypeNDayAvgCostResp float64 - GetSpotInstanceTypeNDayAvgCostErr error - RefreshOnDemandCacheErr error - RefreshSpotCacheErr error - onDemandCacheCount int - spotCacheCount int -} - -func (p *ec2PricingMock) GetOnDemandInstanceTypeCost(instanceType string) (float64, error) { - return p.GetOndemandInstanceTypeCostResp, p.GetOndemandInstanceTypeCostErr -} - -func (p *ec2PricingMock) GetSpotInstanceTypeNDayAvgCost(instanceType string, availabilityZones []string, days int) (float64, error) { - return p.GetSpotInstanceTypeNDayAvgCostResp, p.GetSpotInstanceTypeNDayAvgCostErr -} - -func (p *ec2PricingMock) RefreshOnDemandCache() error { - return p.RefreshOnDemandCacheErr -} - -func (p *ec2PricingMock) RefreshSpotCache(days int) error { - return p.RefreshSpotCacheErr -} - -func (p *ec2PricingMock) OnDemandCacheCount() int { - return p.onDemandCacheCount -} - -func (p *ec2PricingMock) SpotCacheCount() int { - return p.spotCacheCount -} - -func (p *ec2PricingMock) Save() error { - return nil -} From 8ea3f1070f5d90fa2bb574c0f8245144c8dd654f Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 7 Jul 2022 17:54:58 -0500 Subject: [PATCH 02/12] readded examples and selector tests --- cmd/examples/example1.go | 79 ++++ pkg/selector/selector_test.go | 856 ++++++++++++++++++++++++++++++++++ 2 files changed, 935 insertions(+) create mode 100644 cmd/examples/example1.go create mode 100644 pkg/selector/selector_test.go diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go new file mode 100644 index 0000000..f30bae6 --- /dev/null +++ b/cmd/examples/example1.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" +) + +func main() { + // Load an AWS session by looking at shared credentials or environment variables + // https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("us-east-2"), + }) + if err != nil { + fmt.Printf("Oh no, AWS session credentials cannot be found: %v", err) + return + } + + // Instantiate a new instance of a selector with the AWS session + instanceSelector := selector.New(sess) + + // Instantiate an int range filter to specify min and max vcpus + vcpusRange := selector.IntRangeFilter{ + LowerBound: 2, + UpperBound: 4, + } + // Instantiate a byte quantity range filter to specify min and max memory in GiB + memoryRange := selector.ByteQuantityRangeFilter{ + LowerBound: bytequantity.FromGiB(2), + UpperBound: bytequantity.FromGiB(4), + } + // Create a string for the CPU Architecture so that it can be passed as a pointer + // when creating the Filter struct + cpuArch := "x86_64" + + // Create a Filter struct with criteria you would like to filter + // The full struct definition can be found here for all of the supported filters: + // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go + filters := selector.Filters{ + VCpusRange: &vcpusRange, + MemoryRange: &memoryRange, + CPUArchitecture: &cpuArch, + } + + // Pass the Filter struct to the FilteredInstanceTypes function of your + // selector instance to get a list of filtered instance types and their details. + instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters) + if err != nil { + fmt.Printf("Oh no, there was an error getting instance types: %v", err) + return + } + + // Pass in the list of instance type details to the SortInstanceTypes if you + // wish to sort the instances based on set filters. + sortFilter := selector.SpotPriceSortFlag + sortDirection := selector.SortAscendingFlag + instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) + if err != nil { + fmt.Printf("Oh no, there was an error filtering instance types: %v", err) + return + } + + // Truncate results and format them for output with your desired formatting function. + maxResults := 100 + instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) + if err != nil { + fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) + return + } + instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) + + // Print the returned instance types slice + fmt.Println(instanceTypes) +} diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go new file mode 100644 index 0000000..b75cede --- /dev/null +++ b/pkg/selector/selector_test.go @@ -0,0 +1,856 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector_test + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" +) + +const ( + describeInstanceTypesPages = "DescribeInstanceTypesPages" + describeInstanceTypes = "DescribeInstanceTypes" + describeInstanceTypeOfferings = "DescribeInstanceTypeOfferings" + describeAvailabilityZones = "DescribeAvailabilityZones" + mockFilesPath = "../../test/static" +) + +// Mocking helpers + +type itFn = func(page *ec2.DescribeInstanceTypesOutput, lastPage bool) bool +type ioFn = func(page *ec2.DescribeInstanceTypeOfferingsOutput, lastPage bool) bool + +type mockedEC2 struct { + ec2iface.EC2API + DescribeInstanceTypesPagesResp ec2.DescribeInstanceTypesOutput + DescribeInstanceTypesPagesErr error + DescribeInstanceTypesResp ec2.DescribeInstanceTypesOutput + DescribeInstanceTypesErr error + DescribeInstanceTypeOfferingsRespFn func(zone string) *ec2.DescribeInstanceTypeOfferingsOutput + DescribeInstanceTypeOfferingsResp ec2.DescribeInstanceTypeOfferingsOutput + DescribeInstanceTypeOfferingsErr error + DescribeAvailabilityZonesResp ec2.DescribeAvailabilityZonesOutput + DescribeAvailabilityZonesErr error +} + +func (m mockedEC2) DescribeAvailabilityZones(input *ec2.DescribeAvailabilityZonesInput) (*ec2.DescribeAvailabilityZonesOutput, error) { + return &m.DescribeAvailabilityZonesResp, m.DescribeAvailabilityZonesErr +} + +func (m mockedEC2) DescribeInstanceTypes(input *ec2.DescribeInstanceTypesInput) (*ec2.DescribeInstanceTypesOutput, error) { + return &m.DescribeInstanceTypesResp, m.DescribeInstanceTypesErr +} + +func (m mockedEC2) DescribeInstanceTypesPages(input *ec2.DescribeInstanceTypesInput, fn itFn) error { + fn(&m.DescribeInstanceTypesPagesResp, true) + return m.DescribeInstanceTypesPagesErr +} + +func (m mockedEC2) DescribeInstanceTypeOfferingsPages(input *ec2.DescribeInstanceTypeOfferingsInput, fn ioFn) error { + if m.DescribeInstanceTypeOfferingsRespFn != nil { + fn(m.DescribeInstanceTypeOfferingsRespFn(*input.Filters[0].Values[0]), true) + } else { + fn(&m.DescribeInstanceTypeOfferingsResp, true) + } + return m.DescribeInstanceTypeOfferingsErr +} + +func mockMultiRespDescribeInstanceTypesOfferings(t *testing.T, locationToFile map[string]string) mockedEC2 { + api := describeInstanceTypeOfferings + locationToResp := map[string]ec2.DescribeInstanceTypeOfferingsOutput{} + for zone, file := range locationToFile { + mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) + mockFile, err := ioutil.ReadFile(mockFilename) + h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) + ditoo := ec2.DescribeInstanceTypeOfferingsOutput{} + err = json.Unmarshal(mockFile, &ditoo) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + locationToResp[zone] = ditoo + } + return mockedEC2{ + DescribeInstanceTypeOfferingsRespFn: func(input string) *ec2.DescribeInstanceTypeOfferingsOutput { + resp := locationToResp[input] + return &resp + }, + } +} + +func setupMock(t *testing.T, api string, file string) mockedEC2 { + mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) + mockFile, err := ioutil.ReadFile(mockFilename) + h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) + switch api { + case describeInstanceTypes: + dito := ec2.DescribeInstanceTypesOutput{} + err = json.Unmarshal(mockFile, &dito) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + return mockedEC2{ + DescribeInstanceTypesResp: dito, + } + case describeInstanceTypesPages: + dito := ec2.DescribeInstanceTypesOutput{} + err = json.Unmarshal(mockFile, &dito) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + return mockedEC2{ + DescribeInstanceTypesPagesResp: dito, + } + case describeInstanceTypeOfferings: + ditoo := ec2.DescribeInstanceTypeOfferingsOutput{} + err = json.Unmarshal(mockFile, &ditoo) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + return mockedEC2{ + DescribeInstanceTypeOfferingsResp: ditoo, + } + case describeAvailabilityZones: + dazo := ec2.DescribeAvailabilityZonesOutput{} + err = json.Unmarshal(mockFile, &dazo) + h.Assert(t, err == nil, "Error parsing mock json file contents"+mockFilename) + return mockedEC2{ + DescribeAvailabilityZonesResp: dazo, + } + default: + h.Assert(t, false, "Unable to mock the provided API type "+api) + } + return mockedEC2{} +} + +func getSelector(ec2Mock mockedEC2) selector.Selector { + return selector.Selector{ + EC2: ec2Mock, + EC2Pricing: &ec2PricingMock{}, + InstanceTypesProvider: instancetypes.NewProvider("", "us-east-1", 0, ec2Mock), + } +} + +// Tests + +func TestNew(t *testing.T) { + itf := selector.New(session.Must(session.NewSession())) + h.Assert(t, itf != nil, "selector instance created without error") +} + +func TestFilterInstanceTypes(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filter := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, + } + + results, err := itf.FilterInstanceTypes(filter) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 intance type with 2 vcpus") + instanceTypeName := results[0].InstanceType + h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") + h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) +} + +func TestFilterInstanceTypes_NoResults(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 0, "Should return 0 instance type with 4 vcpus") +} + +func TestFilterInstanceTypes_AZFilteredIn(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "t3_micro.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, + } + itf := getSelector(ec2Mock) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, + AvailabilityZones: &[]string{"us-east-2a"}, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus but actually returned "+strconv.Itoa(len(results))) + instanceTypeName := results[0].InstanceType + h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") + h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) +} + +func TestFilterInstanceTypes_AZFilteredOut(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "t3_micro.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a_only_c5d12x.json").DescribeInstanceTypeOfferingsResp, + DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, + } + itf := getSelector(ec2Mock) + filters := selector.Filters{ + AvailabilityZones: &[]string{"us-east-2a"}, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 0, "Should return 0 instance types in us-east-2a but actually returned "+strconv.Itoa(len(results))) +} + +func TestFilterInstanceTypes_AZFilteredErr(t *testing.T) { + itf := getSelector(mockedEC2{}) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, + AvailabilityZones: &[]string{"blah"}, + } + + _, err := itf.FilterInstanceTypes(filters) + + h.Assert(t, err != nil, "Should error since bad zone was passed in") +} + +func TestFilterInstanceTypes_Gpus(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro_and_p3_16xl.json")) + gpuMemory, err := bytequantity.ParseToByteQuantity("128g") + h.Ok(t, err) + filters := selector.Filters{ + GpusRange: &selector.IntRangeFilter{LowerBound: 8, UpperBound: 8}, + GpuMemoryRange: &selector.ByteQuantityRangeFilter{ + LowerBound: gpuMemory, + UpperBound: gpuMemory, + }, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus but actually returned "+strconv.Itoa(len(results))) + instanceTypeName := results[0].InstanceType + h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") + h.Assert(t, *instanceTypeName == "p3.16xlarge", "Should return p3.16xlarge, got %s instead", *results[0].InstanceType) +} + +func TestFilterInstanceTypes_MoreFilters(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, + BareMetal: aws.Bool(false), + CPUArchitecture: aws.String("x86_64"), + Hypervisor: aws.String("nitro"), + EnaSupport: aws.Bool(true), + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus") + instanceTypeName := results[0].InstanceType + h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") + h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) +} + +func TestFilterInstanceTypes_Failure(t *testing.T) { + itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Assert(t, results == nil, "Results should be nil") + h.Assert(t, err != nil, "An error should be returned") +} + +func TestFilterInstanceTypes_InstanceTypeBase(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesResp: setupMock(t, describeInstanceTypes, "c4_large.json").DescribeInstanceTypesResp, + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + } + itf := getSelector(ec2Mock) + c4Large := "c4.large" + filters := selector.Filters{ + InstanceTypeBase: &c4Large, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 3, "c4.large should return 3 similar instance types") +} + +func TestFilterInstanceTypes_AllowList(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + } + itf := getSelector(ec2Mock) + allowRegex, err := regexp.Compile("c4.large") + h.Ok(t, err) + filters := selector.Filters{ + AllowList: allowRegex, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Allow List Regex: 'c4.large' should return 1 instance type") +} + +func TestFilterInstanceTypes_DenyList(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + } + itf := getSelector(ec2Mock) + denyRegex, err := regexp.Compile("c4.large") + h.Ok(t, err) + filters := selector.Filters{ + DenyList: denyRegex, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 24, "Deny List Regex: 'c4.large' should return 24 instance type matching regex but returned %d", len(results)) +} + +func TestFilterInstanceTypes_AllowAndDenyList(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp, + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + } + itf := getSelector(ec2Mock) + allowRegex, err := regexp.Compile("c4.*") + h.Ok(t, err) + denyRegex, err := regexp.Compile("c4.large") + h.Ok(t, err) + filters := selector.Filters{ + AllowList: allowRegex, + DenyList: denyRegex, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 4, "Allow/Deny List Regex: 'c4.large' should return 4 instance types matching the regex but returned %d", len(results)) +} + +func TestFilterInstanceTypes_X8664_AMD64(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{ + CPUArchitecture: aws.String("amd64"), + } + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with x86_64/amd64 cpu architecture") + instanceTypeName := results[0].InstanceType + h.Assert(t, instanceTypeName != nil, "Instance type name should not be nil") + h.Assert(t, *instanceTypeName == "t3.micro", "Should return t3.micro, got %s instead", results[0]) + +} + +func TestFilterInstanceTypes_VirtType_PV(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "pv_instances.json")) + filters := selector.Filters{ + VirtualizationType: aws.String("pv"), + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: pv") + + filters = selector.Filters{ + VirtualizationType: aws.String("paravirtual"), + } + + results, err = itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") +} + +func TestFilterInstanceTypes_PricePerHour(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + itf.EC2Pricing = &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + onDemandCacheCount: 1, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} + +func TestFilterInstanceTypes_PricePerHour_NoResults(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + itf.EC2Pricing = &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + onDemandCacheCount: 1, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0105, + UpperBound: 0.0105, + }, + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 0, "Should return 0 instance types") +} + +func TestFilterInstanceTypes_PricePerHour_OD(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + itf.EC2Pricing = &ec2PricingMock{ + GetOndemandInstanceTypeCostResp: 0.0104, + onDemandCacheCount: 1, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + UsageClass: aws.String("on-demand"), + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} + +func TestFilterInstanceTypes_PricePerHour_Spot(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + itf.EC2Pricing = &ec2PricingMock{ + GetSpotInstanceTypeNDayAvgCostResp: 0.0104, + spotCacheCount: 1, + } + filters := selector.Filters{ + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0104, + UpperBound: 0.0104, + }, + UsageClass: aws.String("spot"), + } + + results, err := itf.FilterInstanceTypes(filters) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} + +// checkSortResults is a helper function for comparing the results of sorting tests. Returns true if +// the order of instance types in the instanceTypes list matches the the order of instance type names +// in the expectedResullt list, and returns false otherwise. +func checkSortResults(instaceTypes []*instancetypes.Details, expectedResult []string) bool { + if len(instaceTypes) != len(expectedResult) { + return false + } + + for i := 0; i < len(instaceTypes); i++ { + actualName := instaceTypes[i].InstanceTypeInfo.InstanceType + expectedName := expectedResult[i] + + if actualName == nil || *actualName != expectedName { + return false + } + } + + return true +} + +func TestSortInstanceTypes_OneElement(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := selector.NameSortFlag + sortDirection := selector.SortAscendingFlag + results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should only return one element, but it returned: %d elements", len(results))) +} + +func TestSortInstanceTypes_Emptylist(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "empty.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := selector.NameSortFlag + sortDirection := selector.SortAscendingFlag + results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Ok(t, err) + h.Assert(t, len(results) == 0, fmt.Sprintf("Should return empty list, but it returned: %d elements", len(results))) +} + +func TestSortInstanceTypes_Name(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := selector.NameSortFlag + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.2xlarge", "a1.4xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = selector.SortDescendingFlag + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.large", "a1.4xlarge", "a1.2xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_Memory(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := selector.MemorySortFlag + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = selector.SortDescendingFlag + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_Vcpu(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := selector.VcpuSortFlag + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = selector.SortDescendingFlag + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_SpotPrice(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // add spot prices + prices := make(map[string]*float64) + price1, price2, price3 := 1.0, 2.0, 3.0 + prices["a1.large"] = &price1 + prices["a1.2xlarge"] = &price2 + prices["a1.4xlarge"] = &price3 + for _, v := range results { + name := v.InstanceTypeInfo.InstanceType + v.SpotPrice = prices[*name] + } + + // test ascending + sortFilter := selector.SpotPriceSortFlag + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = selector.SortDescendingFlag + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_OnDemandPrice(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // add on demand prices + prices := make(map[string]*float64) + price1, price2, price3 := 1.0, 2.0, 3.0 + prices["a1.large"] = &price1 + prices["a1.2xlarge"] = &price2 + prices["a1.4xlarge"] = &price3 + for _, v := range results { + name := v.InstanceTypeInfo.InstanceType + v.OndemandPricePerHour = prices[*name] + } + + // sort ascending + sortFilter := selector.ODPriceSortFlag + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // sort descending + sortDirection = selector.SortDescendingFlag + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_InvalidFilter(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := "blah blah blah" + sortDirection := selector.SortAscendingFlag + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedResults == nil, fmt.Sprintf("Returned instance types details should be nil, but instead got: %s", outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_InvalidDirection(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := selector.NameSortFlag + sortDirection := "fdsfds" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedResults == nil, fmt.Sprintf("Returned instance types details should be nil, but instead got: %s", outputs.OneLineOutput(sortedResults))) +} + +func TestRetrieveInstanceTypesSupportedInAZ_WithZoneName(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") + ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) + h.Ok(t, err) + h.Assert(t, len(results) == 228, "Should return 228 entries in us-east-2a golden file w/ no resource filters applied") +} + +func TestRetrieveInstanceTypesSupportedInAZ_WithZoneID(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") + ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"use2-az1"}) + h.Ok(t, err) + h.Assert(t, len(results) == 228, "Should return 228 entries in use2-az2 golden file w/ no resource filter applied") +} + +func TestRetrieveInstanceTypesSupportedInAZ_WithRegion(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") + ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2"}) + h.Ok(t, err) + h.Assert(t, len(results) == 228, "Should return 228 entries in us-east-2 golden file w/ no resource filter applied") +} + +func TestRetrieveInstanceTypesSupportedInAZ_WithBadZone(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") + ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"blah"}) + h.Assert(t, err != nil, "Should return an error since a bad zone was passed in") + h.Assert(t, results == nil, "Should return nil results due to error") +} + +func TestRetrieveInstanceTypesSupportedInAZ_Error(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypeOfferingsErr: errors.New("error"), + DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, + } + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) + h.Assert(t, err != nil, "Should return an error since ec2 api mock is configured to return an error") + h.Assert(t, results == nil, "Should return nil results due to error") +} + +func TestAggregateFilterTransform(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypes, "g2_2xlarge.json")) + g22Xlarge := "g2.2xlarge" + filters := selector.Filters{ + InstanceTypeBase: &g22Xlarge, + } + filters, err := itf.AggregateFilterTransform(filters) + h.Ok(t, err) + h.Assert(t, filters.GpusRange != nil, "g2.2Xlarge as a base instance type should filter out non-GPU instances") + h.Assert(t, *filters.BareMetal == false, "g2.2Xlarge as a base instance type should filter out bare metal instances") + h.Assert(t, *filters.Fpga == false, "g2.2Xlarge as a base instance type should filter out FPGA instances") + h.Assert(t, *filters.CPUArchitecture == "x86_64", "g2.2Xlarge as a base instance type should only return x86_64 instance types") +} + +func TestAggregateFilterTransform_InvalidInstanceType(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypes, "empty.json")) + t3Micro := "t3.microoon" + filters := selector.Filters{ + InstanceTypeBase: &t3Micro, + } + _, err := itf.AggregateFilterTransform(filters) + h.Nok(t, err) +} + +func TestRetrieveInstanceTypesSupportedInAZs_Intersection(t *testing.T) { + ec2Mock := mockMultiRespDescribeInstanceTypesOfferings(t, map[string]string{ + "us-east-2a": "us-east-2a.json", + "us-east-2b": "us-east-2b.json", + }) + ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a", "us-east-2b"}) + h.Ok(t, err) + h.Assert(t, len(results) == 3, "Should return instance types that are included in both files") + + // Check reversed zones to ensure order does not matter + results, err = itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2b", "us-east-2a"}) + h.Ok(t, err) + h.Assert(t, len(results) == 3, "Should return instance types that are included in both files when passed in reverse order") +} + +func TestRetrieveInstanceTypesSupportedInAZs_Duplicates(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2b.json").DescribeInstanceTypeOfferingsResp, + DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, + } + itf := getSelector(ec2Mock) + results, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2b", "us-east-2b"}) + h.Ok(t, err) + h.Assert(t, len(results) == 3, "Should return instance types that are included in both files") +} + +func TestRetrieveInstanceTypesSupportedInAZs_GoodAndBadZone(t *testing.T) { + ec2Mock := mockedEC2{ + DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, + DescribeAvailabilityZonesResp: setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp, + } + itf := getSelector(ec2Mock) + _, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-weast-2k", "us-east-2a"}) + h.Nok(t, err) +} + +func TestRetrieveInstanceTypesSupportedInAZs_DescribeAZErr(t *testing.T) { + itf := getSelector(mockedEC2{DescribeAvailabilityZonesErr: fmt.Errorf("error")}) + _, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"}) + h.Nok(t, err) +} + +type ec2PricingMock struct { + GetOndemandInstanceTypeCostResp float64 + GetOndemandInstanceTypeCostErr error + GetSpotInstanceTypeNDayAvgCostResp float64 + GetSpotInstanceTypeNDayAvgCostErr error + RefreshOnDemandCacheErr error + RefreshSpotCacheErr error + onDemandCacheCount int + spotCacheCount int +} + +func (p *ec2PricingMock) GetOnDemandInstanceTypeCost(instanceType string) (float64, error) { + return p.GetOndemandInstanceTypeCostResp, p.GetOndemandInstanceTypeCostErr +} + +func (p *ec2PricingMock) GetSpotInstanceTypeNDayAvgCost(instanceType string, availabilityZones []string, days int) (float64, error) { + return p.GetSpotInstanceTypeNDayAvgCostResp, p.GetSpotInstanceTypeNDayAvgCostErr +} + +func (p *ec2PricingMock) RefreshOnDemandCache() error { + return p.RefreshOnDemandCacheErr +} + +func (p *ec2PricingMock) RefreshSpotCache(days int) error { + return p.RefreshSpotCacheErr +} + +func (p *ec2PricingMock) OnDemandCacheCount() int { + return p.onDemandCacheCount +} + +func (p *ec2PricingMock) SpotCacheCount() int { + return p.spotCacheCount +} + +func (p *ec2PricingMock) Save() error { + return nil +} From ef7b7819bf9aa404a5cb68d0152f98bb76d027a4 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 7 Jul 2022 18:05:15 -0500 Subject: [PATCH 03/12] added testing files for sortin tests --- .../3_instances.json | 187 ++++++++++++++++++ .../DescribeInstanceTypesPages/empty.json | 3 + 2 files changed, 190 insertions(+) create mode 100644 test/static/DescribeInstanceTypesPages/3_instances.json create mode 100644 test/static/DescribeInstanceTypesPages/empty.json diff --git a/test/static/DescribeInstanceTypesPages/3_instances.json b/test/static/DescribeInstanceTypesPages/3_instances.json new file mode 100644 index 0000000..5753226 --- /dev/null +++ b/test/static/DescribeInstanceTypesPages/3_instances.json @@ -0,0 +1,187 @@ +{ + "InstanceTypes": [ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": [ + 8 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.4xlarge", + "MemoryInfo": { + "SizeInMiB": 32768 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 8, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 16, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 16, + "ValidCores": [ + 16 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.large", + "MemoryInfo": { + "SizeInMiB": 4096 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 10, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 3, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/static/DescribeInstanceTypesPages/empty.json b/test/static/DescribeInstanceTypesPages/empty.json new file mode 100644 index 0000000..9341a66 --- /dev/null +++ b/test/static/DescribeInstanceTypesPages/empty.json @@ -0,0 +1,3 @@ +{ + "InstanceTypes": [] +} \ No newline at end of file From 6c5ce453e3a4fea2dc35e2cdc5aaaec2f6e35077 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 7 Jul 2022 18:14:29 -0500 Subject: [PATCH 04/12] updated readme code example and help printout --- README.md | 104 ++++++++++----------------------------- cmd/examples/example1.go | 4 +- 2 files changed, 27 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 6925820..56e2173 100644 --- a/README.md +++ b/README.md @@ -241,15 +241,17 @@ Suite Flags: Global Flags: - --cache-dir string Directory to save the pricing and instance type caches (default "~/.ec2-instance-selector/") - --cache-ttl int Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches. (default 168) - -h, --help Help - --max-results int The maximum number of instance types that match your criteria to return (default 20) - -o, --output string Specify the output format (table, table-wide, one-line, simple) (default "simple") - --profile string AWS CLI profile to use for credentials and config - -r, --region string AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence) - -v, --verbose Verbose - will print out full instance specs - --version Prints CLI version + --cache-dir string Directory to save the pricing and instance type caches (default "~/.ec2-instance-selector/") + --cache-ttl int Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches. (default 168) + -h, --help Help + --max-results int The maximum number of instance types that match your criteria to return (default 20) + -o, --output string Specify the output format (table, table-wide, one-line, simple) (default "simple") + --profile string AWS CLI profile to use for credentials and config + -r, --region string AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence) + --sort-direction string Specify the direction to sort in (ascending, descending) (default "ascending") + --sort-filter string Specify the field to sort by (on-demand-price, spot-price, vcpu, memory, instance-type-name) (default "instance-type-name") + -v, --verbose Verbose - will print out full instance specs + --version Prints CLI version ``` @@ -257,71 +259,6 @@ Global Flags: This is a minimal example of using the instance selector go package directly: -**NOTE:** The example below is intended for `v3+`. For versions `v2.3.1` and earlier, refer to the following dropdown: -
- Example for v2.3.1 - - ```go -package main - -import ( - "fmt" - - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" -) - -func main() { - // Load an AWS session by looking at shared credentials or environment variables - // https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ - sess, err := session.NewSession(&aws.Config{ - Region: aws.String("us-east-2"), - }) - if err != nil { - fmt.Printf("Oh no, AWS session credentials cannot be found: %v", err) - return - } - - // Instantiate a new instance of a selector with the AWS session - instanceSelector := selector.New(sess) - - // Instantiate an int range filter to specify min and max vcpus - vcpusRange := selector.IntRangeFilter{ - LowerBound: 2, - UpperBound: 4, - } - // Instantiate a byte quantity range filter to specify min and max memory in GiB - memoryRange := selector.ByteQuantityRangeFilter{ - LowerBound: bytequantity.FromGiB(2), - UpperBound: bytequantity.FromGiB(4), - } - // Create a string for the CPU Architecture so that it can be passed as a pointer - // when creating the Filter struct - cpuArch := "x86_64" - - // Create a Filter struct with criteria you would like to filter - // The full struct definition can be found here for all of the supported filters: - // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go - filters := selector.Filters{ - VCpusRange: &vcpusRange, - MemoryRange: &memoryRange, - CPUArchitecture: &cpuArch, - } - - // Pass the Filter struct to the Filter function of your selector instance - instanceTypesSlice, err := instanceSelector.Filter(filters) - if err != nil { - fmt.Printf("Oh no, there was an error :( %v", err) - return - } - // Print the returned instance types slice - fmt.Println(instanceTypesSlice) -} - ``` -
- **cmd/examples/example1.go** ```go#cmd/examples/example1.go package main @@ -374,16 +311,25 @@ func main() { } // Pass the Filter struct to the FilteredInstanceTypes function of your - // selector instance to get a list of filtered instance types and their details + // selector instance to get a list of filtered instance types and their details. instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters) if err != nil { fmt.Printf("Oh no, there was an error getting instance types: %v", err) return } - // Pass in your list of instance type details to the appropriate output function - // in order to format the instance types as printable strings. - maxResults := 100 + // Pass in the list of instance type details to the SortInstanceTypes if you + // wish to sort the instances based on set filters. + sortFilter := selector.NameSortFlag + sortDirection := selector.SortAscendingFlag + instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) + if err != nil { + fmt.Printf("Oh no, there was an error filtering instance types: %v", err) + return + } + + // Truncate results and format them for output with your desired formatting function. + maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) @@ -403,7 +349,7 @@ func main() { $ git clone https://github.com/aws/amazon-ec2-instance-selector.git $ cd amazon-ec2-instance-selector/ $ go run cmd/examples/example1.go -[c4.large c5.large c5a.large c5ad.large c5d.large c6i.large t2.medium t3.medium t3.small t3a.medium t3a.small] +[c4.large c5.large c5a.large c5ad.large c5d.large c6a.large c6i.large c6id.large t2.medium t3.medium] ``` ## Building diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index f30bae6..4bbffca 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -57,7 +57,7 @@ func main() { // Pass in the list of instance type details to the SortInstanceTypes if you // wish to sort the instances based on set filters. - sortFilter := selector.SpotPriceSortFlag + sortFilter := selector.NameSortFlag sortDirection := selector.SortAscendingFlag instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) if err != nil { @@ -66,7 +66,7 @@ func main() { } // Truncate results and format them for output with your desired formatting function. - maxResults := 100 + maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) From 5035033c735529c024681d802a28bb854567e7a6 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 7 Jul 2022 18:19:18 -0500 Subject: [PATCH 05/12] fixed typo in TruncateResults test --- pkg/selector/outputs/outputs_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index d30beb1..2ce71ef 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -146,7 +146,7 @@ func TestTruncateResults(t *testing.T) { h.Assert(t, numTrucated == 0, fmt.Sprintf("Should truncate 0 results, but actually truncated: %d results", numTrucated)) } -func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { +func TestTruncateResults_NegativeMaxResults(t *testing.T) { instanceTypes := getInstanceTypes(t, "25_instances.json") maxResults := aws.Int(-1) @@ -157,7 +157,7 @@ func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but %d results were truncated", numTrucated)) } -func TestFormatInstanceTypes_NilMaxResults(t *testing.T) { +func TestTrucateResults_NilMaxResults(t *testing.T) { instanceTypes := getInstanceTypes(t, "25_instances.json") var maxResults *int = nil From 27a434d5e8cad32480b849655a57feb717e93c46 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Fri, 8 Jul 2022 14:02:30 -0500 Subject: [PATCH 06/12] made sorting flags private --- README.md | 8 ++++--- cmd/examples/example1.go | 8 ++++--- cmd/main.go | 29 ++++++++++++++++-------- pkg/selector/selector.go | 34 ++++++++++++++-------------- pkg/selector/selector_test.go | 42 +++++++++++++++++------------------ 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 56e2173..2f18f0b 100644 --- a/README.md +++ b/README.md @@ -320,8 +320,8 @@ func main() { // Pass in the list of instance type details to the SortInstanceTypes if you // wish to sort the instances based on set filters. - sortFilter := selector.NameSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "instance-type-name" + sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) if err != nil { fmt.Printf("Oh no, there was an error filtering instance types: %v", err) @@ -329,10 +329,12 @@ func main() { } // Truncate results and format them for output with your desired formatting function. + // All formatting functions can be found here: + // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { - fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) + fmt.Printf("Oh no, there was an error truncating instance types: %v", err) return } instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index 4bbffca..a671db1 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -57,8 +57,8 @@ func main() { // Pass in the list of instance type details to the SortInstanceTypes if you // wish to sort the instances based on set filters. - sortFilter := selector.NameSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "instance-type-name" + sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) if err != nil { fmt.Printf("Oh no, there was an error filtering instance types: %v", err) @@ -66,10 +66,12 @@ func main() { } // Truncate results and format them for output with your desired formatting function. + // All formatting functions can be found here: + // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { - fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) + fmt.Printf("Oh no, there was an error truncating instance types: %v", err) return } instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) diff --git a/cmd/main.go b/cmd/main.go index 8fa586c..6ac0672 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -119,6 +119,17 @@ const ( oneLineOutput = "one-line" simpleOutput = "simple" verboseOutput = "verbose" + + // Sorting constants + + sortODPrice = "on-demand-price" + sortSpotPrice = "spot-price" + sortVcpu = "vcpu" + sortMemory = "memory" + sortName = "instance-type-name" + + sortAscending = "ascending" + sortDescending = "descending" ) var ( @@ -150,16 +161,16 @@ Full docs can be found at github.com/aws/amazon-` + binName } cliSortCriteria := []string{ - selector.ODPriceSortFlag, - selector.SpotPriceSortFlag, - selector.VcpuSortFlag, - selector.MemorySortFlag, - selector.NameSortFlag, + sortODPrice, + sortSpotPrice, + sortVcpu, + sortMemory, + sortName, } cliSortDirections := []string{ - selector.SortAscendingFlag, - selector.SortDescendingFlag, + sortAscending, + sortDescending, } // Registers flags with specific input types from the cli pkg @@ -226,8 +237,8 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") - cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(selector.SortAscendingFlag), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) - cli.ConfigStringOptionsFlag(sortFilter, nil, cli.StringMe(selector.NameSortFlag), fmt.Sprintf("Specify the field to sort by (%s)", strings.Join(cliSortCriteria, ", ")), cliSortCriteria) + cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) + cli.ConfigStringOptionsFlag(sortFilter, nil, cli.StringMe(sortName), fmt.Sprintf("Specify the field to sort by (%s)", strings.Join(cliSortCriteria, ", ")), cliSortCriteria) // Parses the user input with the registered flags and runs type specific validation on the user input flags, err := cli.ParseAndValidateFlags() diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 92bf4e5..33f654a 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -100,16 +100,16 @@ const ( pricePerHour = "pricePerHour" - // Sorting constants + // Sorting flags - ODPriceSortFlag = "on-demand-price" - SpotPriceSortFlag = "spot-price" - VcpuSortFlag = "vcpu" - MemorySortFlag = "memory" - NameSortFlag = "instance-type-name" + sortODPrice = "on-demand-price" + sortSpotPrice = "spot-price" + sortVcpu = "vcpu" + sortMemory = "memory" + sortName = "instance-type-name" - SortAscendingFlag = "ascending" - SortDescendingFlag = "descending" + sortAscending = "ascending" + sortDescending = "descending" ) // New creates an instance of Selector provided an aws session @@ -227,9 +227,9 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so var isDescending bool if sortDirectionFlag != nil { switch *sortDirectionFlag { - case SortDescendingFlag: + case sortDescending: isDescending = true - case SortAscendingFlag: + case sortAscending: isDescending = false default: return nil, fmt.Errorf("invalid sort direction flag: %s", *sortDirectionFlag) @@ -241,11 +241,11 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so // if sorting based on either on demand or spot price, ensure the appropriate cache // has been refreshed. if sortFilterFlag != nil { - if *sortFilterFlag == ODPriceSortFlag { + if *sortFilterFlag == sortODPrice { if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) } - } else if *sortFilterFlag == SpotPriceSortFlag { + } else if *sortFilterFlag == sortSpotPrice { if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) } @@ -262,7 +262,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so // Handle nil values by making non nil values always less than the nil values. That way the // nil values can be bubbled up to the end of the list. switch *sortFilterFlag { - case ODPriceSortFlag: + case sortODPrice: if firstType.OndemandPricePerHour == nil { return false } else if secondType.OndemandPricePerHour == nil { @@ -274,7 +274,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } else { return *firstType.OndemandPricePerHour <= *secondType.OndemandPricePerHour } - case SpotPriceSortFlag: + case sortSpotPrice: if firstType.SpotPrice == nil { return false } else if secondType.SpotPrice == nil { @@ -286,7 +286,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } else { return *firstType.SpotPrice <= *secondType.SpotPrice } - case VcpuSortFlag: + case sortVcpu: if firstType.VCpuInfo == nil || firstType.VCpuInfo.DefaultVCpus == nil { return false } else if secondType.VCpuInfo == nil || secondType.VCpuInfo.DefaultVCpus == nil { @@ -298,7 +298,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } else { return *firstType.VCpuInfo.DefaultVCpus <= *secondType.VCpuInfo.DefaultVCpus } - case MemorySortFlag: + case sortMemory: if firstType.MemoryInfo == nil || firstType.MemoryInfo.SizeInMiB == nil { return false } else if secondType.MemoryInfo == nil || secondType.MemoryInfo.SizeInMiB == nil { @@ -310,7 +310,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } else { return *firstType.MemoryInfo.SizeInMiB <= *secondType.MemoryInfo.SizeInMiB } - case NameSortFlag: + case sortName: if firstType.InstanceType == nil { return false } else if secondType.InstanceType == nil { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index b75cede..b99c2ac 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -499,8 +499,8 @@ func TestSortInstanceTypes_OneElement(t *testing.T) { results, err := itf.FilterInstanceTypes(filters) h.Ok(t, err) - sortFilter := selector.NameSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "instance-type-name" + sortDirection := "ascending" results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) h.Ok(t, err) @@ -513,8 +513,8 @@ func TestSortInstanceTypes_Emptylist(t *testing.T) { results, err := itf.FilterInstanceTypes(filters) h.Ok(t, err) - sortFilter := selector.NameSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "instance-type-name" + sortDirection := "ascending" results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) h.Ok(t, err) @@ -528,8 +528,8 @@ func TestSortInstanceTypes_Name(t *testing.T) { h.Ok(t, err) // test ascending - sortFilter := selector.NameSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "instance-type-name" + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults := []string{"a1.2xlarge", "a1.4xlarge", "a1.large"} @@ -538,7 +538,7 @@ func TestSortInstanceTypes_Name(t *testing.T) { h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) // test descending - sortDirection = selector.SortDescendingFlag + sortDirection = "descending" sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults = []string{"a1.large", "a1.4xlarge", "a1.2xlarge"} @@ -554,8 +554,8 @@ func TestSortInstanceTypes_Memory(t *testing.T) { h.Ok(t, err) // test ascending - sortFilter := selector.MemorySortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "memory" + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} @@ -564,7 +564,7 @@ func TestSortInstanceTypes_Memory(t *testing.T) { h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) // test descending - sortDirection = selector.SortDescendingFlag + sortDirection = "descending" sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} @@ -580,8 +580,8 @@ func TestSortInstanceTypes_Vcpu(t *testing.T) { h.Ok(t, err) // test ascending - sortFilter := selector.VcpuSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "vcpu" + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} @@ -590,7 +590,7 @@ func TestSortInstanceTypes_Vcpu(t *testing.T) { h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) // test descending - sortDirection = selector.SortDescendingFlag + sortDirection = "descending" sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} @@ -617,8 +617,8 @@ func TestSortInstanceTypes_SpotPrice(t *testing.T) { } // test ascending - sortFilter := selector.SpotPriceSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "spot-price" + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} @@ -627,7 +627,7 @@ func TestSortInstanceTypes_SpotPrice(t *testing.T) { h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) // test descending - sortDirection = selector.SortDescendingFlag + sortDirection = "descending" sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} @@ -654,8 +654,8 @@ func TestSortInstanceTypes_OnDemandPrice(t *testing.T) { } // sort ascending - sortFilter := selector.ODPriceSortFlag - sortDirection := selector.SortAscendingFlag + sortFilter := "on-demand-price" + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} @@ -664,7 +664,7 @@ func TestSortInstanceTypes_OnDemandPrice(t *testing.T) { h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) // sort descending - sortDirection = selector.SortDescendingFlag + sortDirection = "descending" sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} @@ -680,7 +680,7 @@ func TestSortInstanceTypes_InvalidFilter(t *testing.T) { h.Ok(t, err) sortFilter := "blah blah blah" - sortDirection := selector.SortAscendingFlag + sortDirection := "ascending" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) h.Assert(t, err != nil, "An error should be returned") @@ -693,7 +693,7 @@ func TestSortInstanceTypes_InvalidDirection(t *testing.T) { results, err := itf.FilterInstanceTypes(filters) h.Ok(t, err) - sortFilter := selector.NameSortFlag + sortFilter := "instance-type-name" sortDirection := "fdsfds" sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) From f0fcb1b0e1b0d969c07de601f189d0b701b36444 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Fri, 8 Jul 2022 14:29:42 -0500 Subject: [PATCH 07/12] fixed typos in comments --- README.md | 4 ++-- cmd/examples/example1.go | 4 ++-- pkg/selector/selector.go | 28 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2f18f0b..159d5e1 100644 --- a/README.md +++ b/README.md @@ -318,8 +318,8 @@ func main() { return } - // Pass in the list of instance type details to the SortInstanceTypes if you - // wish to sort the instances based on set filters. + // Pass in the list of instance type details to the SortInstanceTypes function + // if you wish to sort the instances based on set filters. sortFilter := "instance-type-name" sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index a671db1..17229ad 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -55,8 +55,8 @@ func main() { return } - // Pass in the list of instance type details to the SortInstanceTypes if you - // wish to sort the instances based on set filters. + // Pass in the list of instance type details to the SortInstanceTypes function + // if you wish to sort the instances based on set filters. sortFilter := "instance-type-name" sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 33f654a..e7a4fcf 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -215,37 +215,37 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai return filteredInstanceTypes, nil } -// SortInstanceTypes acepts a list of instance type details, a sort filter flag, and a sort direction flag and -// returns a sorted list of instance type details sorted based on the sort filter and sort direction. -// Accepted sort filter flags: "on-demand-price", "spot-price", "vcpu", "memory" , "instance-type-name". -// Accepted sort direction flags: "ascending", "descending". -func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, sortFilterFlag *string, sortDirectionFlag *string) ([]*instancetypes.Details, error) { +// SortInstanceTypes accepts a list of instance type details, a sort filter flag, and a sort direction flag and +// returns a list of instance type details sorted based on the sort filter and sort direction. +// Sort filter options: "on-demand-price", "spot-price", "vcpu", "memory" , "instance-type-name". +// Sort direction options: "ascending", "descending". +func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, sortFilter *string, sortDirection *string) ([]*instancetypes.Details, error) { if len(instanceTypes) <= 1 { return instanceTypes, nil } var isDescending bool - if sortDirectionFlag != nil { - switch *sortDirectionFlag { + if sortDirection != nil { + switch *sortDirection { case sortDescending: isDescending = true case sortAscending: isDescending = false default: - return nil, fmt.Errorf("invalid sort direction flag: %s", *sortDirectionFlag) + return nil, fmt.Errorf("invalid sort direction: %s", *sortDirection) } } else { - return nil, fmt.Errorf("sort direction flag is nil") + return nil, fmt.Errorf("sort direction is nil") } // if sorting based on either on demand or spot price, ensure the appropriate cache // has been refreshed. - if sortFilterFlag != nil { - if *sortFilterFlag == sortODPrice { + if sortFilter != nil { + if *sortFilter == sortODPrice { if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) } - } else if *sortFilterFlag == sortSpotPrice { + } else if *sortFilter == sortSpotPrice { if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) } @@ -261,7 +261,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so // Determine which value to sort by. // Handle nil values by making non nil values always less than the nil values. That way the // nil values can be bubbled up to the end of the list. - switch *sortFilterFlag { + switch *sortFilter { case sortODPrice: if firstType.OndemandPricePerHour == nil { return false @@ -330,7 +330,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so }) if !isSortingFlagValid { - return nil, fmt.Errorf("invalid sort filter flag: %s", *sortFilterFlag) + return nil, fmt.Errorf("invalid sort filter: %s", *sortFilter) } return instanceTypes, nil From 36bbfb31029c540b98d4624bca8188fb2144d682 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Fri, 8 Jul 2022 14:33:41 -0500 Subject: [PATCH 08/12] fixed selector_test method typo --- pkg/selector/selector_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index b99c2ac..b926bd8 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -475,7 +475,7 @@ func TestFilterInstanceTypes_PricePerHour_Spot(t *testing.T) { // checkSortResults is a helper function for comparing the results of sorting tests. Returns true if // the order of instance types in the instanceTypes list matches the the order of instance type names -// in the expectedResullt list, and returns false otherwise. +// in the expectedResult list, and returns false otherwise. func checkSortResults(instaceTypes []*instancetypes.Details, expectedResult []string) bool { if len(instaceTypes) != len(expectedResult) { return false From 60fdce717860e1dd0c1ca6ab23d0552c9eff1d2b Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Thu, 14 Jul 2022 13:00:48 -0700 Subject: [PATCH 09/12] fixed overall typos and fixed structure of SortInstanceTypes --- README.md | 4 +- cmd/examples/example1.go | 4 +- cmd/main.go | 5 ++- pkg/selector/selector.go | 71 ++++++++++++++++++++--------------- pkg/selector/selector_test.go | 8 ++-- 5 files changed, 54 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 159d5e1..e011b19 100644 --- a/README.md +++ b/README.md @@ -324,13 +324,15 @@ func main() { sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) if err != nil { - fmt.Printf("Oh no, there was an error filtering instance types: %v", err) + fmt.Printf("Oh no, there was an error sorting instance types: %v", err) return } // Truncate results and format them for output with your desired formatting function. // All formatting functions can be found here: // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go + // Examples of formatted outputs can be found here: + // https://github.com/aws/amazon-ec2-instance-selector#examples maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index 17229ad..c9a7fb0 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -61,13 +61,15 @@ func main() { sortDirection := "ascending" instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) if err != nil { - fmt.Printf("Oh no, there was an error filtering instance types: %v", err) + fmt.Printf("Oh no, there was an error sorting instance types: %v", err) return } // Truncate results and format them for output with your desired formatting function. // All formatting functions can be found here: // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go + // Examples of formatted outputs can be found here: + // https://github.com/aws/amazon-ec2-instance-selector#examples maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { diff --git a/cmd/main.go b/cmd/main.go index 6ac0672..9b2316c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -124,7 +124,7 @@ const ( sortODPrice = "on-demand-price" sortSpotPrice = "spot-price" - sortVcpu = "vcpu" + sortVCPU = "vcpu" sortMemory = "memory" sortName = "instance-type-name" @@ -163,7 +163,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cliSortCriteria := []string{ sortODPrice, sortSpotPrice, - sortVcpu, + sortVCPU, sortMemory, sortName, } @@ -369,6 +369,7 @@ Full docs can be found at github.com/aws/amazon-` + binName instanceTypeDetails, err = instanceSelector.SortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) if err != nil { fmt.Printf("An error occurred when sorting instance types: %v", err) + os.Exit(1) } // format instance types as strings diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index e7a4fcf..5cf2ec9 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -104,7 +104,7 @@ const ( sortODPrice = "on-demand-price" sortSpotPrice = "spot-price" - sortVcpu = "vcpu" + sortVCPU = "vcpu" sortMemory = "memory" sortName = "instance-type-name" @@ -215,45 +215,61 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai return filteredInstanceTypes, nil } +// isSortFilterValid determines whether the given sortFilter matches any +// of the given valid flags +func isSortFilterValid(sortFilter string, validFlags ...string) bool { + for _, flag := range validFlags { + if sortFilter == flag { + return true + } + } + + return false +} + // SortInstanceTypes accepts a list of instance type details, a sort filter flag, and a sort direction flag and // returns a list of instance type details sorted based on the sort filter and sort direction. -// Sort filter options: "on-demand-price", "spot-price", "vcpu", "memory" , "instance-type-name". +// Sort filter options: "on-demand-price", "spot-price", "vcpu", "memory", "instance-type-name". // Sort direction options: "ascending", "descending". func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, sortFilter *string, sortDirection *string) ([]*instancetypes.Details, error) { if len(instanceTypes) <= 1 { return instanceTypes, nil } - var isDescending bool - if sortDirection != nil { - switch *sortDirection { - case sortDescending: - isDescending = true - case sortAscending: - isDescending = false - default: - return nil, fmt.Errorf("invalid sort direction: %s", *sortDirection) - } - } else { + // validate direction flag and determine sorting direction + if sortDirection == nil { return nil, fmt.Errorf("sort direction is nil") } + var isDescending bool + switch *sortDirection { + case sortDescending: + isDescending = true + case sortAscending: + isDescending = false + default: + return nil, fmt.Errorf("invalid sort direction: %s", *sortDirection) + } - // if sorting based on either on demand or spot price, ensure the appropriate cache + // validate filter flag + if sortFilter == nil { + return nil, fmt.Errorf("sort filter is nil") + } else if !isSortFilterValid(*sortFilter, sortODPrice, sortSpotPrice, sortVCPU, sortMemory, sortName) { + return nil, fmt.Errorf("invalid sort filter: %s", *sortFilter) + } + + // if sorting based on either on-demand or spot price, ensure the appropriate cache // has been refreshed. - if sortFilter != nil { - if *sortFilter == sortODPrice { - if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { - return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) - } - } else if *sortFilter == sortSpotPrice { - if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { - return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) - } + if *sortFilter == sortODPrice { + if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) + } + } else if *sortFilter == sortSpotPrice { + if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) } } // sort instance types based on filter flag and isDescending - isSortingFlagValid := true sort.Slice(instanceTypes, func(i, j int) bool { firstType := instanceTypes[i] secondType := instanceTypes[j] @@ -286,7 +302,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } else { return *firstType.SpotPrice <= *secondType.SpotPrice } - case sortVcpu: + case sortVCPU: if firstType.VCpuInfo == nil || firstType.VCpuInfo.DefaultVCpus == nil { return false } else if secondType.VCpuInfo == nil || secondType.VCpuInfo.DefaultVCpus == nil { @@ -324,15 +340,10 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } default: // invalid sorting flag. Do not sort. - isSortingFlagValid = false return true } }) - if !isSortingFlagValid { - return nil, fmt.Errorf("invalid sort filter: %s", *sortFilter) - } - return instanceTypes, nil } diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index b926bd8..3db1f40 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -476,13 +476,13 @@ func TestFilterInstanceTypes_PricePerHour_Spot(t *testing.T) { // checkSortResults is a helper function for comparing the results of sorting tests. Returns true if // the order of instance types in the instanceTypes list matches the the order of instance type names // in the expectedResult list, and returns false otherwise. -func checkSortResults(instaceTypes []*instancetypes.Details, expectedResult []string) bool { - if len(instaceTypes) != len(expectedResult) { +func checkSortResults(instanceTypes []*instancetypes.Details, expectedResult []string) bool { + if len(instanceTypes) != len(expectedResult) { return false } - for i := 0; i < len(instaceTypes); i++ { - actualName := instaceTypes[i].InstanceTypeInfo.InstanceType + for i := 0; i < len(instanceTypes); i++ { + actualName := instanceTypes[i].InstanceTypeInfo.InstanceType expectedName := expectedResult[i] if actualName == nil || *actualName != expectedName { From 2871abf2b9e8c392e703708ee9d4abcaed0c08aa Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Fri, 15 Jul 2022 10:44:16 -0700 Subject: [PATCH 10/12] refactored sorting filter validation --- pkg/selector/selector.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 5cf2ec9..2e69f16 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -215,16 +215,28 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai return filteredInstanceTypes, nil } -// isSortFilterValid determines whether the given sortFilter matches any -// of the given valid flags -func isSortFilterValid(sortFilter string, validFlags ...string) bool { +// validateFilter determines whether the given sortFilter matches +// one of the valid filter flags. +func validateFilter(sortFilter *string) error { + if sortFilter == nil { + return fmt.Errorf("sort filter is nil") + } + + validFlags := []string{ + sortODPrice, + sortSpotPrice, + sortVCPU, + sortMemory, + sortName, + } + for _, flag := range validFlags { - if sortFilter == flag { - return true + if *sortFilter == flag { + return nil } } - return false + return fmt.Errorf("invalid sort filter: %s. (valid options: %s)", *sortFilter, strings.Join(validFlags, ", ")) } // SortInstanceTypes accepts a list of instance type details, a sort filter flag, and a sort direction flag and @@ -247,14 +259,12 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so case sortAscending: isDescending = false default: - return nil, fmt.Errorf("invalid sort direction: %s", *sortDirection) + return nil, fmt.Errorf("invalid sort direction: %s (valid options: %s, %s)", *sortDirection, sortAscending, sortDescending) } // validate filter flag - if sortFilter == nil { - return nil, fmt.Errorf("sort filter is nil") - } else if !isSortFilterValid(*sortFilter, sortODPrice, sortSpotPrice, sortVCPU, sortMemory, sortName) { - return nil, fmt.Errorf("invalid sort filter: %s", *sortFilter) + if err := validateFilter(sortFilter); err != nil { + return nil, err } // if sorting based on either on-demand or spot price, ensure the appropriate cache From 244789b789551b9da50f0119311f5f1bca4cd583 Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Fri, 15 Jul 2022 10:47:39 -0700 Subject: [PATCH 11/12] renamed validateFilter into validateSortFilter --- pkg/selector/selector.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 2e69f16..1ba3aec 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -215,9 +215,9 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai return filteredInstanceTypes, nil } -// validateFilter determines whether the given sortFilter matches +// validateSortFilter determines whether the given sortFilter matches // one of the valid filter flags. -func validateFilter(sortFilter *string) error { +func validateSortFilter(sortFilter *string) error { if sortFilter == nil { return fmt.Errorf("sort filter is nil") } @@ -263,7 +263,7 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so } // validate filter flag - if err := validateFilter(sortFilter); err != nil { + if err := validateSortFilter(sortFilter); err != nil { return nil, err } From bdf0a3e7c60ab75e6ee3c3a5ffa175c03d10bc5b Mon Sep 17 00:00:00 2001 From: Rodrigo Okamoto Date: Wed, 20 Jul 2022 14:05:56 -0500 Subject: [PATCH 12/12] functioning json path sorting solution --- cmd/examples/example1.go | 49 +++-- cmd/main.go | 96 ++++++++-- go.mod | 1 + go.sum | 2 + pkg/cli/sorter/sorter.go | 260 +++++++++++++++++++++++++++ pkg/instancetypes/instancetypes.go | 2 +- pkg/selector/outputs/outputs.go | 4 +- pkg/selector/outputs/outputs_test.go | 2 +- pkg/selector/selector.go | 10 +- pkg/selector/selector_test.go | 2 +- 10 files changed, 393 insertions(+), 35 deletions(-) create mode 100644 pkg/cli/sorter/sorter.go diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index c9a7fb0..abcfec2 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "fmt" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli/sorter" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" "github.com/aws/aws-sdk-go/aws" @@ -38,6 +40,11 @@ func main() { // when creating the Filter struct cpuArch := "x86_64" + priceRange := selector.Float64RangeFilter{ + LowerBound: 0, + UpperBound: 100, + } + // Create a Filter struct with criteria you would like to filter // The full struct definition can be found here for all of the supported filters: // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go @@ -45,6 +52,7 @@ func main() { VCpusRange: &vcpusRange, MemoryRange: &memoryRange, CPUArchitecture: &cpuArch, + PricePerHour: &priceRange, } // Pass the Filter struct to the FilteredInstanceTypes function of your @@ -55,16 +63,6 @@ func main() { return } - // Pass in the list of instance type details to the SortInstanceTypes function - // if you wish to sort the instances based on set filters. - sortFilter := "instance-type-name" - sortDirection := "ascending" - instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) - if err != nil { - fmt.Printf("Oh no, there was an error sorting instance types: %v", err) - return - } - // Truncate results and format them for output with your desired formatting function. // All formatting functions can be found here: // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go @@ -76,8 +74,37 @@ func main() { fmt.Printf("Oh no, there was an error truncating instance types: %v", err) return } - instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) + + instanceTypesSlice[3].OnDemandPricePerHour = nil + instanceTypesSlice[8].OnDemandPricePerHour = nil + + // Pass in the list of instance type details to the SortInstanceTypes function + // if you wish to sort the instances based on set filters. + sortFilter := "on-demand-price" + sortDirection := "descending" + sortedTypes, err := instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) + if err != nil { + fmt.Printf("Oh no, there was an error sorting instance types: %v", err) + return + } + instanceTypes := outputs.TableOutputWide(sortedTypes) + + jsonTypeIndent, err := json.MarshalIndent(instanceTypesSlice[0], "", " ") + fmt.Println(string(jsonTypeIndent)) + fmt.Println() // Print the returned instance types slice fmt.Println(instanceTypes) + + // TODO: remove this. This is for testing purposes + fmt.Println() + + // jsonType, err := json.Marshal(instanceTypesSlice[0]) + // fmt.Println(string(jsonType) + "\n") + + sorter, err := sorter.NewSorter(instanceTypesSlice, "$.OndemandPricePerHour", sortDirection) + sorter.Sort() + types := sorter.InstanceTypes() + str := outputs.TableOutputWide(types) + fmt.Printf(str[0]) } diff --git a/cmd/main.go b/cmd/main.go index 9b2316c..c7d7c27 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "time" commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli/sorter" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/env" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" @@ -110,7 +111,7 @@ const ( cacheTTL = "cache-ttl" cacheDir = "cache-dir" sortDirection = "sort-direction" - sortFilter = "sort-filter" + sortBy = "sort-by" // Output constants @@ -122,6 +123,7 @@ const ( // Sorting constants + // TODO: remove sortODPrice = "on-demand-price" sortSpotPrice = "spot-price" sortVCPU = "vcpu" @@ -129,7 +131,9 @@ const ( sortName = "instance-type-name" sortAscending = "ascending" + sortAsc = "asc" sortDescending = "descending" + sortDesc = "desc" ) var ( @@ -160,17 +164,20 @@ Full docs can be found at github.com/aws/amazon-` + binName simpleOutput, } - cliSortCriteria := []string{ - sortODPrice, - sortSpotPrice, - sortVCPU, - sortMemory, - sortName, - } + // TODO: remove + // cliSortCriteria := []string{ + // sortODPrice, + // sortSpotPrice, + // sortVCPU, + // sortMemory, + // sortName, + // } cliSortDirections := []string{ sortAscending, + sortAsc, sortDescending, + sortDesc, } // Registers flags with specific input types from the cli pkg @@ -238,7 +245,9 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) - cli.ConfigStringOptionsFlag(sortFilter, nil, cli.StringMe(sortName), fmt.Sprintf("Specify the field to sort by (%s)", strings.Join(cliSortCriteria, ", ")), cliSortCriteria) + // TODO: modify this to accept all strings and change help comment to include text about json path and defaults + // TODO: modify description to include json path and defaults + cli.ConfigStringFlag(sortBy, nil, cli.StringMe(sortName), "Specify the field to sort by", nil) // Parses the user input with the registered flags and runs type specific validation on the user input flags, err := cli.ParseAndValidateFlags() @@ -321,14 +330,28 @@ Full docs can be found at github.com/aws/amazon-` + binName DedicatedHosts: cli.BoolMe(flags[dedicatedHosts]), } - // If output type is `table-wide`, cache both prices for better comparison in output, - // even if the actual filter is applied on any one of those based on usage class - // Save time by hydrating all caches in parallel + sortFilterFlag := cli.StringMe(flags[sortBy]) + lowercaseField := strings.ToLower(*sortFilterFlag) outputFlag := cli.StringMe(flags[output]) if outputFlag != nil && *outputFlag == tableWideOutput { + // If output type is `table-wide`, cache both prices for better comparison in output, + // even if the actual filter is applied on any one of those based on usage class + // Save time by hydrating all caches in parallel if err := hydrateCaches(*instanceSelector); err != nil { log.Printf("%v", err) } + } else if strings.Contains(lowercaseField, "price") { + // if sorting based on either on-demand or spot price, ensure the appropriate cache + // has been refreshed. + if strings.Contains(lowercaseField, "ondemand") { + if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { + log.Printf("there was a problem refreshing the on-demand pricing cache: %v", err) + } + } else if strings.Contains(lowercaseField, "spot") { + if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + log.Printf("there was a problem refreshing the spot pricing cache: %v", err) + } + } } if flags[verbose] != nil { @@ -364,9 +387,9 @@ Full docs can be found at github.com/aws/amazon-` + binName } // sort instance types - sortFilterFlag := cli.StringMe(flags[sortFilter]) sortDirectionFlag := cli.StringMe(flags[sortDirection]) - instanceTypeDetails, err = instanceSelector.SortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) + instanceTypeDetails, err = sortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) + //instanceTypeDetails, err = instanceSelector.SortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) if err != nil { fmt.Printf("An error occurred when sorting instance types: %v", err) os.Exit(1) @@ -542,3 +565,48 @@ func hydrateCaches(instanceSelector selector.Selector) (errs error) { wg.Wait() return errs } + +func sortInstanceTypes(instanceTypes []*instancetypes.Details, sortField *string, sortDirection *string) ([]*instancetypes.Details, error) { + if sortField == nil { + return nil, fmt.Errorf("sortField is nil") + } else if sortDirection == nil { + return nil, fmt.Errorf("sortDirection is nil") + } + + // TODO: can check for default constants here (perhaps a switch statement with default being the json path) + // Extra: + // - If want to do constants in the way that Brandon recommended (for all quantity fields) then we can parse through + // - fields before flags are created so that we can determine the default keys for quantity fields + // - wait this wouldn't work because we don't have the instance types before the CLI flags are collected... + // - OH WAIT we can just directly look at the fields of the struct perhaps? and then just allow + // quantities to be default flags and then use the type variable name as the name of the field? + // - Maybe we can do this through reflect.Value? + // - or maybe we can make a dummy instance type to look at as a json value? + // Helpful link: https://pkg.go.dev/reflect#Value + + /* + maybe get default quantative values this way? + + v := reflect.ValueOf(x) + + values := make([]interface{}, v.NumField()) + + for i := 0; i < v.NumField(); i++ { + values[i] = v.Field(i).Interface() + } + + + // However, still need some way to get field name so that we can use that as a valid key for users + + // perhaps can have a map that maps field names (keys) to json paths so that we can pass it into + // the sorter + */ + + // TODO: check for actual json paths here + sorter, err := sorter.NewSorter(instanceTypes, *sortField, *sortDirection) + if err != nil { + return nil, err + } + sorter.Sort() + return sorter.InstanceTypes(), nil +} diff --git a/go.mod b/go.mod index 388553c..398c6d0 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect go.uber.org/atomic v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 53c0f70..7e6a59d 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/pkg/cli/sorter/sorter.go b/pkg/cli/sorter/sorter.go new file mode 100644 index 0000000..804a6c5 --- /dev/null +++ b/pkg/cli/sorter/sorter.go @@ -0,0 +1,260 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package sorter + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" + "github.com/oliveagle/jsonpath" +) + +const ( + sortAscending = "ascending" + sortAsc = "asc" + sortDescending = "descending" + sortDesc = "desc" +) + +// sorterNode represents a sortable instance type which holds the value +// to sort by instance sort +type sorterNode struct { + instanceType *instancetypes.Details + fieldValue reflect.Value +} + +// Sorter is used to sort instance types based on a sorting field +// and direction +type Sorter struct { + sorters []*sorterNode + sortField string + isDescending bool +} + +// NewSorter creates a new Sorter object to be used to sort the given instance types +// based on the sorting field and direction +// TODO: explain how sorting field is a JSON path to the appropriate Details struct property +// TODO: explain valid sort directions +// TODO: maybe make the strings pointers instead so that the CLI can pass the flags +// in directly (also can have nil checks in here and return appropriate errors) +// TODO: maybe instead of "sortField" call it something to do with "JSON path"/"path" +func NewSorter(instanceTypes []*instancetypes.Details, sortField string, sortDirection string) (*Sorter, error) { + // TODO: determine if sortField is valid. Maybe do this in newSorterNode because + // the json path library we are using already validates the sortField + + // validate direction flag and determine sorting direction + // if sortDirection == nil { + // return nil, fmt.Errorf("sort direction is nil") + // } + var isDescending bool + switch sortDirection { + case sortDescending, sortDesc: + isDescending = true + case sortAscending, sortAsc: + isDescending = false + default: + return nil, fmt.Errorf("invalid sort direction: %s (valid options: %s, %s, %s, %s)", sortDirection, sortAscending, sortAsc, sortDescending, sortDesc) + } + + // Create sorterNode objects for each instance type + sorters := []*sorterNode{} + for _, instanceType := range instanceTypes { + newSorter, err := newSorterNode(instanceType, sortField) + if err != nil { + return nil, fmt.Errorf("error creating sorting node: %v", err) + } + + sorters = append(sorters, newSorter) + } + + return &Sorter{ + sorters: sorters, + sortField: sortField, + isDescending: isDescending, + }, nil +} + +// newSorterNode creates a new sorterNode object which represents the given instance type +// and can be used in sorting of instance types based on the given sortField +func newSorterNode(instanceType *instancetypes.Details, sortField string) (*sorterNode, error) { + // TODO: figure out if there is a better way to get correct format than to + // marshal and then unmarshal instance types + + // convert instance type into json + jsonInstanceType, err := json.Marshal(instanceType) + if err != nil { + return nil, err + } + + // unmarshal json instance types in order to get proper format + // for json path parsing + var jsonData interface{} + err = json.Unmarshal(jsonInstanceType, &jsonData) + if err != nil { + return nil, err + } + + // get the desired field from the json data based on the passed in + // json path + result, err := jsonpath.JsonPathLookup(jsonData, sortField) + if err != nil { + return nil, err + } + + // TODO: + // Bug where one value might be a "float" but then another might be nil + // and we need some way to compare those two. Can't just set the value + // of float to nil and can't make nil be a pointer because then it + // will try to compare float with a nil + + // maybe just do a "if valj != reflect.float" when vali is, then treat + // case where valj is nil + // This does not work......... + + // maybe turn everything into pointers? + + // Maybe if we knew the type of the field we could use that to determine + // which of the types is correct? AKA if we have a type that is a float + // switch on that type instead of the type of valI and then in + // the case we can have an if where if the val does not equal the type, + // then it is not less (AKA it bubbles up) + + // if result == nil { + // var ptr *interface{} = nil + // result = ptr + // } + + return &sorterNode{ + instanceType: instanceType, + fieldValue: reflect.ValueOf(result), + }, nil +} + +// Sort the instance types in the Sorter based on the Sorter's sort field and +// direction +func (s *Sorter) Sort() { + sort.Sort(s) +} + +func (s *Sorter) Len() int { + return len(s.sorters) +} + +func (s *Sorter) Swap(i, j int) { + // originalI := s.sorters[i] + + // s.sorters[i] = s.sorters[j] + // s.sorters[j] = originalI + + s.sorters[i], s.sorters[j] = s.sorters[j], s.sorters[i] +} + +func (s *Sorter) Less(i, j int) bool { + valI := s.sorters[i].fieldValue + valJ := s.sorters[j].fieldValue + + fmt.Println("CALLING IS LESS") + less, _ := isLess(valI, valJ, s.isDescending) + + return less +} + +// isLess determines whether the first value (valI) is less than the +// second value (valJ) or not +func isLess(valI, valJ reflect.Value, isDescending bool) (bool, error) { + // TODO: add more types + switch valI.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + fmt.Println("=====found int type!======") + + if isDescending { + return valI.Int() > valJ.Int(), nil + } else { + return valI.Int() <= valJ.Int(), nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + fmt.Println("=====found uint type!======") + + if isDescending { + return valI.Uint() > valJ.Uint(), nil + } else { + return valI.Uint() <= valJ.Uint(), nil + } + case reflect.Float32, reflect.Float64: + fmt.Printf("=====found float type!====== values: %f, %f\n") + //fmt.Printf(" Result: %b\n", (valI.Float() < valJ.Float())) + + if valJ.Kind() != reflect.Float32 && valJ.Kind() != reflect.Float64 { + fmt.Println("valJ is not a float") + return true, nil + } + + if isDescending { + return valI.Float() > valJ.Float(), nil + } else { + return valI.Float() <= valJ.Float(), nil + } + case reflect.String: + fmt.Println("=====found string type!======") + + if isDescending { + return strings.Compare(valI.String(), valJ.String()) > 0, nil + } else { + return strings.Compare(valI.String(), valJ.String()) <= 0, nil + } + case reflect.Pointer: + fmt.Println("=====found ptr type!======") + + // Handle nil values by making non nil values always less than the nil values. That way the + // nil values can be bubbled up to the end of the list. + if valI.IsNil() { + return false, nil + } else if valJ.Kind() == reflect.Pointer && valJ.IsNil() { + return true, nil + } + + return isLess(valI.Elem(), valJ.Elem(), isDescending) + case reflect.Invalid: + // handle invalid values (like nil values) by making valid values + // always less than the invalid values. That way the invalid values + // always bubble up to the end of the list + return false, nil + default: + fmt.Printf("====unsortable type!==== %v\n", valI.Kind()) + + // TODO: log that an unsortable type has been passed + + // handle unsortable values (like nil values) by making sortable values + // always less than the unsortable values. That way the unsortable values + // always bubble up to the end of the list + return false, nil + } + + // TODO: add bool types +} + +// InstanceTypes returns the list of instance types held in the Sorter +func (s *Sorter) InstanceTypes() []*instancetypes.Details { + instanceTypes := []*instancetypes.Details{} + + for _, node := range s.sorters { + instanceTypes = append(instanceTypes, node.instanceType) + } + + return instanceTypes +} diff --git a/pkg/instancetypes/instancetypes.go b/pkg/instancetypes/instancetypes.go index 4e83a84..dad0e24 100644 --- a/pkg/instancetypes/instancetypes.go +++ b/pkg/instancetypes/instancetypes.go @@ -36,7 +36,7 @@ var ( // Details hold all the information on an ec2 instance type type Details struct { ec2.InstanceTypeInfo - OndemandPricePerHour *float64 + OnDemandPricePerHour *float64 SpotPrice *float64 } diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index 61d5828..a46defe 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -164,8 +164,8 @@ func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string { onDemandPricePerHourStr := "-Not Fetched-" spotPricePerHourStr := "-Not Fetched-" - if instanceTypeInfo.OndemandPricePerHour != nil { - onDemandPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.OndemandPricePerHour)) + if instanceTypeInfo.OnDemandPricePerHour != nil { + onDemandPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.OnDemandPricePerHour)) } if instanceTypeInfo.SpotPrice != nil { spotPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.SpotPrice)) diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index 2ce71ef..c62b611 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -42,7 +42,7 @@ func getInstanceTypes(t *testing.T, file string) []*instancetypes.Details { instanceTypesDetails := []*instancetypes.Details{} for _, it := range dito.InstanceTypes { odPrice := float64(0.53) - instanceTypesDetails = append(instanceTypesDetails, &instancetypes.Details{InstanceTypeInfo: *it, OndemandPricePerHour: &odPrice}) + instanceTypesDetails = append(instanceTypesDetails, &instancetypes.Details{InstanceTypeInfo: *it, OnDemandPricePerHour: &odPrice}) } return instanceTypesDetails } diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 1ba3aec..4052521 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -289,16 +289,16 @@ func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, so // nil values can be bubbled up to the end of the list. switch *sortFilter { case sortODPrice: - if firstType.OndemandPricePerHour == nil { + if firstType.OnDemandPricePerHour == nil { return false - } else if secondType.OndemandPricePerHour == nil { + } else if secondType.OnDemandPricePerHour == nil { return true } if isDescending { - return *firstType.OndemandPricePerHour > *secondType.OndemandPricePerHour + return *firstType.OnDemandPricePerHour > *secondType.OnDemandPricePerHour } else { - return *firstType.OndemandPricePerHour <= *secondType.OndemandPricePerHour + return *firstType.OnDemandPricePerHour <= *secondType.OnDemandPricePerHour } case sortSpotPrice: if firstType.SpotPrice == nil { @@ -386,7 +386,7 @@ func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetype log.Printf("Could not retrieve instantaneous hourly on-demand price for instance type %s\n", instanceTypeName) } else { instanceTypeHourlyPriceOnDemand = &price - instanceTypeInfo.OndemandPricePerHour = instanceTypeHourlyPriceOnDemand + instanceTypeInfo.OnDemandPricePerHour = instanceTypeHourlyPriceOnDemand } } if itf.EC2Pricing.SpotCacheCount() > 0 && contains(instanceTypeInfo.SupportedUsageClasses, "spot") { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index 3db1f40..4d618e9 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -650,7 +650,7 @@ func TestSortInstanceTypes_OnDemandPrice(t *testing.T) { prices["a1.4xlarge"] = &price3 for _, v := range results { name := v.InstanceTypeInfo.InstanceType - v.OndemandPricePerHour = prices[*name] + v.OnDemandPricePerHour = prices[*name] } // sort ascending