From 8ac398b1947db984e47c02c6a0ec6daf161755b1 Mon Sep 17 00:00:00 2001 From: "Rodrigo O.C. Pereira" <68402662+digocorbellini@users.noreply.github.com> Date: Mon, 27 Jun 2022 12:36:17 -0500 Subject: [PATCH 1/7] Deprecated filter methods (#133) * fixed issue with OD pricing for european regions * made string replacement more readable in getRegionForPricingAPI * implemented sorting of instance types * fixed typo in filtering error message * moved sort to selector.go and refactored FilterVerbose tests * brought back FilterWithOutput() in selector.go * working implementation of refactoring solution * Deprecated the filter methods in selector.go * added the names of replacement functions to deprecated comments Co-authored-by: Rodrigo Okamoto --- pkg/selector/selector.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index ebb10a0..4088d2d 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -132,6 +132,9 @@ func (itf Selector) Save() error { // Filter accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a simple list of instance type strings +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and +// OutputInstanceTypes() in the next major version. func (itf Selector) Filter(filters Filters) ([]string, error) { outputFn := InstanceTypesOutputFn(outputs.SimpleInstanceTypeOutput) output, _, err := itf.FilterWithOutput(filters, outputFn) @@ -140,6 +143,9 @@ func (itf Selector) Filter(filters Filters) ([]string, error) { // FilterVerbose accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a list instanceTypeInfo +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() in the next +// major version. func (itf Selector) FilterVerbose(filters Filters) ([]*instancetypes.Details, error) { instanceTypeInfoSlice, err := itf.rawFilter(filters) if err != nil { @@ -151,6 +157,9 @@ func (itf Selector) FilterVerbose(filters Filters) ([]*instancetypes.Details, er // FilterWithOutput accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a list of strings based on the custom outputFn +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and +// OutputInstanceTypes() in the next major version. func (itf Selector) FilterWithOutput(filters Filters, outputFn InstanceTypesOutput) ([]string, int, error) { instanceTypeInfoSlice, err := itf.rawFilter(filters) if err != nil { From fd78895fe02ac889bb5fa69c0a384f634efb7cb2 Mon Sep 17 00:00:00 2001 From: "Rodrigo O.C. Pereira" <68402662+digocorbellini@users.noreply.github.com> Date: Wed, 6 Jul 2022 15:37:31 -0500 Subject: [PATCH 2/7] Refactoring of Filter functions (#134) * fixed issue with OD pricing for european regions * made string replacement more readable in getRegionForPricingAPI * implemented sorting of instance types * fixed typo in filtering error message * moved sort to selector.go and refactored FilterVerbose tests * brought back FilterWithOutput() in selector.go * working implementation of refactoring solution * added method comments * converted Filter and FilterVerbose tests to TestGetFilteredOutput tests * added filtering and output tests * split filter functions into fetching filtered types and outputing types * removed OutputInstanceTypes. Output functions only in Outputs.go * fixed typos and updated function comments * fixed invalid maxResults check in main * added error to TruncateResults for negative values * updated error message for formatting instance types * reduced the amount of times pricing caches are refreshed * fixed cache refresh related comments Co-authored-by: Rodrigo Okamoto --- cmd/examples/example1.go | 21 +- cmd/main.go | 197 ++++++------ pkg/selector/outputs/outputs.go | 22 +- pkg/selector/outputs/outputs_test.go | 48 +++ pkg/selector/selector.go | 133 +++----- pkg/selector/selector_test.go | 450 +++++++++++++-------------- pkg/selector/types.go | 3 - 7 files changed, 468 insertions(+), 406 deletions(-) diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index 31b44b0..ef0200b 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -46,12 +47,24 @@ func main() { CPUArchitecture: &cpuArch, } - // Pass the Filter struct to the Filter function of your selector instance - instanceTypesSlice, err := instanceSelector.Filter(filters) + // 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 :( %v", err) + 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(instanceTypesSlice) + fmt.Println(instanceTypes) } diff --git a/cmd/main.go b/cmd/main.go index f57e56d..7feca88 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,7 @@ import ( commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli" "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" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" "github.com/aws/aws-sdk-go/aws/session" @@ -41,10 +42,6 @@ const ( defaultProfile = "default" awsConfigFile = "~/.aws/config" spotPricingDaysBack = 30 - - tableOutput = "table" - tableWideOutput = "table-wide" - oneLine = "one-line" ) // Filter Flag Constants @@ -112,6 +109,14 @@ const ( output = "output" cacheTTL = "cache-ttl" cacheDir = "cache-dir" + + // Output constants + + tableOutput = "table" + tableWideOutput = "table-wide" + oneLineOutput = "one-line" + simpleOutput = "simple" + verboseOutput = "verbose" ) var ( @@ -138,9 +143,8 @@ Full docs can be found at github.com/aws/amazon-` + binName cliOutputTypes := []string{ tableOutput, tableWideOutput, - oneLine, + oneLineOutput, } - resultsOutputFn := outputs.SimpleInstanceTypeOutput // Registers flags with specific input types from the cli pkg // Filter Flags - These will be grouped at the top of the help flags @@ -200,7 +204,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigIntFlag(maxResults, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_MAX_RESULTS", 20), "The maximum number of instance types that match your criteria to return") cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil) cli.ConfigStringFlag(region, cli.StringMe("r"), nil, "AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence)", nil) - cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil) + cli.ConfigStringFlag(output, cli.StringMe("o"), cli.StringMe(simpleOutput), fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil) cli.ConfigIntFlag(cacheTTL, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_CACHE_TTL", 168), "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.") cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches") cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") @@ -237,30 +241,6 @@ Full docs can be found at github.com/aws/amazon-` + binName } } registerShutdown(shutdown) - outputFlag := cli.StringMe(flags[output]) - if outputFlag != nil && *outputFlag == tableWideOutput { - // If output type is `table-wide`, simply print both prices for better comparison, - // 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 flags[pricePerHour] != nil { - // Else, if price filters are applied, only hydrate the respective cache as we don't have to print the prices - if flags[usageClass] == nil || *cli.StringMe(flags[usageClass]) == "on-demand" { - if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { - log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err) - } - } - } else { - if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { - log.Printf("There was a problem refreshing the spot pricing cache: %v", err) - } - } - } - } filters := selector.Filters{ VCpusRange: cli.IntRangeMe(flags[vcpus]), @@ -288,7 +268,6 @@ Full docs can be found at github.com/aws/amazon-` + binName Region: cli.StringMe(flags[region]), AvailabilityZones: cli.StringSliceMe(flags[availabilityZones]), CurrentGeneration: cli.BoolMe(flags[currentGeneration]), - MaxResults: cli.IntMe(flags[maxResults]), NetworkInterfaces: cli.IntRangeMe(flags[networkInterfaces]), NetworkPerformance: cli.IntRangeMe(flags[networkPerformance]), NetworkEncryption: cli.BoolMe(flags[networkEncryption]), @@ -313,8 +292,18 @@ 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 + outputFlag := cli.StringMe(flags[output]) + if outputFlag != nil && *outputFlag == tableWideOutput { + if err := hydrateCaches(*instanceSelector); err != nil { + log.Printf("%v", err) + } + } + if flags[verbose] != nil { - resultsOutputFn = outputs.VerboseInstanceTypeOutput + outputFlag = cli.StringMe(verboseOutput) transformedFilters, err := instanceSelector.AggregateFilterTransform(filters) if err != nil { fmt.Printf("An error occurred while transforming the aggregate filters") @@ -338,18 +327,26 @@ Full docs can be found at github.com/aws/amazon-` + binName } } - outputFn := getOutputFn(outputFlag, selector.InstanceTypesOutputFn(resultsOutputFn)) - - instanceTypes, itemsTruncated, err := instanceSelector.FilterWithOutput(filters, outputFn) + // get filtered instance types + instanceTypeDetails, err := instanceSelector.FilterInstanceTypes(filters) if err != nil { fmt.Printf("An error occurred when filtering instance types: %v", err) os.Exit(1) } + + // format instance types as strings + maxOutputResults := cli.IntMe(flags[maxResults]) + instanceTypes, itemsTruncated, err := formatInstanceTypes(instanceTypeDetails, maxOutputResults, outputFlag) + if err != nil { + fmt.Printf("An error occured formatting instance types: %v", err) + os.Exit(1) + } if len(instanceTypes) == 0 { log.Println("The criteria was too narrow and returned no valid instance types. Consider broadening your criteria so that more instance types are returned.") os.Exit(1) } + // print output for _, instanceType := range instanceTypes { fmt.Println(instanceType) } @@ -360,60 +357,6 @@ Full docs can be found at github.com/aws/amazon-` + binName shutdown() } -func hydrateCaches(instanceSelector selector.Selector) (errs error) { - wg := &sync.WaitGroup{} - hydrateTasks := []func(*sync.WaitGroup) error{ - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err)) - } - } - return nil - }, - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err)) - } - } - return nil - }, - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.InstanceTypesProvider.CacheCount() == 0 { - if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err)) - } - } - return nil - }, - } - wg.Add(len(hydrateTasks)) - for _, task := range hydrateTasks { - go task(wg) - } - wg.Wait() - return errs -} - -func getOutputFn(outputFlag *string, currentFn selector.InstanceTypesOutputFn) selector.InstanceTypesOutputFn { - outputFn := selector.InstanceTypesOutputFn(currentFn) - if outputFlag != nil { - switch *outputFlag { - case tableWideOutput: - return selector.InstanceTypesOutputFn(outputs.TableOutputWide) - case tableOutput: - return selector.InstanceTypesOutputFn(outputs.TableOutputShort) - case oneLine: - return selector.InstanceTypesOutputFn(outputs.OneLineOutput) - } - } - return outputFn -} - func getRegionAndProfileAWSSession(regionName *string, profileName *string) (*session.Session, error) { sessOpts := session.Options{SharedConfigState: session.SharedConfigEnable} if regionName != nil { @@ -487,3 +430,77 @@ func registerShutdown(shutdown func()) { shutdown() }() } + +// formatInstanceTypes accepts a list of instance types details, a number of max results, and an output flag +// and returns a list of formatted strings representing the passed in intance types with at most maxResults number +// of results. The format of the strings is determined by the output flag. The number of truncated results +// is also returned. +// Accepted output flags: "table", "table-wide", "one-line", "simple", "verbose". +func formatInstanceTypes(instanceTypes []*instancetypes.Details, maxResults *int, outputFlag *string) ([]string, int, error) { + if outputFlag == nil { + return nil, 0, fmt.Errorf("output flag is nil") + } + + instanceTypes, numOfItemsTruncated, err := outputs.TruncateResults(maxResults, instanceTypes) + if err != nil { + return nil, 0, err + } + + // See which output format to use + var outputString []string + switch *outputFlag { + case simpleOutput: + outputString = outputs.SimpleInstanceTypeOutput(instanceTypes) + case oneLineOutput: + outputString = outputs.OneLineOutput(instanceTypes) + case tableOutput: + outputString = outputs.TableOutputShort(instanceTypes) + case tableWideOutput: + outputString = outputs.TableOutputWide(instanceTypes) + case verboseOutput: + outputString = outputs.VerboseInstanceTypeOutput(instanceTypes) + default: + return nil, 0, fmt.Errorf("invalid output flag") + } + + return outputString, numOfItemsTruncated, nil +} + +func hydrateCaches(instanceSelector selector.Selector) (errs error) { + wg := &sync.WaitGroup{} + hydrateTasks := []func(*sync.WaitGroup) error{ + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err)) + } + } + return nil + }, + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err)) + } + } + return nil + }, + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.InstanceTypesProvider.CacheCount() == 0 { + if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err)) + } + } + return nil + }, + } + wg.Add(len(hydrateTasks)) + for _, task := range hydrateTasks { + go task(wg) + } + wg.Wait() + return errs +} diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index 318cf8e..61d5828 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -26,6 +26,24 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" ) +// TruncateResults is used to prepare a list of details for output by truncating the number of results +// in the list to have at most maxResults elements. Returns the truncated list of instance types and +// the number of truncated items. +func TruncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int, error) { + if maxResults == nil { + return instanceTypeInfoSlice, 0, nil + } else if *maxResults < 0 { + return nil, 0, fmt.Errorf("negative max results value") + } + + upperIndex := *maxResults + if *maxResults > len(instanceTypeInfoSlice) { + upperIndex = len(instanceTypeInfoSlice) + } + + return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex, nil +} + // SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { instanceTypeStrings := []string{} @@ -35,7 +53,7 @@ func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) [] return instanceTypeStrings } -// VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names +// VerboseInstanceTypeOutput is an OutputFn which returns a list of full instance specs func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { output, err := json.MarshalIndent(instanceTypeInfoSlice, "", " ") if err != nil { @@ -174,7 +192,7 @@ func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string { return []string{buf.String()} } -// OneLineOutput is an output function which prints the instance type names on a single line separated by commas +// OneLineOutput is an output function which returns the instance type names on a single line separated by commas func OneLineOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { instanceTypeNames := []string{} for _, instanceType := range instanceTypeInfoSlice { diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index 6f484fd..d30beb1 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -23,6 +23,7 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "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/service/ec2" ) @@ -119,3 +120,50 @@ func TestOneLineOutput(t *testing.T) { instanceTypeOut = outputs.OneLineOutput(nil) h.Assert(t, len(instanceTypeOut) == 0, "Should return 0 instance types when passed nil") } + +func TestTruncateResults(t *testing.T) { + instanceTypes := getInstanceTypes(t, "25_instances.json") + + // test 0 for max results + maxResults := aws.Int(0) + truncatedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) + h.Ok(t, err) + h.Assert(t, len(truncatedResult) == 0, fmt.Sprintf("Should return 0 instance types since max results is set to %d, but only %d are returned in total", *maxResults, len(truncatedResult))) + h.Assert(t, numTrucated == 25, fmt.Sprintf("Should truncate 25 results, but actually truncated: %d results", numTrucated)) + + // test 1 for max results + maxResults = aws.Int(1) + truncatedResult, numTrucated, err = outputs.TruncateResults(maxResults, instanceTypes) + h.Ok(t, err) + h.Assert(t, len(truncatedResult) == 1, fmt.Sprintf("Should return 1 instance type since max results is set to %d, but only %d are returned in total", *maxResults, len(truncatedResult))) + h.Assert(t, numTrucated == 24, fmt.Sprintf("Should truncate 24 results, but actually truncated: %d results", numTrucated)) + + // test 30 for max results + maxResults = aws.Int(30) + truncatedResult, numTrucated, err = outputs.TruncateResults(maxResults, instanceTypes) + h.Ok(t, err) + h.Assert(t, len(truncatedResult) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to %d but only %d are returned in total", *maxResults, len(truncatedResult))) + h.Assert(t, numTrucated == 0, fmt.Sprintf("Should truncate 0 results, but actually truncated: %d results", numTrucated)) +} + +func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { + instanceTypes := getInstanceTypes(t, "25_instances.json") + + maxResults := aws.Int(-1) + formattedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, formattedResult == nil, fmt.Sprintf("returned list should be nil, but it is actually: %s", outputs.OneLineOutput(formattedResult))) + h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but %d results were truncated", numTrucated)) +} + +func TestFormatInstanceTypes_NilMaxResults(t *testing.T) { + instanceTypes := getInstanceTypes(t, "25_instances.json") + + var maxResults *int = nil + formattedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) + + h.Ok(t, err) + h.Assert(t, len(formattedResult) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to nil but only %d are returned in total", len(formattedResult))) + h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but actually truncated: %d results", numTrucated)) +} diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 4088d2d..f20d8f2 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -27,7 +27,6 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" - "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/request" "github.com/aws/aws-sdk-go/aws/session" @@ -46,6 +45,7 @@ const ( zoneNameLocationType = "availability-zone" regionNameLocationType = "region" sdkName = "instance-selector" + spotPricingDaysBack = 30 // Filter Keys @@ -130,77 +130,28 @@ func (itf Selector) Save() error { return multierr.Append(itf.EC2Pricing.Save(), itf.InstanceTypesProvider.Save()) } -// Filter accepts a Filters struct which is used to select the available instance types -// matching the criteria within Filters and returns a simple list of instance type strings -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and -// OutputInstanceTypes() in the next major version. -func (itf Selector) Filter(filters Filters) ([]string, error) { - outputFn := InstanceTypesOutputFn(outputs.SimpleInstanceTypeOutput) - output, _, err := itf.FilterWithOutput(filters, outputFn) - return output, err -} - -// FilterVerbose accepts a Filters struct which is used to select the available instance types -// matching the criteria within Filters and returns a list instanceTypeInfo -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() in the next -// major version. -func (itf Selector) FilterVerbose(filters Filters) ([]*instancetypes.Details, error) { - instanceTypeInfoSlice, err := itf.rawFilter(filters) - if err != nil { - return nil, err - } - instanceTypeInfoSlice, _ = itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) - return instanceTypeInfoSlice, nil -} - -// FilterWithOutput accepts a Filters struct which is used to select the available instance types -// matching the criteria within Filters and returns a list of strings based on the custom outputFn -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and -// OutputInstanceTypes() in the next major version. -func (itf Selector) FilterWithOutput(filters Filters, outputFn InstanceTypesOutput) ([]string, int, error) { - instanceTypeInfoSlice, err := itf.rawFilter(filters) - if err != nil { - return nil, 0, err - } - instanceTypeInfoSlice, numOfItemsTruncated := itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) - output := outputFn.Output(instanceTypeInfoSlice) - return output, numOfItemsTruncated, nil -} - -func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int) { - if maxResults == nil { - return instanceTypeInfoSlice, 0 - } - upperIndex := *maxResults - if *maxResults > len(instanceTypeInfoSlice) { - upperIndex = len(instanceTypeInfoSlice) - } - return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex -} - -// AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. -func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { - transforms := []FiltersTransform{ - TransformFn(itf.TransformBaseInstanceType), - TransformFn(itf.TransformFlexible), - TransformFn(itf.TransformForService), - } - var err error - for _, transform := range transforms { - filters, err = transform.Transform(filters) - if err != nil { - return filters, err +// FilterInstanceTypes accepts a Filters struct which is used to select the available instance types +// matching the criteria within Filters and returns the detailed specs of matching instance types +func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Details, error) { + // refresh OD or Spot pricing caches if pricing filters are used depending on + // which usage class is selected (default usage class is on demand) + if filters.PricePerHour != nil { + // If price filters are applied, only hydrate the respective cache as we don't have to print the prices + if filters.UsageClass == nil || *filters.UsageClass == "on-demand" { + if itf.EC2Pricing.OnDemandCacheCount() == 0 { + 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 itf.EC2Pricing.SpotCacheCount() == 0 { + if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) + } + } } } - return filters, nil -} -// rawFilter accepts a Filters struct which is used to select the available instance types -// matching the criteria within Filters and returns the detailed specs of matching instance types -func (itf Selector) rawFilter(filters Filters) ([]*instancetypes.Details, error) { filters, err := itf.AggregateFilterTransform(filters) if err != nil { return nil, err @@ -249,9 +200,40 @@ func (itf Selector) rawFilter(filters Filters) ([]*instancetypes.Details, error) for it := range instanceTypes { filteredInstanceTypes = append(filteredInstanceTypes, it) } + return sortInstanceTypeInfo(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 + } + 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 + }) + return instanceTypeInfoSlice +} + +// AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. +func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { + transforms := []FiltersTransform{ + TransformFn(itf.TransformBaseInstanceType), + TransformFn(itf.TransformFlexible), + TransformFn(itf.TransformForService), + } + var err error + for _, transform := range transforms { + filters, err = transform.Transform(filters) + if err != nil { + return filters, err + } + } + return filters, nil +} + func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetypes.Details, availabilityZones []string, locationInstanceOfferings map[string]string) (*instancetypes.Details, error) { instanceTypeName := *instanceTypeInfo.InstanceType isFpga := instanceTypeInfo.FpgaInfo != nil @@ -351,19 +333,6 @@ func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetype return &instanceTypeInfo, nil } -// sortInstanceTypeInfo will sort based on instance type info alpha-numerically -func sortInstanceTypeInfo(instanceTypeInfoSlice []*instancetypes.Details) []*instancetypes.Details { - if len(instanceTypeInfoSlice) < 2 { - return instanceTypeInfoSlice - } - 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 - }) - return instanceTypeInfoSlice -} - // executeFilters accepts a mapping of filter name to filter pairs which are iterated through // to determine if the instance type matches the filter values. func (itf Selector) executeFilters(filterToInstanceSpecMapping map[string]filterPair, instanceType string) (bool, error) { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index a270938..d77f3a8 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -154,38 +154,34 @@ func TestNew(t *testing.T) { h.Assert(t, itf != nil, "selector instance created without error") } -func TestFilterVerbose(t *testing.T) { +func TestFilterInstanceTypes(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filters := selector.Filters{ + filter := selector.Filters{ VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, } - results, err := itf.FilterVerbose(filters) + + results, err := itf.FilterInstanceTypes(filter) + 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))) - h.Assert(t, *results[0].InstanceType == "t3.micro", "Should return t3.micro, got %s instead", results[0].InstanceType) + 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 TestFilterVerbose_NoResults(t *testing.T) { +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.FilterVerbose(filters) + + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) h.Assert(t, len(results) == 0, "Should return 0 instance type with 4 vcpus") } -func TestFilterVerbose_Failure(t *testing.T) { - itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) - filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, - } - results, err := itf.FilterVerbose(filters) - h.Assert(t, results == nil, "Results should be nil") - h.Assert(t, err != nil, "An error should be returned") -} - -func TestFilterVerbose_AZFilteredIn(t *testing.T) { +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, @@ -196,13 +192,17 @@ func TestFilterVerbose_AZFilteredIn(t *testing.T) { VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, AvailabilityZones: &[]string{"us-east-2a"}, } - results, err := itf.FilterVerbose(filters) + + 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))) - h.Assert(t, *results[0].InstanceType == "t3.micro", "Should return t3.micro, got %s instead", results[0].InstanceType) + 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 TestFilterVerbose_AZFilteredOut(t *testing.T) { +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, @@ -212,22 +212,26 @@ func TestFilterVerbose_AZFilteredOut(t *testing.T) { filters := selector.Filters{ AvailabilityZones: &[]string{"us-east-2a"}, } - results, err := itf.FilterVerbose(filters) + + 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 TestFilterVerboseAZ_FilteredErr(t *testing.T) { +func TestFilterInstanceTypes_AZFilteredErr(t *testing.T) { itf := getSelector(mockedEC2{}) filters := selector.Filters{ VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, AvailabilityZones: &[]string{"blah"}, } - _, err := itf.FilterVerbose(filters) + + _, err := itf.FilterInstanceTypes(filters) + h.Assert(t, err != nil, "Should error since bad zone was passed in") } -func TestFilterVerbose_Gpus(t *testing.T) { +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) @@ -238,72 +242,233 @@ func TestFilterVerbose_Gpus(t *testing.T) { UpperBound: gpuMemory, }, } - results, err := itf.FilterVerbose(filters) + + 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))) - h.Assert(t, *results[0].InstanceType == "p3.16xlarge", "Should return p3.16xlarge, got %s instead", *results[0].InstanceType) + 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 TestFilter(t *testing.T) { +func TestFilterInstanceTypes_MoreFilters(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, + 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.Filter(filters) + + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus") - h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) + 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 TestFilter_MoreFilters(t *testing.T) { +func TestFilterInstanceTypes_X8664_AMD64(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), + CPUArchitecture: aws.String("amd64"), } - results, err := itf.Filter(filters) + results, err := itf.FilterInstanceTypes(filters) h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus") - h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) + 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 TestFilter_TruncateToMaxResults(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "25_instances.json")) +func TestFilterInstanceTypes_VirtType_PV(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "pv_instances.json")) filters := selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, + VirtualizationType: aws.String("pv"), } - results, err := itf.Filter(filters) + + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) - h.Assert(t, len(results) > 1, "Should return > 1 instance types since max results is not set") + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: pv") filters = selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, - MaxResults: aws.Int(1), + VirtualizationType: aws.String("paravirtual"), } - results, err = itf.Filter(filters) + + results, err = itf.FilterInstanceTypes(filters) + h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should return 1 instance types since max results is set") + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") +} - filters = selector.Filters{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, - MaxResults: aws.Int(30), +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.Filter(filters) + + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) - h.Assert(t, len(results) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to 30 but only %d are returned in total", len(results))) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) } -func TestFilter_Failure(t *testing.T) { - itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) +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{ - VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, + PricePerHour: &selector.Float64RangeFilter{ + LowerBound: 0.0105, + UpperBound: 0.0105, + }, } - results, err := itf.Filter(filters) - h.Assert(t, results == nil, "Results should be nil") - h.Assert(t, err != nil, "An error should be returned") + + 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) { @@ -377,22 +542,6 @@ func TestAggregateFilterTransform_InvalidInstanceType(t *testing.T) { h.Nok(t, err) } -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 3, "c4.large should return 3 similar instance types") -} - func TestRetrieveInstanceTypesSupportedInAZs_Intersection(t *testing.T) { ec2Mock := mockMultiRespDescribeInstanceTypesOfferings(t, map[string]string{ "us-east-2a": "us-east-2a.json", @@ -437,85 +586,6 @@ func TestRetrieveInstanceTypesSupportedInAZs_DescribeAZErr(t *testing.T) { h.Nok(t, err) } -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Allow List Regex: 'c4.large' should return 1 instance type") -} - -func TestFilter_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.Filter(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 TestFilter_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.Filter(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 TestFilter_X8664_AMD64(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filters := selector.Filters{ - CPUArchitecture: aws.String("amd64"), - } - results, err := itf.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, "Should only return 1 instance type with x86_64/amd64 cpu architecture") - h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) -} - -func TestFilter_VirtType_PV(t *testing.T) { - itf := getSelector(setupMock(t, describeInstanceTypesPages, "pv_instances.json")) - filters := selector.Filters{ - VirtualizationType: aws.String("pv"), - } - results, err := itf.Filter(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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") -} - type ec2PricingMock struct { GetOndemandInstanceTypeCostResp float64 GetOndemandInstanceTypeCostErr error @@ -554,73 +624,3 @@ func (p *ec2PricingMock) SpotCacheCount() int { func (p *ec2PricingMock) Save() error { return nil } - -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} - -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 0, "Should return 0 instance types") -} - -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} - -func TestFilter_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.Filter(filters) - h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) -} diff --git a/pkg/selector/types.go b/pkg/selector/types.go index 95a634d..b5773fb 100644 --- a/pkg/selector/types.go +++ b/pkg/selector/types.go @@ -169,9 +169,6 @@ type Filters struct { // Possibly values are: xen or nitro Hypervisor *string - // MaxResults is the maximum number of instance types to return that match the filter criteria - MaxResults *int - // MemoryRange filter is a range of acceptable DRAM memory in Gibibytes (GiB) for the instance type MemoryRange *ByteQuantityRangeFilter From 8d513bcfb5b566a3c748a405e4188885d75a65dc Mon Sep 17 00:00:00 2001 From: "Rodrigo O.C. Pereira" <68402662+digocorbellini@users.noreply.github.com> Date: Thu, 7 Jul 2022 16:59:12 -0500 Subject: [PATCH 3/7] updated readme (#136) * updated example code in readme * updated help printout in readme * added simple output to choices for output format * added a drop down for v2.3.1 example code * fixed v2.3.1 example code * copied v2.3.1 example code from old readme Co-authored-by: Rodrigo Okamoto --- README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++--- cmd/main.go | 1 + 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c66f599..6925820 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ $ ec2-instance-selector --help ``` ```bash#help -ec2-instance-selector is a CLI tool to filter EC2 instance types based on resource criteria. +ec2-instance-selector is a CLI tool to filter EC2 instance types based on resource criteria. Filtering allows you to select all the instance types that match your application requirements. Full docs can be found at github.com/aws/amazon-ec2-instance-selector @@ -245,7 +245,7 @@ Global Flags: --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) + -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 @@ -257,8 +257,11 @@ Global Flags: This is a minimal example of using the instance selector go package directly: -**cmd/examples/example1.go** -```go#cmd/examples/example1.go +**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 ( @@ -315,6 +318,81 @@ func main() { } // Print the returned instance types slice fmt.Println(instanceTypesSlice) +} + ``` +
+ +**cmd/examples/example1.go** +```go#cmd/examples/example1.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/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 7feca88..2b526ba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -144,6 +144,7 @@ Full docs can be found at github.com/aws/amazon-` + binName tableOutput, tableWideOutput, oneLineOutput, + simpleOutput, } // Registers flags with specific input types from the cli pkg From 338d9e85ca68df635ec4ec932cfc40b47adca3ff Mon Sep 17 00:00:00 2001 From: Austin Siu Date: Mon, 11 Jul 2022 14:06:32 -0500 Subject: [PATCH 4/7] Revert "updated readme" and "Refactoring of Filter functions" (#139) * Revert "updated readme (#136)" This reverts commit 8d513bcfb5b566a3c748a405e4188885d75a65dc. * Revert "Refactoring of Filter functions (#134)" This reverts commit fd78895fe02ac889bb5fa69c0a384f634efb7cb2. --- README.md | 86 +---- cmd/examples/example1.go | 21 +- cmd/main.go | 198 ++++++------ pkg/selector/outputs/outputs.go | 22 +- pkg/selector/outputs/outputs_test.go | 48 --- pkg/selector/selector.go | 133 +++++--- pkg/selector/selector_test.go | 450 +++++++++++++-------------- pkg/selector/types.go | 3 + 8 files changed, 410 insertions(+), 551 deletions(-) diff --git a/README.md b/README.md index 6925820..c66f599 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ $ ec2-instance-selector --help ``` ```bash#help -ec2-instance-selector is a CLI tool to filter EC2 instance types based on resource criteria. +ec2-instance-selector is a CLI tool to filter EC2 instance types based on resource criteria. Filtering allows you to select all the instance types that match your application requirements. Full docs can be found at github.com/aws/amazon-ec2-instance-selector @@ -245,7 +245,7 @@ Global Flags: --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") + -o, --output string Specify the output format (table, table-wide, one-line) --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 @@ -257,11 +257,8 @@ 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 +**cmd/examples/example1.go** +```go#cmd/examples/example1.go package main import ( @@ -318,81 +315,6 @@ func main() { } // Print the returned instance types slice fmt.Println(instanceTypesSlice) -} - ``` -
- -**cmd/examples/example1.go** -```go#cmd/examples/example1.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/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/examples/example1.go b/cmd/examples/example1.go index ef0200b..31b44b0 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -5,7 +5,6 @@ import ( "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" ) @@ -47,24 +46,12 @@ func main() { 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) + // 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 getting instance types: %v", err) + fmt.Printf("Oh no, there was an error :( %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) + fmt.Println(instanceTypesSlice) } diff --git a/cmd/main.go b/cmd/main.go index 2b526ba..f57e56d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,7 +25,6 @@ import ( commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli" "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" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" "github.com/aws/aws-sdk-go/aws/session" @@ -42,6 +41,10 @@ const ( defaultProfile = "default" awsConfigFile = "~/.aws/config" spotPricingDaysBack = 30 + + tableOutput = "table" + tableWideOutput = "table-wide" + oneLine = "one-line" ) // Filter Flag Constants @@ -109,14 +112,6 @@ const ( output = "output" cacheTTL = "cache-ttl" cacheDir = "cache-dir" - - // Output constants - - tableOutput = "table" - tableWideOutput = "table-wide" - oneLineOutput = "one-line" - simpleOutput = "simple" - verboseOutput = "verbose" ) var ( @@ -143,9 +138,9 @@ Full docs can be found at github.com/aws/amazon-` + binName cliOutputTypes := []string{ tableOutput, tableWideOutput, - oneLineOutput, - simpleOutput, + oneLine, } + resultsOutputFn := outputs.SimpleInstanceTypeOutput // Registers flags with specific input types from the cli pkg // Filter Flags - These will be grouped at the top of the help flags @@ -205,7 +200,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigIntFlag(maxResults, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_MAX_RESULTS", 20), "The maximum number of instance types that match your criteria to return") cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil) cli.ConfigStringFlag(region, cli.StringMe("r"), nil, "AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence)", nil) - cli.ConfigStringFlag(output, cli.StringMe("o"), cli.StringMe(simpleOutput), fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil) + cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil) cli.ConfigIntFlag(cacheTTL, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_CACHE_TTL", 168), "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.") cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches") cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") @@ -242,6 +237,30 @@ Full docs can be found at github.com/aws/amazon-` + binName } } registerShutdown(shutdown) + outputFlag := cli.StringMe(flags[output]) + if outputFlag != nil && *outputFlag == tableWideOutput { + // If output type is `table-wide`, simply print both prices for better comparison, + // 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 flags[pricePerHour] != nil { + // Else, if price filters are applied, only hydrate the respective cache as we don't have to print the prices + if flags[usageClass] == nil || *cli.StringMe(flags[usageClass]) == "on-demand" { + if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { + log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err) + } + } + } else { + if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + log.Printf("There was a problem refreshing the spot pricing cache: %v", err) + } + } + } + } filters := selector.Filters{ VCpusRange: cli.IntRangeMe(flags[vcpus]), @@ -269,6 +288,7 @@ Full docs can be found at github.com/aws/amazon-` + binName Region: cli.StringMe(flags[region]), AvailabilityZones: cli.StringSliceMe(flags[availabilityZones]), CurrentGeneration: cli.BoolMe(flags[currentGeneration]), + MaxResults: cli.IntMe(flags[maxResults]), NetworkInterfaces: cli.IntRangeMe(flags[networkInterfaces]), NetworkPerformance: cli.IntRangeMe(flags[networkPerformance]), NetworkEncryption: cli.BoolMe(flags[networkEncryption]), @@ -293,18 +313,8 @@ 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 - outputFlag := cli.StringMe(flags[output]) - if outputFlag != nil && *outputFlag == tableWideOutput { - if err := hydrateCaches(*instanceSelector); err != nil { - log.Printf("%v", err) - } - } - if flags[verbose] != nil { - outputFlag = cli.StringMe(verboseOutput) + resultsOutputFn = outputs.VerboseInstanceTypeOutput transformedFilters, err := instanceSelector.AggregateFilterTransform(filters) if err != nil { fmt.Printf("An error occurred while transforming the aggregate filters") @@ -328,18 +338,11 @@ Full docs can be found at github.com/aws/amazon-` + binName } } - // get filtered instance types - instanceTypeDetails, err := instanceSelector.FilterInstanceTypes(filters) - if err != nil { - fmt.Printf("An error occurred when filtering instance types: %v", err) - os.Exit(1) - } + outputFn := getOutputFn(outputFlag, selector.InstanceTypesOutputFn(resultsOutputFn)) - // format instance types as strings - maxOutputResults := cli.IntMe(flags[maxResults]) - instanceTypes, itemsTruncated, err := formatInstanceTypes(instanceTypeDetails, maxOutputResults, outputFlag) + instanceTypes, itemsTruncated, err := instanceSelector.FilterWithOutput(filters, outputFn) if err != nil { - fmt.Printf("An error occured formatting instance types: %v", err) + fmt.Printf("An error occurred when filtering instance types: %v", err) os.Exit(1) } if len(instanceTypes) == 0 { @@ -347,7 +350,6 @@ Full docs can be found at github.com/aws/amazon-` + binName os.Exit(1) } - // print output for _, instanceType := range instanceTypes { fmt.Println(instanceType) } @@ -358,6 +360,60 @@ Full docs can be found at github.com/aws/amazon-` + binName shutdown() } +func hydrateCaches(instanceSelector selector.Selector) (errs error) { + wg := &sync.WaitGroup{} + hydrateTasks := []func(*sync.WaitGroup) error{ + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err)) + } + } + return nil + }, + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { + if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err)) + } + } + return nil + }, + func(waitGroup *sync.WaitGroup) error { + defer waitGroup.Done() + if instanceSelector.InstanceTypesProvider.CacheCount() == 0 { + if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil { + return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err)) + } + } + return nil + }, + } + wg.Add(len(hydrateTasks)) + for _, task := range hydrateTasks { + go task(wg) + } + wg.Wait() + return errs +} + +func getOutputFn(outputFlag *string, currentFn selector.InstanceTypesOutputFn) selector.InstanceTypesOutputFn { + outputFn := selector.InstanceTypesOutputFn(currentFn) + if outputFlag != nil { + switch *outputFlag { + case tableWideOutput: + return selector.InstanceTypesOutputFn(outputs.TableOutputWide) + case tableOutput: + return selector.InstanceTypesOutputFn(outputs.TableOutputShort) + case oneLine: + return selector.InstanceTypesOutputFn(outputs.OneLineOutput) + } + } + return outputFn +} + func getRegionAndProfileAWSSession(regionName *string, profileName *string) (*session.Session, error) { sessOpts := session.Options{SharedConfigState: session.SharedConfigEnable} if regionName != nil { @@ -431,77 +487,3 @@ func registerShutdown(shutdown func()) { shutdown() }() } - -// formatInstanceTypes accepts a list of instance types details, a number of max results, and an output flag -// and returns a list of formatted strings representing the passed in intance types with at most maxResults number -// of results. The format of the strings is determined by the output flag. The number of truncated results -// is also returned. -// Accepted output flags: "table", "table-wide", "one-line", "simple", "verbose". -func formatInstanceTypes(instanceTypes []*instancetypes.Details, maxResults *int, outputFlag *string) ([]string, int, error) { - if outputFlag == nil { - return nil, 0, fmt.Errorf("output flag is nil") - } - - instanceTypes, numOfItemsTruncated, err := outputs.TruncateResults(maxResults, instanceTypes) - if err != nil { - return nil, 0, err - } - - // See which output format to use - var outputString []string - switch *outputFlag { - case simpleOutput: - outputString = outputs.SimpleInstanceTypeOutput(instanceTypes) - case oneLineOutput: - outputString = outputs.OneLineOutput(instanceTypes) - case tableOutput: - outputString = outputs.TableOutputShort(instanceTypes) - case tableWideOutput: - outputString = outputs.TableOutputWide(instanceTypes) - case verboseOutput: - outputString = outputs.VerboseInstanceTypeOutput(instanceTypes) - default: - return nil, 0, fmt.Errorf("invalid output flag") - } - - return outputString, numOfItemsTruncated, nil -} - -func hydrateCaches(instanceSelector selector.Selector) (errs error) { - wg := &sync.WaitGroup{} - hydrateTasks := []func(*sync.WaitGroup) error{ - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err)) - } - } - return nil - }, - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.EC2Pricing.SpotCacheCount() == 0 { - if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err)) - } - } - return nil - }, - func(waitGroup *sync.WaitGroup) error { - defer waitGroup.Done() - if instanceSelector.InstanceTypesProvider.CacheCount() == 0 { - if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil { - return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err)) - } - } - return nil - }, - } - wg.Add(len(hydrateTasks)) - for _, task := range hydrateTasks { - go task(wg) - } - wg.Wait() - return errs -} diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index 61d5828..318cf8e 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -26,24 +26,6 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" ) -// TruncateResults is used to prepare a list of details for output by truncating the number of results -// in the list to have at most maxResults elements. Returns the truncated list of instance types and -// the number of truncated items. -func TruncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int, error) { - if maxResults == nil { - return instanceTypeInfoSlice, 0, nil - } else if *maxResults < 0 { - return nil, 0, fmt.Errorf("negative max results value") - } - - upperIndex := *maxResults - if *maxResults > len(instanceTypeInfoSlice) { - upperIndex = len(instanceTypeInfoSlice) - } - - return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex, nil -} - // SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { instanceTypeStrings := []string{} @@ -53,7 +35,7 @@ func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) [] return instanceTypeStrings } -// VerboseInstanceTypeOutput is an OutputFn which returns a list of full instance specs +// VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { output, err := json.MarshalIndent(instanceTypeInfoSlice, "", " ") if err != nil { @@ -192,7 +174,7 @@ func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string { return []string{buf.String()} } -// OneLineOutput is an output function which returns the instance type names on a single line separated by commas +// OneLineOutput is an output function which prints the instance type names on a single line separated by commas func OneLineOutput(instanceTypeInfoSlice []*instancetypes.Details) []string { instanceTypeNames := []string{} for _, instanceType := range instanceTypeInfoSlice { diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index d30beb1..6f484fd 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -23,7 +23,6 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "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/service/ec2" ) @@ -120,50 +119,3 @@ func TestOneLineOutput(t *testing.T) { instanceTypeOut = outputs.OneLineOutput(nil) h.Assert(t, len(instanceTypeOut) == 0, "Should return 0 instance types when passed nil") } - -func TestTruncateResults(t *testing.T) { - instanceTypes := getInstanceTypes(t, "25_instances.json") - - // test 0 for max results - maxResults := aws.Int(0) - truncatedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) - h.Ok(t, err) - h.Assert(t, len(truncatedResult) == 0, fmt.Sprintf("Should return 0 instance types since max results is set to %d, but only %d are returned in total", *maxResults, len(truncatedResult))) - h.Assert(t, numTrucated == 25, fmt.Sprintf("Should truncate 25 results, but actually truncated: %d results", numTrucated)) - - // test 1 for max results - maxResults = aws.Int(1) - truncatedResult, numTrucated, err = outputs.TruncateResults(maxResults, instanceTypes) - h.Ok(t, err) - h.Assert(t, len(truncatedResult) == 1, fmt.Sprintf("Should return 1 instance type since max results is set to %d, but only %d are returned in total", *maxResults, len(truncatedResult))) - h.Assert(t, numTrucated == 24, fmt.Sprintf("Should truncate 24 results, but actually truncated: %d results", numTrucated)) - - // test 30 for max results - maxResults = aws.Int(30) - truncatedResult, numTrucated, err = outputs.TruncateResults(maxResults, instanceTypes) - h.Ok(t, err) - h.Assert(t, len(truncatedResult) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to %d but only %d are returned in total", *maxResults, len(truncatedResult))) - h.Assert(t, numTrucated == 0, fmt.Sprintf("Should truncate 0 results, but actually truncated: %d results", numTrucated)) -} - -func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { - instanceTypes := getInstanceTypes(t, "25_instances.json") - - maxResults := aws.Int(-1) - formattedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) - - h.Assert(t, err != nil, "An error should be returned") - h.Assert(t, formattedResult == nil, fmt.Sprintf("returned list should be nil, but it is actually: %s", outputs.OneLineOutput(formattedResult))) - h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but %d results were truncated", numTrucated)) -} - -func TestFormatInstanceTypes_NilMaxResults(t *testing.T) { - instanceTypes := getInstanceTypes(t, "25_instances.json") - - var maxResults *int = nil - formattedResult, numTrucated, err := outputs.TruncateResults(maxResults, instanceTypes) - - h.Ok(t, err) - h.Assert(t, len(formattedResult) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to nil but only %d are returned in total", len(formattedResult))) - h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but actually truncated: %d results", numTrucated)) -} diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index f20d8f2..4088d2d 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -27,6 +27,7 @@ import ( "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" + "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/request" "github.com/aws/aws-sdk-go/aws/session" @@ -45,7 +46,6 @@ const ( zoneNameLocationType = "availability-zone" regionNameLocationType = "region" sdkName = "instance-selector" - spotPricingDaysBack = 30 // Filter Keys @@ -130,28 +130,77 @@ func (itf Selector) Save() error { return multierr.Append(itf.EC2Pricing.Save(), itf.InstanceTypesProvider.Save()) } -// FilterInstanceTypes accepts a Filters struct which is used to select the available instance types -// matching the criteria within Filters and returns the detailed specs of matching instance types -func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Details, error) { - // refresh OD or Spot pricing caches if pricing filters are used depending on - // which usage class is selected (default usage class is on demand) - if filters.PricePerHour != nil { - // If price filters are applied, only hydrate the respective cache as we don't have to print the prices - if filters.UsageClass == nil || *filters.UsageClass == "on-demand" { - if itf.EC2Pricing.OnDemandCacheCount() == 0 { - 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 itf.EC2Pricing.SpotCacheCount() == 0 { - if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { - return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) - } - } +// Filter accepts a Filters struct which is used to select the available instance types +// matching the criteria within Filters and returns a simple list of instance type strings +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and +// OutputInstanceTypes() in the next major version. +func (itf Selector) Filter(filters Filters) ([]string, error) { + outputFn := InstanceTypesOutputFn(outputs.SimpleInstanceTypeOutput) + output, _, err := itf.FilterWithOutput(filters, outputFn) + return output, err +} + +// FilterVerbose accepts a Filters struct which is used to select the available instance types +// matching the criteria within Filters and returns a list instanceTypeInfo +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() in the next +// major version. +func (itf Selector) FilterVerbose(filters Filters) ([]*instancetypes.Details, error) { + instanceTypeInfoSlice, err := itf.rawFilter(filters) + if err != nil { + return nil, err + } + instanceTypeInfoSlice, _ = itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) + return instanceTypeInfoSlice, nil +} + +// FilterWithOutput accepts a Filters struct which is used to select the available instance types +// matching the criteria within Filters and returns a list of strings based on the custom outputFn +// +// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and +// OutputInstanceTypes() in the next major version. +func (itf Selector) FilterWithOutput(filters Filters, outputFn InstanceTypesOutput) ([]string, int, error) { + instanceTypeInfoSlice, err := itf.rawFilter(filters) + if err != nil { + return nil, 0, err + } + instanceTypeInfoSlice, numOfItemsTruncated := itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) + output := outputFn.Output(instanceTypeInfoSlice) + return output, numOfItemsTruncated, nil +} + +func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int) { + if maxResults == nil { + return instanceTypeInfoSlice, 0 + } + upperIndex := *maxResults + if *maxResults > len(instanceTypeInfoSlice) { + upperIndex = len(instanceTypeInfoSlice) + } + return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex +} + +// AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. +func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { + transforms := []FiltersTransform{ + TransformFn(itf.TransformBaseInstanceType), + TransformFn(itf.TransformFlexible), + TransformFn(itf.TransformForService), + } + var err error + for _, transform := range transforms { + filters, err = transform.Transform(filters) + if err != nil { + return filters, err } } + return filters, nil +} +// rawFilter accepts a Filters struct which is used to select the available instance types +// matching the criteria within Filters and returns the detailed specs of matching instance types +func (itf Selector) rawFilter(filters Filters) ([]*instancetypes.Details, error) { filters, err := itf.AggregateFilterTransform(filters) if err != nil { return nil, err @@ -200,40 +249,9 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai for it := range instanceTypes { filteredInstanceTypes = append(filteredInstanceTypes, it) } - return sortInstanceTypeInfo(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 - } - 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 - }) - return instanceTypeInfoSlice -} - -// AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. -func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { - transforms := []FiltersTransform{ - TransformFn(itf.TransformBaseInstanceType), - TransformFn(itf.TransformFlexible), - TransformFn(itf.TransformForService), - } - var err error - for _, transform := range transforms { - filters, err = transform.Transform(filters) - if err != nil { - return filters, err - } - } - return filters, nil -} - func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetypes.Details, availabilityZones []string, locationInstanceOfferings map[string]string) (*instancetypes.Details, error) { instanceTypeName := *instanceTypeInfo.InstanceType isFpga := instanceTypeInfo.FpgaInfo != nil @@ -333,6 +351,19 @@ func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetype return &instanceTypeInfo, nil } +// sortInstanceTypeInfo will sort based on instance type info alpha-numerically +func sortInstanceTypeInfo(instanceTypeInfoSlice []*instancetypes.Details) []*instancetypes.Details { + if len(instanceTypeInfoSlice) < 2 { + return instanceTypeInfoSlice + } + 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 + }) + return instanceTypeInfoSlice +} + // executeFilters accepts a mapping of filter name to filter pairs which are iterated through // to determine if the instance type matches the filter values. func (itf Selector) executeFilters(filterToInstanceSpecMapping map[string]filterPair, instanceType string) (bool, error) { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index d77f3a8..a270938 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -154,34 +154,38 @@ func TestNew(t *testing.T) { h.Assert(t, itf != nil, "selector instance created without error") } -func TestFilterInstanceTypes(t *testing.T) { +func TestFilterVerbose(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) - filter := selector.Filters{ + filters := selector.Filters{ VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, } - - results, err := itf.FilterInstanceTypes(filter) - + results, err := itf.FilterVerbose(filters) 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]) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus but actually returned "+strconv.Itoa(len(results))) + h.Assert(t, *results[0].InstanceType == "t3.micro", "Should return t3.micro, got %s instead", results[0].InstanceType) } -func TestFilterInstanceTypes_NoResults(t *testing.T) { +func TestFilterVerbose_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) - + results, err := itf.FilterVerbose(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) { +func TestFilterVerbose_Failure(t *testing.T) { + itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) + filters := selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, + } + results, err := itf.FilterVerbose(filters) + h.Assert(t, results == nil, "Results should be nil") + h.Assert(t, err != nil, "An error should be returned") +} + +func TestFilterVerbose_AZFilteredIn(t *testing.T) { ec2Mock := mockedEC2{ DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "t3_micro.json").DescribeInstanceTypesPagesResp, DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp, @@ -192,17 +196,13 @@ func TestFilterInstanceTypes_AZFilteredIn(t *testing.T) { VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, AvailabilityZones: &[]string{"us-east-2a"}, } - - results, err := itf.FilterInstanceTypes(filters) - + results, err := itf.FilterVerbose(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]) + h.Assert(t, *results[0].InstanceType == "t3.micro", "Should return t3.micro, got %s instead", results[0].InstanceType) } -func TestFilterInstanceTypes_AZFilteredOut(t *testing.T) { +func TestFilterVerbose_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, @@ -212,26 +212,22 @@ func TestFilterInstanceTypes_AZFilteredOut(t *testing.T) { filters := selector.Filters{ AvailabilityZones: &[]string{"us-east-2a"}, } - - results, err := itf.FilterInstanceTypes(filters) - + results, err := itf.FilterVerbose(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) { +func TestFilterVerboseAZ_FilteredErr(t *testing.T) { itf := getSelector(mockedEC2{}) filters := selector.Filters{ VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, AvailabilityZones: &[]string{"blah"}, } - - _, err := itf.FilterInstanceTypes(filters) - + _, err := itf.FilterVerbose(filters) h.Assert(t, err != nil, "Should error since bad zone was passed in") } -func TestFilterInstanceTypes_Gpus(t *testing.T) { +func TestFilterVerbose_Gpus(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro_and_p3_16xl.json")) gpuMemory, err := bytequantity.ParseToByteQuantity("128g") h.Ok(t, err) @@ -242,233 +238,72 @@ func TestFilterInstanceTypes_Gpus(t *testing.T) { UpperBound: gpuMemory, }, } - - results, err := itf.FilterInstanceTypes(filters) - + results, err := itf.FilterVerbose(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) + h.Assert(t, *results[0].InstanceType == "p3.16xlarge", "Should return p3.16xlarge, got %s instead", *results[0].InstanceType) } -func TestFilterInstanceTypes_MoreFilters(t *testing.T) { +func TestFilter(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), + VCpusRange: &selector.IntRangeFilter{LowerBound: 2, UpperBound: 2}, } - - results, err := itf.FilterInstanceTypes(filters) - + results, err := itf.Filter(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)) + h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) } -func TestFilterInstanceTypes_X8664_AMD64(t *testing.T) { +func TestFilter_MoreFilters(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) filters := selector.Filters{ - CPUArchitecture: aws.String("amd64"), + 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) + results, err := itf.Filter(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]) - + h.Assert(t, len(results) == 1, "Should only return 1 instance type with 2 vcpus") + h.Assert(t, results[0] == "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")) +func TestFilter_TruncateToMaxResults(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "25_instances.json")) filters := selector.Filters{ - VirtualizationType: aws.String("pv"), + VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, } - - results, err := itf.FilterInstanceTypes(filters) - + results, err := itf.Filter(filters) h.Ok(t, err) - h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: pv") + h.Assert(t, len(results) > 1, "Should return > 1 instance types since max results is not set") 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, + VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, + MaxResults: aws.Int(1), } - filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0105, - UpperBound: 0.0105, - }, - } - - results, err := itf.FilterInstanceTypes(filters) - + results, err = itf.Filter(filters) h.Ok(t, err) - h.Assert(t, len(results) == 0, "Should return 0 instance types") -} + h.Assert(t, len(results) == 1, "Should return 1 instance types since max results is set") -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"), + filters = selector.Filters{ + VCpusRange: &selector.IntRangeFilter{LowerBound: 0, UpperBound: 100}, + MaxResults: aws.Int(30), } - - results, err := itf.FilterInstanceTypes(filters) - + results, err = itf.Filter(filters) h.Ok(t, err) - h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) + h.Assert(t, len(results) == 25, fmt.Sprintf("Should return 25 instance types since max results is set to 30 but only %d are returned in total", 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, - } +func TestFilter_Failure(t *testing.T) { + itf := getSelector(mockedEC2{DescribeInstanceTypesPagesErr: errors.New("error")}) filters := selector.Filters{ - PricePerHour: &selector.Float64RangeFilter{ - LowerBound: 0.0104, - UpperBound: 0.0104, - }, - UsageClass: aws.String("spot"), + VCpusRange: &selector.IntRangeFilter{LowerBound: 4, UpperBound: 4}, } - - 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))) + results, err := itf.Filter(filters) + h.Assert(t, results == nil, "Results should be nil") + h.Assert(t, err != nil, "An error should be returned") } func TestRetrieveInstanceTypesSupportedInAZ_WithZoneName(t *testing.T) { @@ -542,6 +377,22 @@ func TestAggregateFilterTransform_InvalidInstanceType(t *testing.T) { h.Nok(t, err) } +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 3, "c4.large should return 3 similar instance types") +} + func TestRetrieveInstanceTypesSupportedInAZs_Intersection(t *testing.T) { ec2Mock := mockMultiRespDescribeInstanceTypesOfferings(t, map[string]string{ "us-east-2a": "us-east-2a.json", @@ -586,6 +437,85 @@ func TestRetrieveInstanceTypesSupportedInAZs_DescribeAZErr(t *testing.T) { h.Nok(t, err) } +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Allow List Regex: 'c4.large' should return 1 instance type") +} + +func TestFilter_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.Filter(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 TestFilter_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.Filter(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 TestFilter_X8664_AMD64(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{ + CPUArchitecture: aws.String("amd64"), + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, "Should only return 1 instance type with x86_64/amd64 cpu architecture") + h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) +} + +func TestFilter_VirtType_PV(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "pv_instances.json")) + filters := selector.Filters{ + VirtualizationType: aws.String("pv"), + } + results, err := itf.Filter(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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") +} + type ec2PricingMock struct { GetOndemandInstanceTypeCostResp float64 GetOndemandInstanceTypeCostErr error @@ -624,3 +554,73 @@ func (p *ec2PricingMock) SpotCacheCount() int { func (p *ec2PricingMock) Save() error { return nil } + +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} + +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 0, "Should return 0 instance types") +} + +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} + +func TestFilter_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.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) +} diff --git a/pkg/selector/types.go b/pkg/selector/types.go index b5773fb..95a634d 100644 --- a/pkg/selector/types.go +++ b/pkg/selector/types.go @@ -169,6 +169,9 @@ type Filters struct { // Possibly values are: xen or nitro Hypervisor *string + // MaxResults is the maximum number of instance types to return that match the filter criteria + MaxResults *int + // MemoryRange filter is a range of acceptable DRAM memory in Gibibytes (GiB) for the instance type MemoryRange *ByteQuantityRangeFilter From cec208c8d60d8348fbba31f791068b65269d459a Mon Sep 17 00:00:00 2001 From: Austin Siu Date: Mon, 11 Jul 2022 14:39:09 -0500 Subject: [PATCH 5/7] Handle pricing api change in aws-sdk-go 1.44.46 (#140) --- go.mod | 2 +- go.sum | 2 ++ pkg/ec2pricing/ec2pricing_test.go | 2 +- pkg/ec2pricing/odpricing.go | 6 +++++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 388553c..cf39dfd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/aws/amazon-ec2-instance-selector/v2 go 1.18 require ( - github.com/aws/aws-sdk-go v1.43.31 + github.com/aws/aws-sdk-go v1.44.51 github.com/blang/semver/v4 v4.0.0 github.com/imdario/mergo v0.3.11 github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 53c0f70..475a643 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.43.31 h1:yJZIr8nMV1hXjAvvOLUFqZRJcHV7udPQBfhJqawDzI0= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.51 h1:jO9hoLynZOrMM4dj0KjeKIK+c6PA+HQbKoHOkAEye2Y= +github.com/aws/aws-sdk-go v1.44.51/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= diff --git a/pkg/ec2pricing/ec2pricing_test.go b/pkg/ec2pricing/ec2pricing_test.go index f4dfbac..ee023ed 100644 --- a/pkg/ec2pricing/ec2pricing_test.go +++ b/pkg/ec2pricing/ec2pricing_test.go @@ -75,7 +75,7 @@ func setupMock(t *testing.T, api string, file string) mockedPricing { err = json.Unmarshal(mockFile, &productsMap) h.Assert(t, err == nil, "Error parsing mock json file contents "+mockFilename) productsOutput := pricing.GetProductsOutput{ - PriceList: []aws.JSONValue{productsMap}, + PriceList: []*string{aws.String(string(mockFile))}, } return mockedPricing{ GetProductsPagesResp: productsOutput, diff --git a/pkg/ec2pricing/odpricing.go b/pkg/ec2pricing/odpricing.go index f4ea64b..b62d154 100644 --- a/pkg/ec2pricing/odpricing.go +++ b/pkg/ec2pricing/odpricing.go @@ -225,10 +225,14 @@ func (c *OnDemandPricing) getRegionForPricingAPI() string { } // parseOndemandUnitPrice takes a priceList from the pricing API and parses its weirdness -func (c *OnDemandPricing) parseOndemandUnitPrice(priceList aws.JSONValue) (string, float64, error) { +func (c *OnDemandPricing) parseOndemandUnitPrice(priceDoc *string) (string, float64, error) { // TODO: this could probably be cleaned up a bit by adding a couple structs with json tags // We still need to some weird for-loops to get at elements under json keys that are IDs... // But it would probably be cleaner than this. + var priceList map[string]interface{} + if err := json.Unmarshal([]byte(*priceDoc), &priceList); err != nil { + return "", float64(-1.0), fmt.Errorf("unable to deserialize pricing doc") + } attributes, ok := priceList["product"].(map[string]interface{})["attributes"] if !ok { return "", float64(-1.0), fmt.Errorf("unable to find product attributes") From 603b2d7093adc4e68b31db3f718a62cc6a064dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bryan=E2=84=A2?= <61433408+brycahta@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:35:54 -0500 Subject: [PATCH 6/7] Update README.md (#141) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c66f599..8f29f68 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ brew install ec2-instance-selector #### Install w/ Curl for Linux/Mac ``` -curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.3.0/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector +curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.3.1/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector ``` To execute the CLI, you will need AWS credentials configured. Take a look at the [AWS CLI configuration documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#config-settings-and-precedence) for details on the various ways to configure credentials. An easy way to try out the ec2-instance-selector CLI is to populate the following environment variables with your AWS API credentials. From 0154f4b7b6eab749fa03f82d7edd875d5605f3bc Mon Sep 17 00:00:00 2001 From: Austin Siu Date: Tue, 12 Jul 2022 10:38:35 -0500 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A5=91=F0=9F=A4=96=20v2.3.2=20release?= =?UTF-8?q?=20prep=20=F0=9F=A4=96=F0=9F=A5=91=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f29f68..612d31f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ brew install ec2-instance-selector #### Install w/ Curl for Linux/Mac ``` -curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.3.1/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector +curl -Lo ec2-instance-selector https://github.com/aws/amazon-ec2-instance-selector/releases/download/v2.3.2/ec2-instance-selector-`uname | tr '[:upper:]' '[:lower:]'`-amd64 && chmod +x ec2-instance-selector ``` To execute the CLI, you will need AWS credentials configured. Take a look at the [AWS CLI configuration documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#config-settings-and-precedence) for details on the various ways to configure credentials. An easy way to try out the ec2-instance-selector CLI is to populate the following environment variables with your AWS API credentials.