Skip to content

Commit fd78895

Browse files
digocorbelliniRodrigo Okamoto
and
Rodrigo Okamoto
authored
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 <rodocp@amazon.com>
1 parent 8ac398b commit fd78895

File tree

7 files changed

+468
-406
lines changed

7 files changed

+468
-406
lines changed

cmd/examples/example1.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity"
77
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector"
8+
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs"
89
"github.com/aws/aws-sdk-go/aws"
910
"github.com/aws/aws-sdk-go/aws/session"
1011
)
@@ -46,12 +47,24 @@ func main() {
4647
CPUArchitecture: &cpuArch,
4748
}
4849

49-
// Pass the Filter struct to the Filter function of your selector instance
50-
instanceTypesSlice, err := instanceSelector.Filter(filters)
50+
// Pass the Filter struct to the FilteredInstanceTypes function of your
51+
// selector instance to get a list of filtered instance types and their details
52+
instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters)
5153
if err != nil {
52-
fmt.Printf("Oh no, there was an error :( %v", err)
54+
fmt.Printf("Oh no, there was an error getting instance types: %v", err)
5355
return
5456
}
57+
58+
// Pass in your list of instance type details to the appropriate output function
59+
// in order to format the instance types as printable strings.
60+
maxResults := 100
61+
instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice)
62+
if err != nil {
63+
fmt.Printf("Oh no, there was an error truncating instnace types: %v", err)
64+
return
65+
}
66+
instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice)
67+
5568
// Print the returned instance types slice
56-
fmt.Println(instanceTypesSlice)
69+
fmt.Println(instanceTypes)
5770
}

cmd/main.go

Lines changed: 107 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli"
2727
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/env"
28+
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes"
2829
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector"
2930
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs"
3031
"github.com/aws/aws-sdk-go/aws/session"
@@ -41,10 +42,6 @@ const (
4142
defaultProfile = "default"
4243
awsConfigFile = "~/.aws/config"
4344
spotPricingDaysBack = 30
44-
45-
tableOutput = "table"
46-
tableWideOutput = "table-wide"
47-
oneLine = "one-line"
4845
)
4946

5047
// Filter Flag Constants
@@ -112,6 +109,14 @@ const (
112109
output = "output"
113110
cacheTTL = "cache-ttl"
114111
cacheDir = "cache-dir"
112+
113+
// Output constants
114+
115+
tableOutput = "table"
116+
tableWideOutput = "table-wide"
117+
oneLineOutput = "one-line"
118+
simpleOutput = "simple"
119+
verboseOutput = "verbose"
115120
)
116121

117122
var (
@@ -138,9 +143,8 @@ Full docs can be found at github.com/aws/amazon-` + binName
138143
cliOutputTypes := []string{
139144
tableOutput,
140145
tableWideOutput,
141-
oneLine,
146+
oneLineOutput,
142147
}
143-
resultsOutputFn := outputs.SimpleInstanceTypeOutput
144148

145149
// Registers flags with specific input types from the cli pkg
146150
// 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
200204
cli.ConfigIntFlag(maxResults, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_MAX_RESULTS", 20), "The maximum number of instance types that match your criteria to return")
201205
cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil)
202206
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)
203-
cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil)
207+
cli.ConfigStringFlag(output, cli.StringMe("o"), cli.StringMe(simpleOutput), fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil)
204208
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.")
205209
cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches")
206210
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
237241
}
238242
}
239243
registerShutdown(shutdown)
240-
outputFlag := cli.StringMe(flags[output])
241-
if outputFlag != nil && *outputFlag == tableWideOutput {
242-
// If output type is `table-wide`, simply print both prices for better comparison,
243-
// even if the actual filter is applied on any one of those based on usage class
244-
// Save time by hydrating all caches in parallel
245-
if err := hydrateCaches(*instanceSelector); err != nil {
246-
log.Printf("%v", err)
247-
}
248-
} else if flags[pricePerHour] != nil {
249-
// Else, if price filters are applied, only hydrate the respective cache as we don't have to print the prices
250-
if flags[usageClass] == nil || *cli.StringMe(flags[usageClass]) == "on-demand" {
251-
if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
252-
if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil {
253-
log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err)
254-
}
255-
}
256-
} else {
257-
if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
258-
if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil {
259-
log.Printf("There was a problem refreshing the spot pricing cache: %v", err)
260-
}
261-
}
262-
}
263-
}
264244

265245
filters := selector.Filters{
266246
VCpusRange: cli.IntRangeMe(flags[vcpus]),
@@ -288,7 +268,6 @@ Full docs can be found at github.com/aws/amazon-` + binName
288268
Region: cli.StringMe(flags[region]),
289269
AvailabilityZones: cli.StringSliceMe(flags[availabilityZones]),
290270
CurrentGeneration: cli.BoolMe(flags[currentGeneration]),
291-
MaxResults: cli.IntMe(flags[maxResults]),
292271
NetworkInterfaces: cli.IntRangeMe(flags[networkInterfaces]),
293272
NetworkPerformance: cli.IntRangeMe(flags[networkPerformance]),
294273
NetworkEncryption: cli.BoolMe(flags[networkEncryption]),
@@ -313,8 +292,18 @@ Full docs can be found at github.com/aws/amazon-` + binName
313292
DedicatedHosts: cli.BoolMe(flags[dedicatedHosts]),
314293
}
315294

295+
// If output type is `table-wide`, cache both prices for better comparison in output,
296+
// even if the actual filter is applied on any one of those based on usage class
297+
// Save time by hydrating all caches in parallel
298+
outputFlag := cli.StringMe(flags[output])
299+
if outputFlag != nil && *outputFlag == tableWideOutput {
300+
if err := hydrateCaches(*instanceSelector); err != nil {
301+
log.Printf("%v", err)
302+
}
303+
}
304+
316305
if flags[verbose] != nil {
317-
resultsOutputFn = outputs.VerboseInstanceTypeOutput
306+
outputFlag = cli.StringMe(verboseOutput)
318307
transformedFilters, err := instanceSelector.AggregateFilterTransform(filters)
319308
if err != nil {
320309
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
338327
}
339328
}
340329

341-
outputFn := getOutputFn(outputFlag, selector.InstanceTypesOutputFn(resultsOutputFn))
342-
343-
instanceTypes, itemsTruncated, err := instanceSelector.FilterWithOutput(filters, outputFn)
330+
// get filtered instance types
331+
instanceTypeDetails, err := instanceSelector.FilterInstanceTypes(filters)
344332
if err != nil {
345333
fmt.Printf("An error occurred when filtering instance types: %v", err)
346334
os.Exit(1)
347335
}
336+
337+
// format instance types as strings
338+
maxOutputResults := cli.IntMe(flags[maxResults])
339+
instanceTypes, itemsTruncated, err := formatInstanceTypes(instanceTypeDetails, maxOutputResults, outputFlag)
340+
if err != nil {
341+
fmt.Printf("An error occured formatting instance types: %v", err)
342+
os.Exit(1)
343+
}
348344
if len(instanceTypes) == 0 {
349345
log.Println("The criteria was too narrow and returned no valid instance types. Consider broadening your criteria so that more instance types are returned.")
350346
os.Exit(1)
351347
}
352348

349+
// print output
353350
for _, instanceType := range instanceTypes {
354351
fmt.Println(instanceType)
355352
}
@@ -360,60 +357,6 @@ Full docs can be found at github.com/aws/amazon-` + binName
360357
shutdown()
361358
}
362359

363-
func hydrateCaches(instanceSelector selector.Selector) (errs error) {
364-
wg := &sync.WaitGroup{}
365-
hydrateTasks := []func(*sync.WaitGroup) error{
366-
func(waitGroup *sync.WaitGroup) error {
367-
defer waitGroup.Done()
368-
if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
369-
if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil {
370-
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err))
371-
}
372-
}
373-
return nil
374-
},
375-
func(waitGroup *sync.WaitGroup) error {
376-
defer waitGroup.Done()
377-
if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
378-
if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil {
379-
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err))
380-
}
381-
}
382-
return nil
383-
},
384-
func(waitGroup *sync.WaitGroup) error {
385-
defer waitGroup.Done()
386-
if instanceSelector.InstanceTypesProvider.CacheCount() == 0 {
387-
if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil {
388-
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err))
389-
}
390-
}
391-
return nil
392-
},
393-
}
394-
wg.Add(len(hydrateTasks))
395-
for _, task := range hydrateTasks {
396-
go task(wg)
397-
}
398-
wg.Wait()
399-
return errs
400-
}
401-
402-
func getOutputFn(outputFlag *string, currentFn selector.InstanceTypesOutputFn) selector.InstanceTypesOutputFn {
403-
outputFn := selector.InstanceTypesOutputFn(currentFn)
404-
if outputFlag != nil {
405-
switch *outputFlag {
406-
case tableWideOutput:
407-
return selector.InstanceTypesOutputFn(outputs.TableOutputWide)
408-
case tableOutput:
409-
return selector.InstanceTypesOutputFn(outputs.TableOutputShort)
410-
case oneLine:
411-
return selector.InstanceTypesOutputFn(outputs.OneLineOutput)
412-
}
413-
}
414-
return outputFn
415-
}
416-
417360
func getRegionAndProfileAWSSession(regionName *string, profileName *string) (*session.Session, error) {
418361
sessOpts := session.Options{SharedConfigState: session.SharedConfigEnable}
419362
if regionName != nil {
@@ -487,3 +430,77 @@ func registerShutdown(shutdown func()) {
487430
shutdown()
488431
}()
489432
}
433+
434+
// formatInstanceTypes accepts a list of instance types details, a number of max results, and an output flag
435+
// and returns a list of formatted strings representing the passed in intance types with at most maxResults number
436+
// of results. The format of the strings is determined by the output flag. The number of truncated results
437+
// is also returned.
438+
// Accepted output flags: "table", "table-wide", "one-line", "simple", "verbose".
439+
func formatInstanceTypes(instanceTypes []*instancetypes.Details, maxResults *int, outputFlag *string) ([]string, int, error) {
440+
if outputFlag == nil {
441+
return nil, 0, fmt.Errorf("output flag is nil")
442+
}
443+
444+
instanceTypes, numOfItemsTruncated, err := outputs.TruncateResults(maxResults, instanceTypes)
445+
if err != nil {
446+
return nil, 0, err
447+
}
448+
449+
// See which output format to use
450+
var outputString []string
451+
switch *outputFlag {
452+
case simpleOutput:
453+
outputString = outputs.SimpleInstanceTypeOutput(instanceTypes)
454+
case oneLineOutput:
455+
outputString = outputs.OneLineOutput(instanceTypes)
456+
case tableOutput:
457+
outputString = outputs.TableOutputShort(instanceTypes)
458+
case tableWideOutput:
459+
outputString = outputs.TableOutputWide(instanceTypes)
460+
case verboseOutput:
461+
outputString = outputs.VerboseInstanceTypeOutput(instanceTypes)
462+
default:
463+
return nil, 0, fmt.Errorf("invalid output flag")
464+
}
465+
466+
return outputString, numOfItemsTruncated, nil
467+
}
468+
469+
func hydrateCaches(instanceSelector selector.Selector) (errs error) {
470+
wg := &sync.WaitGroup{}
471+
hydrateTasks := []func(*sync.WaitGroup) error{
472+
func(waitGroup *sync.WaitGroup) error {
473+
defer waitGroup.Done()
474+
if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
475+
if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil {
476+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err))
477+
}
478+
}
479+
return nil
480+
},
481+
func(waitGroup *sync.WaitGroup) error {
482+
defer waitGroup.Done()
483+
if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
484+
if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil {
485+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err))
486+
}
487+
}
488+
return nil
489+
},
490+
func(waitGroup *sync.WaitGroup) error {
491+
defer waitGroup.Done()
492+
if instanceSelector.InstanceTypesProvider.CacheCount() == 0 {
493+
if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil {
494+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err))
495+
}
496+
}
497+
return nil
498+
},
499+
}
500+
wg.Add(len(hydrateTasks))
501+
for _, task := range hydrateTasks {
502+
go task(wg)
503+
}
504+
wg.Wait()
505+
return errs
506+
}

pkg/selector/outputs/outputs.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ import (
2626
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes"
2727
)
2828

29+
// TruncateResults is used to prepare a list of details for output by truncating the number of results
30+
// in the list to have at most maxResults elements. Returns the truncated list of instance types and
31+
// the number of truncated items.
32+
func TruncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int, error) {
33+
if maxResults == nil {
34+
return instanceTypeInfoSlice, 0, nil
35+
} else if *maxResults < 0 {
36+
return nil, 0, fmt.Errorf("negative max results value")
37+
}
38+
39+
upperIndex := *maxResults
40+
if *maxResults > len(instanceTypeInfoSlice) {
41+
upperIndex = len(instanceTypeInfoSlice)
42+
}
43+
44+
return instanceTypeInfoSlice[0:upperIndex], len(instanceTypeInfoSlice) - upperIndex, nil
45+
}
46+
2947
// SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names
3048
func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
3149
instanceTypeStrings := []string{}
@@ -35,7 +53,7 @@ func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []
3553
return instanceTypeStrings
3654
}
3755

38-
// VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names
56+
// VerboseInstanceTypeOutput is an OutputFn which returns a list of full instance specs
3957
func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
4058
output, err := json.MarshalIndent(instanceTypeInfoSlice, "", " ")
4159
if err != nil {
@@ -174,7 +192,7 @@ func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string {
174192
return []string{buf.String()}
175193
}
176194

177-
// OneLineOutput is an output function which prints the instance type names on a single line separated by commas
195+
// OneLineOutput is an output function which returns the instance type names on a single line separated by commas
178196
func OneLineOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
179197
instanceTypeNames := []string{}
180198
for _, instanceType := range instanceTypeInfoSlice {

0 commit comments

Comments
 (0)