diff --git a/README.md b/README.md index 6925820..e011b19 100644 --- a/README.md +++ b/README.md @@ -241,15 +241,17 @@ Suite Flags: Global Flags: - --cache-dir string Directory to save the pricing and instance type caches (default "~/.ec2-instance-selector/") - --cache-ttl int Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches. (default 168) - -h, --help Help - --max-results int The maximum number of instance types that match your criteria to return (default 20) - -o, --output string Specify the output format (table, table-wide, one-line, simple) (default "simple") - --profile string AWS CLI profile to use for credentials and config - -r, --region string AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence) - -v, --verbose Verbose - will print out full instance specs - --version Prints CLI version + --cache-dir string Directory to save the pricing and instance type caches (default "~/.ec2-instance-selector/") + --cache-ttl int Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches. (default 168) + -h, --help Help + --max-results int The maximum number of instance types that match your criteria to return (default 20) + -o, --output string Specify the output format (table, table-wide, one-line, simple) (default "simple") + --profile string AWS CLI profile to use for credentials and config + -r, --region string AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence) + --sort-direction string Specify the direction to sort in (ascending, descending) (default "ascending") + --sort-filter string Specify the field to sort by (on-demand-price, spot-price, vcpu, memory, instance-type-name) (default "instance-type-name") + -v, --verbose Verbose - will print out full instance specs + --version Prints CLI version ``` @@ -257,71 +259,6 @@ Global Flags: This is a minimal example of using the instance selector go package directly: -**NOTE:** The example below is intended for `v3+`. For versions `v2.3.1` and earlier, refer to the following dropdown: -
- Example for v2.3.1 - - ```go -package main - -import ( - "fmt" - - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" - "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" -) - -func main() { - // Load an AWS session by looking at shared credentials or environment variables - // https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ - sess, err := session.NewSession(&aws.Config{ - Region: aws.String("us-east-2"), - }) - if err != nil { - fmt.Printf("Oh no, AWS session credentials cannot be found: %v", err) - return - } - - // Instantiate a new instance of a selector with the AWS session - instanceSelector := selector.New(sess) - - // Instantiate an int range filter to specify min and max vcpus - vcpusRange := selector.IntRangeFilter{ - LowerBound: 2, - UpperBound: 4, - } - // Instantiate a byte quantity range filter to specify min and max memory in GiB - memoryRange := selector.ByteQuantityRangeFilter{ - LowerBound: bytequantity.FromGiB(2), - UpperBound: bytequantity.FromGiB(4), - } - // Create a string for the CPU Architecture so that it can be passed as a pointer - // when creating the Filter struct - cpuArch := "x86_64" - - // Create a Filter struct with criteria you would like to filter - // The full struct definition can be found here for all of the supported filters: - // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go - filters := selector.Filters{ - VCpusRange: &vcpusRange, - MemoryRange: &memoryRange, - CPUArchitecture: &cpuArch, - } - - // Pass the Filter struct to the Filter function of your selector instance - instanceTypesSlice, err := instanceSelector.Filter(filters) - if err != nil { - fmt.Printf("Oh no, there was an error :( %v", err) - return - } - // Print the returned instance types slice - fmt.Println(instanceTypesSlice) -} - ``` -
- **cmd/examples/example1.go** ```go#cmd/examples/example1.go package main @@ -374,19 +311,32 @@ func main() { } // Pass the Filter struct to the FilteredInstanceTypes function of your - // selector instance to get a list of filtered instance types and their details + // selector instance to get a list of filtered instance types and their details. instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters) if err != nil { fmt.Printf("Oh no, there was an error getting instance types: %v", err) return } - // Pass in your list of instance type details to the appropriate output function - // in order to format the instance types as printable strings. - maxResults := 100 + // Pass in the list of instance type details to the SortInstanceTypes function + // if you wish to sort the instances based on set filters. + sortFilter := "instance-type-name" + sortDirection := "ascending" + instanceTypesSlice, err = instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) + if err != nil { + fmt.Printf("Oh no, there was an error sorting instance types: %v", err) + return + } + + // Truncate results and format them for output with your desired formatting function. + // All formatting functions can be found here: + // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go + // Examples of formatted outputs can be found here: + // https://github.com/aws/amazon-ec2-instance-selector#examples + maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { - fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) + fmt.Printf("Oh no, there was an error truncating instance types: %v", err) return } instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) @@ -403,7 +353,7 @@ func main() { $ git clone https://github.com/aws/amazon-ec2-instance-selector.git $ cd amazon-ec2-instance-selector/ $ go run cmd/examples/example1.go -[c4.large c5.large c5a.large c5ad.large c5d.large c6i.large t2.medium t3.medium t3.small t3a.medium t3a.small] +[c4.large c5.large c5a.large c5ad.large c5d.large c6a.large c6i.large c6id.large t2.medium t3.medium] ``` ## Building diff --git a/cmd/examples/example1.go b/cmd/examples/example1.go index ef0200b..abcfec2 100644 --- a/cmd/examples/example1.go +++ b/cmd/examples/example1.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "fmt" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli/sorter" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" "github.com/aws/aws-sdk-go/aws" @@ -38,6 +40,11 @@ func main() { // when creating the Filter struct cpuArch := "x86_64" + priceRange := selector.Float64RangeFilter{ + LowerBound: 0, + UpperBound: 100, + } + // Create a Filter struct with criteria you would like to filter // The full struct definition can be found here for all of the supported filters: // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/types.go @@ -45,26 +52,59 @@ func main() { VCpusRange: &vcpusRange, MemoryRange: &memoryRange, CPUArchitecture: &cpuArch, + PricePerHour: &priceRange, } // Pass the Filter struct to the FilteredInstanceTypes function of your - // selector instance to get a list of filtered instance types and their details + // selector instance to get a list of filtered instance types and their details. instanceTypesSlice, err := instanceSelector.FilterInstanceTypes(filters) if err != nil { fmt.Printf("Oh no, there was an error getting instance types: %v", err) return } - // Pass in your list of instance type details to the appropriate output function - // in order to format the instance types as printable strings. - maxResults := 100 + // Truncate results and format them for output with your desired formatting function. + // All formatting functions can be found here: + // https://github.com/aws/amazon-ec2-instance-selector/blob/main/pkg/selector/outputs/outputs.go + // Examples of formatted outputs can be found here: + // https://github.com/aws/amazon-ec2-instance-selector#examples + maxResults := 10 instanceTypesSlice, _, err = outputs.TruncateResults(&maxResults, instanceTypesSlice) if err != nil { - fmt.Printf("Oh no, there was an error truncating instnace types: %v", err) + fmt.Printf("Oh no, there was an error truncating instance types: %v", err) return } - instanceTypes := outputs.SimpleInstanceTypeOutput(instanceTypesSlice) + + instanceTypesSlice[3].OnDemandPricePerHour = nil + instanceTypesSlice[8].OnDemandPricePerHour = nil + + // Pass in the list of instance type details to the SortInstanceTypes function + // if you wish to sort the instances based on set filters. + sortFilter := "on-demand-price" + sortDirection := "descending" + sortedTypes, err := instanceSelector.SortInstanceTypes(instanceTypesSlice, &sortFilter, &sortDirection) + if err != nil { + fmt.Printf("Oh no, there was an error sorting instance types: %v", err) + return + } + instanceTypes := outputs.TableOutputWide(sortedTypes) + + jsonTypeIndent, err := json.MarshalIndent(instanceTypesSlice[0], "", " ") + fmt.Println(string(jsonTypeIndent)) + fmt.Println() // Print the returned instance types slice fmt.Println(instanceTypes) + + // TODO: remove this. This is for testing purposes + fmt.Println() + + // jsonType, err := json.Marshal(instanceTypesSlice[0]) + // fmt.Println(string(jsonType) + "\n") + + sorter, err := sorter.NewSorter(instanceTypesSlice, "$.OndemandPricePerHour", sortDirection) + sorter.Sort() + types := sorter.InstanceTypes() + str := outputs.TableOutputWide(types) + fmt.Printf(str[0]) } diff --git a/cmd/main.go b/cmd/main.go index 2b526ba..c7d7c27 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "time" commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli/sorter" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/env" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" @@ -100,15 +101,17 @@ const ( // Configuration Flag Constants const ( - maxResults = "max-results" - profile = "profile" - help = "help" - verbose = "verbose" - version = "version" - region = "region" - output = "output" - cacheTTL = "cache-ttl" - cacheDir = "cache-dir" + maxResults = "max-results" + profile = "profile" + help = "help" + verbose = "verbose" + version = "version" + region = "region" + output = "output" + cacheTTL = "cache-ttl" + cacheDir = "cache-dir" + sortDirection = "sort-direction" + sortBy = "sort-by" // Output constants @@ -117,6 +120,20 @@ const ( oneLineOutput = "one-line" simpleOutput = "simple" verboseOutput = "verbose" + + // Sorting constants + + // TODO: remove + sortODPrice = "on-demand-price" + sortSpotPrice = "spot-price" + sortVCPU = "vcpu" + sortMemory = "memory" + sortName = "instance-type-name" + + sortAscending = "ascending" + sortAsc = "asc" + sortDescending = "descending" + sortDesc = "desc" ) var ( @@ -147,6 +164,22 @@ Full docs can be found at github.com/aws/amazon-` + binName simpleOutput, } + // TODO: remove + // cliSortCriteria := []string{ + // sortODPrice, + // sortSpotPrice, + // sortVCPU, + // sortMemory, + // sortName, + // } + + cliSortDirections := []string{ + sortAscending, + sortAsc, + sortDescending, + sortDesc, + } + // Registers flags with specific input types from the cli pkg // Filter Flags - These will be grouped at the top of the help flags @@ -211,6 +244,10 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") + cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) + // TODO: modify this to accept all strings and change help comment to include text about json path and defaults + // TODO: modify description to include json path and defaults + cli.ConfigStringFlag(sortBy, nil, cli.StringMe(sortName), "Specify the field to sort by", nil) // Parses the user input with the registered flags and runs type specific validation on the user input flags, err := cli.ParseAndValidateFlags() @@ -293,14 +330,28 @@ Full docs can be found at github.com/aws/amazon-` + binName DedicatedHosts: cli.BoolMe(flags[dedicatedHosts]), } - // If output type is `table-wide`, cache both prices for better comparison in output, - // even if the actual filter is applied on any one of those based on usage class - // Save time by hydrating all caches in parallel + sortFilterFlag := cli.StringMe(flags[sortBy]) + lowercaseField := strings.ToLower(*sortFilterFlag) outputFlag := cli.StringMe(flags[output]) if outputFlag != nil && *outputFlag == tableWideOutput { + // If output type is `table-wide`, cache both prices for better comparison in output, + // even if the actual filter is applied on any one of those based on usage class + // Save time by hydrating all caches in parallel if err := hydrateCaches(*instanceSelector); err != nil { log.Printf("%v", err) } + } else if strings.Contains(lowercaseField, "price") { + // if sorting based on either on-demand or spot price, ensure the appropriate cache + // has been refreshed. + if strings.Contains(lowercaseField, "ondemand") { + if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil { + log.Printf("there was a problem refreshing the on-demand pricing cache: %v", err) + } + } else if strings.Contains(lowercaseField, "spot") { + if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + log.Printf("there was a problem refreshing the spot pricing cache: %v", err) + } + } } if flags[verbose] != nil { @@ -335,6 +386,15 @@ Full docs can be found at github.com/aws/amazon-` + binName os.Exit(1) } + // sort instance types + sortDirectionFlag := cli.StringMe(flags[sortDirection]) + instanceTypeDetails, err = sortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) + //instanceTypeDetails, err = instanceSelector.SortInstanceTypes(instanceTypeDetails, sortFilterFlag, sortDirectionFlag) + if err != nil { + fmt.Printf("An error occurred when sorting instance types: %v", err) + os.Exit(1) + } + // format instance types as strings maxOutputResults := cli.IntMe(flags[maxResults]) instanceTypes, itemsTruncated, err := formatInstanceTypes(instanceTypeDetails, maxOutputResults, outputFlag) @@ -505,3 +565,48 @@ func hydrateCaches(instanceSelector selector.Selector) (errs error) { wg.Wait() return errs } + +func sortInstanceTypes(instanceTypes []*instancetypes.Details, sortField *string, sortDirection *string) ([]*instancetypes.Details, error) { + if sortField == nil { + return nil, fmt.Errorf("sortField is nil") + } else if sortDirection == nil { + return nil, fmt.Errorf("sortDirection is nil") + } + + // TODO: can check for default constants here (perhaps a switch statement with default being the json path) + // Extra: + // - If want to do constants in the way that Brandon recommended (for all quantity fields) then we can parse through + // - fields before flags are created so that we can determine the default keys for quantity fields + // - wait this wouldn't work because we don't have the instance types before the CLI flags are collected... + // - OH WAIT we can just directly look at the fields of the struct perhaps? and then just allow + // quantities to be default flags and then use the type variable name as the name of the field? + // - Maybe we can do this through reflect.Value? + // - or maybe we can make a dummy instance type to look at as a json value? + // Helpful link: https://pkg.go.dev/reflect#Value + + /* + maybe get default quantative values this way? + + v := reflect.ValueOf(x) + + values := make([]interface{}, v.NumField()) + + for i := 0; i < v.NumField(); i++ { + values[i] = v.Field(i).Interface() + } + + + // However, still need some way to get field name so that we can use that as a valid key for users + + // perhaps can have a map that maps field names (keys) to json paths so that we can pass it into + // the sorter + */ + + // TODO: check for actual json paths here + sorter, err := sorter.NewSorter(instanceTypes, *sortField, *sortDirection) + if err != nil { + return nil, err + } + sorter.Sort() + return sorter.InstanceTypes(), nil +} diff --git a/go.mod b/go.mod index 388553c..398c6d0 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect go.uber.org/atomic v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 53c0f70..7e6a59d 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= +github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/pkg/cli/sorter/sorter.go b/pkg/cli/sorter/sorter.go new file mode 100644 index 0000000..804a6c5 --- /dev/null +++ b/pkg/cli/sorter/sorter.go @@ -0,0 +1,260 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package sorter + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" + "github.com/oliveagle/jsonpath" +) + +const ( + sortAscending = "ascending" + sortAsc = "asc" + sortDescending = "descending" + sortDesc = "desc" +) + +// sorterNode represents a sortable instance type which holds the value +// to sort by instance sort +type sorterNode struct { + instanceType *instancetypes.Details + fieldValue reflect.Value +} + +// Sorter is used to sort instance types based on a sorting field +// and direction +type Sorter struct { + sorters []*sorterNode + sortField string + isDescending bool +} + +// NewSorter creates a new Sorter object to be used to sort the given instance types +// based on the sorting field and direction +// TODO: explain how sorting field is a JSON path to the appropriate Details struct property +// TODO: explain valid sort directions +// TODO: maybe make the strings pointers instead so that the CLI can pass the flags +// in directly (also can have nil checks in here and return appropriate errors) +// TODO: maybe instead of "sortField" call it something to do with "JSON path"/"path" +func NewSorter(instanceTypes []*instancetypes.Details, sortField string, sortDirection string) (*Sorter, error) { + // TODO: determine if sortField is valid. Maybe do this in newSorterNode because + // the json path library we are using already validates the sortField + + // validate direction flag and determine sorting direction + // if sortDirection == nil { + // return nil, fmt.Errorf("sort direction is nil") + // } + var isDescending bool + switch sortDirection { + case sortDescending, sortDesc: + isDescending = true + case sortAscending, sortAsc: + isDescending = false + default: + return nil, fmt.Errorf("invalid sort direction: %s (valid options: %s, %s, %s, %s)", sortDirection, sortAscending, sortAsc, sortDescending, sortDesc) + } + + // Create sorterNode objects for each instance type + sorters := []*sorterNode{} + for _, instanceType := range instanceTypes { + newSorter, err := newSorterNode(instanceType, sortField) + if err != nil { + return nil, fmt.Errorf("error creating sorting node: %v", err) + } + + sorters = append(sorters, newSorter) + } + + return &Sorter{ + sorters: sorters, + sortField: sortField, + isDescending: isDescending, + }, nil +} + +// newSorterNode creates a new sorterNode object which represents the given instance type +// and can be used in sorting of instance types based on the given sortField +func newSorterNode(instanceType *instancetypes.Details, sortField string) (*sorterNode, error) { + // TODO: figure out if there is a better way to get correct format than to + // marshal and then unmarshal instance types + + // convert instance type into json + jsonInstanceType, err := json.Marshal(instanceType) + if err != nil { + return nil, err + } + + // unmarshal json instance types in order to get proper format + // for json path parsing + var jsonData interface{} + err = json.Unmarshal(jsonInstanceType, &jsonData) + if err != nil { + return nil, err + } + + // get the desired field from the json data based on the passed in + // json path + result, err := jsonpath.JsonPathLookup(jsonData, sortField) + if err != nil { + return nil, err + } + + // TODO: + // Bug where one value might be a "float" but then another might be nil + // and we need some way to compare those two. Can't just set the value + // of float to nil and can't make nil be a pointer because then it + // will try to compare float with a nil + + // maybe just do a "if valj != reflect.float" when vali is, then treat + // case where valj is nil + // This does not work......... + + // maybe turn everything into pointers? + + // Maybe if we knew the type of the field we could use that to determine + // which of the types is correct? AKA if we have a type that is a float + // switch on that type instead of the type of valI and then in + // the case we can have an if where if the val does not equal the type, + // then it is not less (AKA it bubbles up) + + // if result == nil { + // var ptr *interface{} = nil + // result = ptr + // } + + return &sorterNode{ + instanceType: instanceType, + fieldValue: reflect.ValueOf(result), + }, nil +} + +// Sort the instance types in the Sorter based on the Sorter's sort field and +// direction +func (s *Sorter) Sort() { + sort.Sort(s) +} + +func (s *Sorter) Len() int { + return len(s.sorters) +} + +func (s *Sorter) Swap(i, j int) { + // originalI := s.sorters[i] + + // s.sorters[i] = s.sorters[j] + // s.sorters[j] = originalI + + s.sorters[i], s.sorters[j] = s.sorters[j], s.sorters[i] +} + +func (s *Sorter) Less(i, j int) bool { + valI := s.sorters[i].fieldValue + valJ := s.sorters[j].fieldValue + + fmt.Println("CALLING IS LESS") + less, _ := isLess(valI, valJ, s.isDescending) + + return less +} + +// isLess determines whether the first value (valI) is less than the +// second value (valJ) or not +func isLess(valI, valJ reflect.Value, isDescending bool) (bool, error) { + // TODO: add more types + switch valI.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + fmt.Println("=====found int type!======") + + if isDescending { + return valI.Int() > valJ.Int(), nil + } else { + return valI.Int() <= valJ.Int(), nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + fmt.Println("=====found uint type!======") + + if isDescending { + return valI.Uint() > valJ.Uint(), nil + } else { + return valI.Uint() <= valJ.Uint(), nil + } + case reflect.Float32, reflect.Float64: + fmt.Printf("=====found float type!====== values: %f, %f\n") + //fmt.Printf(" Result: %b\n", (valI.Float() < valJ.Float())) + + if valJ.Kind() != reflect.Float32 && valJ.Kind() != reflect.Float64 { + fmt.Println("valJ is not a float") + return true, nil + } + + if isDescending { + return valI.Float() > valJ.Float(), nil + } else { + return valI.Float() <= valJ.Float(), nil + } + case reflect.String: + fmt.Println("=====found string type!======") + + if isDescending { + return strings.Compare(valI.String(), valJ.String()) > 0, nil + } else { + return strings.Compare(valI.String(), valJ.String()) <= 0, nil + } + case reflect.Pointer: + fmt.Println("=====found ptr type!======") + + // Handle nil values by making non nil values always less than the nil values. That way the + // nil values can be bubbled up to the end of the list. + if valI.IsNil() { + return false, nil + } else if valJ.Kind() == reflect.Pointer && valJ.IsNil() { + return true, nil + } + + return isLess(valI.Elem(), valJ.Elem(), isDescending) + case reflect.Invalid: + // handle invalid values (like nil values) by making valid values + // always less than the invalid values. That way the invalid values + // always bubble up to the end of the list + return false, nil + default: + fmt.Printf("====unsortable type!==== %v\n", valI.Kind()) + + // TODO: log that an unsortable type has been passed + + // handle unsortable values (like nil values) by making sortable values + // always less than the unsortable values. That way the unsortable values + // always bubble up to the end of the list + return false, nil + } + + // TODO: add bool types +} + +// InstanceTypes returns the list of instance types held in the Sorter +func (s *Sorter) InstanceTypes() []*instancetypes.Details { + instanceTypes := []*instancetypes.Details{} + + for _, node := range s.sorters { + instanceTypes = append(instanceTypes, node.instanceType) + } + + return instanceTypes +} diff --git a/pkg/instancetypes/instancetypes.go b/pkg/instancetypes/instancetypes.go index 4e83a84..dad0e24 100644 --- a/pkg/instancetypes/instancetypes.go +++ b/pkg/instancetypes/instancetypes.go @@ -36,7 +36,7 @@ var ( // Details hold all the information on an ec2 instance type type Details struct { ec2.InstanceTypeInfo - OndemandPricePerHour *float64 + OnDemandPricePerHour *float64 SpotPrice *float64 } diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index 61d5828..a46defe 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -164,8 +164,8 @@ func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string { onDemandPricePerHourStr := "-Not Fetched-" spotPricePerHourStr := "-Not Fetched-" - if instanceTypeInfo.OndemandPricePerHour != nil { - onDemandPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.OndemandPricePerHour)) + if instanceTypeInfo.OnDemandPricePerHour != nil { + onDemandPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.OnDemandPricePerHour)) } if instanceTypeInfo.SpotPrice != nil { spotPricePerHourStr = fmt.Sprintf("$%s", formatFloat(*instanceTypeInfo.SpotPrice)) diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index d30beb1..c62b611 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -42,7 +42,7 @@ func getInstanceTypes(t *testing.T, file string) []*instancetypes.Details { instanceTypesDetails := []*instancetypes.Details{} for _, it := range dito.InstanceTypes { odPrice := float64(0.53) - instanceTypesDetails = append(instanceTypesDetails, &instancetypes.Details{InstanceTypeInfo: *it, OndemandPricePerHour: &odPrice}) + instanceTypesDetails = append(instanceTypesDetails, &instancetypes.Details{InstanceTypeInfo: *it, OnDemandPricePerHour: &odPrice}) } return instanceTypesDetails } @@ -146,7 +146,7 @@ func TestTruncateResults(t *testing.T) { h.Assert(t, numTrucated == 0, fmt.Sprintf("Should truncate 0 results, but actually truncated: %d results", numTrucated)) } -func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { +func TestTruncateResults_NegativeMaxResults(t *testing.T) { instanceTypes := getInstanceTypes(t, "25_instances.json") maxResults := aws.Int(-1) @@ -157,7 +157,7 @@ func TestFormatInstanceTypes_NegativeMaxResults(t *testing.T) { h.Assert(t, numTrucated == 0, fmt.Sprintf("No results should be truncated, but %d results were truncated", numTrucated)) } -func TestFormatInstanceTypes_NilMaxResults(t *testing.T) { +func TestTrucateResults_NilMaxResults(t *testing.T) { instanceTypes := getInstanceTypes(t, "25_instances.json") var maxResults *int = nil diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index f20d8f2..4052521 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -99,6 +99,17 @@ const ( virtualizationTypePV = "pv" pricePerHour = "pricePerHour" + + // Sorting flags + + sortODPrice = "on-demand-price" + sortSpotPrice = "spot-price" + sortVCPU = "vcpu" + sortMemory = "memory" + sortName = "instance-type-name" + + sortAscending = "ascending" + sortDescending = "descending" ) // New creates an instance of Selector provided an aws session @@ -201,20 +212,149 @@ func (itf Selector) FilterInstanceTypes(filters Filters) ([]*instancetypes.Detai filteredInstanceTypes = append(filteredInstanceTypes, it) } - return sortInstanceTypeInfo(filteredInstanceTypes), nil + return filteredInstanceTypes, nil +} + +// validateSortFilter determines whether the given sortFilter matches +// one of the valid filter flags. +func validateSortFilter(sortFilter *string) error { + if sortFilter == nil { + return fmt.Errorf("sort filter is nil") + } + + validFlags := []string{ + sortODPrice, + sortSpotPrice, + sortVCPU, + sortMemory, + sortName, + } + + for _, flag := range validFlags { + if *sortFilter == flag { + return nil + } + } + + return fmt.Errorf("invalid sort filter: %s. (valid options: %s)", *sortFilter, strings.Join(validFlags, ", ")) } -// sortInstanceTypeInfo will sort based on instance type info alpha-numerically -func sortInstanceTypeInfo(instanceTypeInfoSlice []*instancetypes.Details) []*instancetypes.Details { - if len(instanceTypeInfoSlice) < 2 { - return instanceTypeInfoSlice +// SortInstanceTypes accepts a list of instance type details, a sort filter flag, and a sort direction flag and +// returns a list of instance type details sorted based on the sort filter and sort direction. +// Sort filter options: "on-demand-price", "spot-price", "vcpu", "memory", "instance-type-name". +// Sort direction options: "ascending", "descending". +func (itf Selector) SortInstanceTypes(instanceTypes []*instancetypes.Details, sortFilter *string, sortDirection *string) ([]*instancetypes.Details, error) { + if len(instanceTypes) <= 1 { + return instanceTypes, nil + } + + // validate direction flag and determine sorting direction + if sortDirection == nil { + return nil, fmt.Errorf("sort direction is nil") + } + var isDescending bool + switch *sortDirection { + case sortDescending: + isDescending = true + case sortAscending: + isDescending = false + default: + return nil, fmt.Errorf("invalid sort direction: %s (valid options: %s, %s)", *sortDirection, sortAscending, sortDescending) + } + + // validate filter flag + if err := validateSortFilter(sortFilter); err != nil { + return nil, err + } + + // if sorting based on either on-demand or spot price, ensure the appropriate cache + // has been refreshed. + if *sortFilter == sortODPrice { + if err := itf.EC2Pricing.RefreshOnDemandCache(); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the on-demand pricing cache: %v", err) + } + } else if *sortFilter == sortSpotPrice { + if err := itf.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil { + return nil, fmt.Errorf("there was a problem refreshing the spot pricing cache: %v", err) + } } - sort.Slice(instanceTypeInfoSlice, func(i, j int) bool { - iInstanceInfo := instanceTypeInfoSlice[i] - jInstanceInfo := instanceTypeInfoSlice[j] - return strings.Compare(aws.StringValue(iInstanceInfo.InstanceType), aws.StringValue(jInstanceInfo.InstanceType)) <= 0 + + // sort instance types based on filter flag and isDescending + sort.Slice(instanceTypes, func(i, j int) bool { + firstType := instanceTypes[i] + secondType := instanceTypes[j] + + // Determine which value to sort by. + // Handle nil values by making non nil values always less than the nil values. That way the + // nil values can be bubbled up to the end of the list. + switch *sortFilter { + case sortODPrice: + if firstType.OnDemandPricePerHour == nil { + return false + } else if secondType.OnDemandPricePerHour == nil { + return true + } + + if isDescending { + return *firstType.OnDemandPricePerHour > *secondType.OnDemandPricePerHour + } else { + return *firstType.OnDemandPricePerHour <= *secondType.OnDemandPricePerHour + } + case sortSpotPrice: + if firstType.SpotPrice == nil { + return false + } else if secondType.SpotPrice == nil { + return true + } + + if isDescending { + return *firstType.SpotPrice > *secondType.SpotPrice + } else { + return *firstType.SpotPrice <= *secondType.SpotPrice + } + case sortVCPU: + if firstType.VCpuInfo == nil || firstType.VCpuInfo.DefaultVCpus == nil { + return false + } else if secondType.VCpuInfo == nil || secondType.VCpuInfo.DefaultVCpus == nil { + return true + } + + if isDescending { + return *firstType.VCpuInfo.DefaultVCpus > *secondType.VCpuInfo.DefaultVCpus + } else { + return *firstType.VCpuInfo.DefaultVCpus <= *secondType.VCpuInfo.DefaultVCpus + } + case sortMemory: + if firstType.MemoryInfo == nil || firstType.MemoryInfo.SizeInMiB == nil { + return false + } else if secondType.MemoryInfo == nil || secondType.MemoryInfo.SizeInMiB == nil { + return true + } + + if isDescending { + return *firstType.MemoryInfo.SizeInMiB > *secondType.MemoryInfo.SizeInMiB + } else { + return *firstType.MemoryInfo.SizeInMiB <= *secondType.MemoryInfo.SizeInMiB + } + case sortName: + if firstType.InstanceType == nil { + return false + } else if secondType.InstanceType == nil { + return true + } + + if isDescending { + return strings.Compare(aws.StringValue(secondType.InstanceType), aws.StringValue(firstType.InstanceType)) <= 0 + } else { + return strings.Compare(aws.StringValue(firstType.InstanceType), aws.StringValue(secondType.InstanceType)) <= 0 + } + default: + // invalid sorting flag. Do not sort. + return true + } }) - return instanceTypeInfoSlice + + return instanceTypes, nil } // AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. @@ -246,7 +386,7 @@ func (itf Selector) prepareFilter(filters Filters, instanceTypeInfo instancetype log.Printf("Could not retrieve instantaneous hourly on-demand price for instance type %s\n", instanceTypeName) } else { instanceTypeHourlyPriceOnDemand = &price - instanceTypeInfo.OndemandPricePerHour = instanceTypeHourlyPriceOnDemand + instanceTypeInfo.OnDemandPricePerHour = instanceTypeHourlyPriceOnDemand } } if itf.EC2Pricing.SpotCacheCount() > 0 && contains(instanceTypeInfo.SupportedUsageClasses, "spot") { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index d77f3a8..4d618e9 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -20,11 +20,13 @@ import ( "io/ioutil" "regexp" "strconv" + "strings" "testing" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" @@ -471,6 +473,234 @@ func TestFilterInstanceTypes_PricePerHour_Spot(t *testing.T) { h.Assert(t, len(results) == 1, fmt.Sprintf("Should return 1 instance type; got %d", len(results))) } +// checkSortResults is a helper function for comparing the results of sorting tests. Returns true if +// the order of instance types in the instanceTypes list matches the the order of instance type names +// in the expectedResult list, and returns false otherwise. +func checkSortResults(instanceTypes []*instancetypes.Details, expectedResult []string) bool { + if len(instanceTypes) != len(expectedResult) { + return false + } + + for i := 0; i < len(instanceTypes); i++ { + actualName := instanceTypes[i].InstanceTypeInfo.InstanceType + expectedName := expectedResult[i] + + if actualName == nil || *actualName != expectedName { + return false + } + } + + return true +} + +func TestSortInstanceTypes_OneElement(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "t3_micro.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := "instance-type-name" + sortDirection := "ascending" + results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Ok(t, err) + h.Assert(t, len(results) == 1, fmt.Sprintf("Should only return one element, but it returned: %d elements", len(results))) +} + +func TestSortInstanceTypes_Emptylist(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "empty.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := "instance-type-name" + sortDirection := "ascending" + results, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Ok(t, err) + h.Assert(t, len(results) == 0, fmt.Sprintf("Should return empty list, but it returned: %d elements", len(results))) +} + +func TestSortInstanceTypes_Name(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := "instance-type-name" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.2xlarge", "a1.4xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = "descending" + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.large", "a1.4xlarge", "a1.2xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_Memory(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := "memory" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = "descending" + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_Vcpu(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // test ascending + sortFilter := "vcpu" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = "descending" + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_SpotPrice(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // add spot prices + prices := make(map[string]*float64) + price1, price2, price3 := 1.0, 2.0, 3.0 + prices["a1.large"] = &price1 + prices["a1.2xlarge"] = &price2 + prices["a1.4xlarge"] = &price3 + for _, v := range results { + name := v.InstanceTypeInfo.InstanceType + v.SpotPrice = prices[*name] + } + + // test ascending + sortFilter := "spot-price" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // test descending + sortDirection = "descending" + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_OnDemandPrice(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + // add on demand prices + prices := make(map[string]*float64) + price1, price2, price3 := 1.0, 2.0, 3.0 + prices["a1.large"] = &price1 + prices["a1.2xlarge"] = &price2 + prices["a1.4xlarge"] = &price3 + for _, v := range results { + name := v.InstanceTypeInfo.InstanceType + v.OnDemandPricePerHour = prices[*name] + } + + // sort ascending + sortFilter := "on-demand-price" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults := []string{"a1.large", "a1.2xlarge", "a1.4xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) + + // sort descending + sortDirection = "descending" + sortedResults, err = itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + expectedResults = []string{"a1.4xlarge", "a1.2xlarge", "a1.large"} + + h.Ok(t, err) + h.Assert(t, len(sortedResults) == len(results), fmt.Sprintf("Should return %d elements, but returned %d", len(results), len(sortedResults))) + h.Assert(t, checkSortResults(sortedResults, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_InvalidFilter(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := "blah blah blah" + sortDirection := "ascending" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedResults == nil, fmt.Sprintf("Returned instance types details should be nil, but instead got: %s", outputs.OneLineOutput(sortedResults))) +} + +func TestSortInstanceTypes_InvalidDirection(t *testing.T) { + itf := getSelector(setupMock(t, describeInstanceTypesPages, "3_instances.json")) + filters := selector.Filters{} + results, err := itf.FilterInstanceTypes(filters) + h.Ok(t, err) + + sortFilter := "instance-type-name" + sortDirection := "fdsfds" + sortedResults, err := itf.SortInstanceTypes(results, &sortFilter, &sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedResults == nil, fmt.Sprintf("Returned instance types details should be nil, but instead got: %s", outputs.OneLineOutput(sortedResults))) +} + func TestRetrieveInstanceTypesSupportedInAZ_WithZoneName(t *testing.T) { ec2Mock := setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json") ec2Mock.DescribeAvailabilityZonesResp = setupMock(t, describeAvailabilityZones, "us-east-2.json").DescribeAvailabilityZonesResp diff --git a/test/static/DescribeInstanceTypesPages/3_instances.json b/test/static/DescribeInstanceTypesPages/3_instances.json new file mode 100644 index 0000000..5753226 --- /dev/null +++ b/test/static/DescribeInstanceTypesPages/3_instances.json @@ -0,0 +1,187 @@ +{ + "InstanceTypes": [ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": [ + 8 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.4xlarge", + "MemoryInfo": { + "SizeInMiB": 32768 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 8, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 16, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 16, + "ValidCores": [ + 16 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.large", + "MemoryInfo": { + "SizeInMiB": 4096 + }, + "NetworkInfo": { + "EnaSupport": "required", + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 10, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 3, + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/static/DescribeInstanceTypesPages/empty.json b/test/static/DescribeInstanceTypesPages/empty.json new file mode 100644 index 0000000..9341a66 --- /dev/null +++ b/test/static/DescribeInstanceTypesPages/empty.json @@ -0,0 +1,3 @@ +{ + "InstanceTypes": [] +} \ No newline at end of file