diff --git a/README.md b/README.md index af62940..aea2309 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,140 @@ t3.medium 2 4 nitro true true t3a.medium 2 4 nitro true true x86_64 Up to 5 Gigabit 3 0 0 $0.0376 $0.01431 ``` +**Sort by memory in ascending order using shorthand** +``` +$ ec2-instance-selector -r us-east-1 -o table-wide --max-results 10 --sort-by memory --sort-direction asc +Instance Type VCPUs Mem (GiB) Hypervisor Current Gen Hibernation Support CPU Arch Network Performance ENIs GPUs GPU Mem (GiB) GPU Info On-Demand Price/Hr Spot Price/Hr (30d avg) +------------- ----- --------- ---------- ----------- ------------------- -------- ------------------- ---- ---- ------------- -------- ------------------ ----------------------- +t2.nano 1 0.5 xen true true i386, x86_64 Low to Moderate 2 0 0 $0.0058 -Not Fetched- +t4g.nano 2 0.5 nitro true false arm64 Up to 5 Gigabit 2 0 0 $0.0042 $0.0013 +t3a.nano 2 0.5 nitro true true x86_64 Up to 5 Gigabit 2 0 0 $0.0047 $0.00178 +t3.nano 2 0.5 nitro true true x86_64 Up to 5 Gigabit 2 0 0 $0.0052 $0.0016 +t1.micro 1 0.6123 xen false false i386, x86_64 Very Low 2 0 0 $0.02 $0.00213 +t3a.micro 2 1 nitro true true x86_64 Up to 5 Gigabit 2 0 0 $0.0094 $0.00332 +t3.micro 2 1 nitro true true x86_64 Up to 5 Gigabit 2 0 0 $0.0104 $0.0031 +t2.micro 1 1 xen true true i386, x86_64 Low to Moderate 2 0 0 $0.0116 $0.0035 +t4g.micro 2 1 nitro true false arm64 Up to 5 Gigabit 2 0 0 $0.0084 $0.0025 +m1.small 1 1.69922 xen false false i386, x86_64 Low 2 0 0 $0.044 $0.00865 +NOTE: 547 entries were truncated, increase --max-results to see more +``` +Available shorthand flags: vcpus, memory, gpu-memory-total, network-interfaces, spot-price, on-demand-price, instance-storage, ebs-optimized-baseline-bandwidth, ebs-optimized-baseline-throughput, ebs-optimized-baseline-iops, gpus, inference-accelerators + +**Sort by memory in descending order using JSON path** +``` +$ ec2-instance-selector -r us-east-1 -o table-wide --max-results 10 --sort-by .MemoryInfo.SizeInMiB --sort-direction desc +Instance Type VCPUs Mem (GiB) Hypervisor Current Gen Hibernation Support CPU Arch Network Performance ENIs GPUs GPU Mem (GiB) GPU Info On-Demand Price/Hr Spot Price/Hr (30d avg) +------------- ----- --------- ---------- ----------- ------------------- -------- ------------------- ---- ---- ------------- -------- ------------------ ----------------------- +u-12tb1.112xlarge 448 12,288 nitro true false x86_64 100 Gigabit 15 0 0 $109.2 -Not Fetched- +u-9tb1.112xlarge 448 9,216 nitro true false x86_64 100 Gigabit 15 0 0 $81.9 -Not Fetched- +u-6tb1.112xlarge 448 6,144 nitro true false x86_64 100 Gigabit 15 0 0 $54.6 -Not Fetched- +u-6tb1.56xlarge 224 6,144 nitro true false x86_64 100 Gigabit 15 0 0 $46.40391 -Not Fetched- +x2iedn.metal 128 4,096 none true false x86_64 100 Gigabit 15 0 0 $26.676 $8.0028 +x2iedn.32xlarge 128 4,096 nitro true false x86_64 100 Gigabit 15 0 0 $26.676 $8.0028 +x1e.32xlarge 128 3,904 xen true false x86_64 25 Gigabit 8 0 0 $26.688 $8.03461 +x2iedn.24xlarge 96 3,072 nitro true false x86_64 75 Gigabit 15 0 0 $20.007 $13.23032 +u-3tb1.56xlarge 224 3,072 nitro true false x86_64 50 Gigabit 8 0 0 $27.3 -Not Fetched- +x2idn.metal 128 2,048 none true false x86_64 100 Gigabit 15 0 0 $13.338 $4.67017 +NOTE: 547 entries were truncated, increase --max-results to see more +``` +JSON path must point to a field in the [instancetype.Details struct](https://github.com/aws/amazon-ec2-instance-selector/blob/5bffbf2750ee09f5f1308bdc8d4b635a2c6e2721/pkg/instancetypes/instancetypes.go#L37). + +**Example output of instance type object using Verbose output** +``` +$ ec2-instance-selector --max-results 1 -v +[ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1750, + "BaselineIops": 10000, + "BaselineThroughputInMBps": 218.75, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 4, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 4, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": null, + "ValidThreadsPerCore": null + }, + "OndemandPricePerHour": 0.204, + "SpotPrice": 0.03939999999999999 + } +] +NOTE: 497 entries were truncated, increase --max-results to see more +``` +NOTE: Use this JSON format as reference when finding JSON paths for sorting + **All CLI Options** ``` @@ -153,7 +287,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 @@ -241,15 +375,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) - --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) + --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-by string Specify the field to sort by. Quantity flags present in this CLI (memory, gpus, etc.) or a JSON path to the appropriate instance type field (Ex: ".MemoryInfo.SizeInMiB") is acceptable. (default ".InstanceType") + --sort-direction string Specify the direction to sort in (ascending, asc, descending, desc) (default "ascending") + -v, --verbose Verbose - will print out full instance specs + --version Prints CLI version ``` diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index 8fcb97b..2e69ed9 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -853,4 +853,31 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. + +------ + +** github.com/oliveagle/jsonpath; version v0.0.0-20180606110733-2e52cf6e6852 -- +https://github.com/oliveagle/jsonpath + +The MIT License (MIT) + +Copyright (c) 2015 oliver + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index f57e56d..e304a7f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,8 +25,10 @@ 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/amazon-ec2-instance-selector/v2/pkg/sorter" "github.com/aws/aws-sdk-go/aws/session" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" @@ -103,15 +105,44 @@ 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" +) + +// Sorting Constants +const ( + // Direction + + sortAscending = "ascending" + sortAsc = "asc" + sortDescending = "descending" + sortDesc = "desc" + + // Sorting Fields + spotPrice = "spot-price" + odPrice = "on-demand-price" + + // JSON field paths + instanceNamePath = ".InstanceType" + vcpuPath = ".VCpuInfo.DefaultVCpus" + memoryPath = ".MemoryInfo.SizeInMiB" + gpuMemoryTotalPath = ".GpuInfo.TotalGpuMemoryInMiB" + networkInterfacesPath = ".NetworkInfo.MaximumNetworkInterfaces" + spotPricePath = ".SpotPrice" + odPricePath = ".OndemandPricePerHour" + instanceStoragePath = ".InstanceStorageInfo.TotalSizeInGB" + ebsOptimizedBaselineBandwidthPath = ".EbsInfo.EbsOptimizedInfo.BaselineBandwidthInMbps" + ebsOptimizedBaselineThroughputPath = ".EbsInfo.EbsOptimizedInfo.BaselineThroughputInMBps" + ebsOptimizedBaselineIOPSPath = ".EbsInfo.EbsOptimizedInfo.BaselineIops" ) var ( @@ -142,6 +173,29 @@ Full docs can be found at github.com/aws/amazon-` + binName } resultsOutputFn := outputs.SimpleInstanceTypeOutput + cliSortDirections := []string{ + sortAscending, + sortAsc, + sortDescending, + sortDesc, + } + + // map quantity cli flags to json paths for easier cli sorting + sortingKeysMap := map[string]string{ + vcpus: vcpuPath, + memory: memoryPath, + gpuMemoryTotal: gpuMemoryTotalPath, + networkInterfaces: networkInterfacesPath, + spotPrice: spotPricePath, + odPrice: odPricePath, + instanceStorage: instanceStoragePath, + ebsOptimizedBaselineBandwidth: ebsOptimizedBaselineBandwidthPath, + ebsOptimizedBaselineThroughput: ebsOptimizedBaselineThroughputPath, + ebsOptimizedBaselineIOPS: ebsOptimizedBaselineIOPSPath, + gpus: gpus, + inferenceAccelerators: inferenceAccelerators, + } + // Registers flags with specific input types from the cli pkg // Filter Flags - These will be grouped at the top of the help flags @@ -206,6 +260,8 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") + cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) + cli.ConfigStringFlag(sortBy, nil, cli.StringMe(instanceNamePath), "Specify the field to sort by. Quantity flags present in this CLI (memory, gpus, etc.) or a JSON path to the appropriate instance type field (Ex: \".MemoryInfo.SizeInMiB\") is acceptable.", nil) // Parses the user input with the registered flags and runs type specific validation on the user input flags, err := cli.ParseAndValidateFlags() @@ -237,6 +293,9 @@ Full docs can be found at github.com/aws/amazon-` + binName } } registerShutdown(shutdown) + + sortField := cli.StringMe(flags[sortBy]) + lowercaseSortField := strings.ToLower(*sortField) outputFlag := cli.StringMe(flags[output]) if outputFlag != nil && *outputFlag == tableWideOutput { // If output type is `table-wide`, simply print both prices for better comparison, @@ -245,18 +304,37 @@ Full docs can be found at github.com/aws/amazon-` + binName if err := hydrateCaches(*instanceSelector); err != nil { log.Printf("%v", err) } - } else if flags[pricePerHour] != nil { + } else { // 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) + if flags[pricePerHour] != nil { + 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) + } } } - } 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) + } + + // refresh appropriate caches if sorting by either spot or on demand pricing + if strings.Contains(lowercaseSortField, "price") { + if strings.Contains(lowercaseSortField, "spot") { + 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) + } + } + } else { + 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) + } } } } @@ -338,16 +416,52 @@ Full docs can be found at github.com/aws/amazon-` + binName } } + // determine if user used a shorthand for sorting flag + if sortFieldShorthandPath, ok := sortingKeysMap[*sortField]; ok { + sortField = &sortFieldShorthandPath + } + outputFn := getOutputFn(outputFlag, selector.InstanceTypesOutputFn(resultsOutputFn)) + var instanceTypes []string + var itemsTruncated int - instanceTypes, itemsTruncated, err := instanceSelector.FilterWithOutput(filters, outputFn) - if err != nil { - fmt.Printf("An error occurred when filtering 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) + sortDirection := cli.StringMe(flags[sortDirection]) + if *sortField == instanceNamePath && (*sortDirection == sortAscending || *sortDirection == sortAsc) { + // filter already sorts in ascending order by name + instanceTypes, itemsTruncated, err = instanceSelector.FilterWithOutput(filters, outputFn) + if err != nil { + fmt.Printf("An error occurred when filtering 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) + } + } else { + // fetch instance types without truncating results + prevMaxResults := filters.MaxResults + filters.MaxResults = nil + instanceTypeDetails, err := instanceSelector.FilterVerbose(filters) + if err != nil { + fmt.Printf("An error occurred when filtering instance types: %v", err) + os.Exit(1) + } + + instanceTypeDetails, err = sorter.Sort(instanceTypeDetails, *sortField, *sortDirection) + if err != nil { + fmt.Printf("Sorting error: %v", err) + os.Exit(1) + } + + // truncate instance types based on user passed in maxResults + instanceTypeDetails, itemsTruncated = truncateResults(prevMaxResults, instanceTypeDetails) + if len(instanceTypeDetails) == 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) + } + + // format instance types for output + instanceTypes = outputFn(instanceTypeDetails) } for _, instanceType := range instanceTypes { @@ -487,3 +601,14 @@ func registerShutdown(shutdown func()) { shutdown() }() } + +func 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 +} diff --git a/go.mod b/go.mod index 9175af9..36ec5bd 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 c9d5e83..3e0d1bb 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/sorter/sorter.go b/pkg/sorter/sorter.go new file mode 100644 index 0000000..62e067e --- /dev/null +++ b/pkg/sorter/sorter.go @@ -0,0 +1,336 @@ +// 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/aws/aws-sdk-go/aws" + "github.com/oliveagle/jsonpath" +) + +const ( + // Sort direction + + sortAscending = "ascending" + sortAsc = "asc" + sortDescending = "descending" + sortDesc = "desc" + + // Not all fields can be reached through a json path (Ex: gpu count) + // so we have special flags for such cases. + + gpuCountField = "gpus" + inferenceAcceleratorsField = "inference-accelerators" +) + +// 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 +} + +// Sort sorts the given instance types by the given field in the given direction +// +// sortField is a json path to a field in the instancetypes.Details struct which represents +// the field to sort instance types by (Ex: ".MemoryInfo.SizeInMiB"). +// +// sortDirection represents the direction to sort in. Valid options: "ascending", "asc", "descending", "desc". +func Sort(instanceTypes []*instancetypes.Details, sortField string, sortDirection string) ([]*instancetypes.Details, error) { + sorter, err := newSorter(instanceTypes, sortField, sortDirection) + if err != nil { + return nil, fmt.Errorf("an error occurred when preparing to sort instance types: %v", err) + } + + if err := sorter.sort(); err != nil { + return nil, fmt.Errorf("an error occurred when sorting instance types: %v", err) + } + + return sorter.instanceTypes(), nil +} + +// newSorter creates a new Sorter object to be used to sort the given instance types +// based on the sorting field and direction +// +// sortField is a json path to a field in the instancetypes.Details struct which represents +// the field to sort instance types by (Ex: ".MemoryInfo.SizeInMiB"). +// +// sortDirection represents the direction to sort in. Valid options: "ascending", "asc", "descending", "desc". +func newSorter(instanceTypes []*instancetypes.Details, sortField string, sortDirection string) (*sorter, error) { + 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) + } + + sortField = formatSortField(sortField) + + // 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 +} + +// formatSortField reformats sortField to match the expected json path format +// of the json lookup library. Format is unchanged if the sorting field +// matches one of the special flags. +func formatSortField(sortField string) string { + // check to see if the sorting field matched one of the special exceptions + if sortField == gpuCountField || sortField == inferenceAcceleratorsField { + return sortField + } + + return "$" + sortField +} + +// 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) { + // some important fields (such as gpu count) can not be accessed directly in the instancetypes.Details + // struct, so we have special hard-coded flags to handle such cases + switch sortField { + case gpuCountField: + gpuCount := getTotalGpusCount(instanceType) + return &sorterNode{ + instanceType: instanceType, + fieldValue: reflect.ValueOf(gpuCount), + }, nil + case inferenceAcceleratorsField: + acceleratorsCount := getTotalAcceleratorsCount(instanceType) + return &sorterNode{ + instanceType: instanceType, + fieldValue: reflect.ValueOf(acceleratorsCount), + }, nil + } + + // 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 { + // handle case where parent objects in path are null + // by setting result to nil + if err.Error() == "get attribute from null object" { + result = nil + } else { + return nil, fmt.Errorf("error during json path lookup: %v", err) + } + } + + 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() error { + if len(s.sorters) <= 1 { + return nil + } + + var sortErr error = nil + + sort.Slice(s.sorters, func(i int, j int) bool { + valI := s.sorters[i].fieldValue + valJ := s.sorters[j].fieldValue + + less, err := isLess(valI, valJ, s.isDescending) + if err != nil { + sortErr = err + } + + return less + }) + + return sortErr +} + +// 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) { + switch valI.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // if valJ is not an int (can occur if the other value is nil) + // then valI is less. This will bubble invalid values to the end + vaJKind := valJ.Kind() + if vaJKind != reflect.Int && vaJKind != reflect.Int8 && vaJKind != reflect.Int16 && vaJKind != reflect.Int32 && vaJKind != reflect.Int64 { + return true, nil + } + + 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: + // if valJ is not a uint (can occur if the other value is nil) + // then valI is less. This will bubble invalid values to the end + vaJKind := valJ.Kind() + if vaJKind != reflect.Uint && vaJKind != reflect.Uint8 && vaJKind != reflect.Uint16 && vaJKind != reflect.Uint32 && vaJKind != reflect.Uint64 { + return true, nil + } + + if isDescending { + return valI.Uint() > valJ.Uint(), nil + } else { + return valI.Uint() <= valJ.Uint(), nil + } + case reflect.Float32, reflect.Float64: + // if valJ is not a float (can occur if the other value is nil) + // then valI is less. This will bubble invalid values to the end + vaJKind := valJ.Kind() + if vaJKind != reflect.Float32 && vaJKind != reflect.Float64 { + return true, nil + } + + if isDescending { + return valI.Float() > valJ.Float(), nil + } else { + return valI.Float() <= valJ.Float(), nil + } + case reflect.String: + // if valJ is not a string (can occur if the other value is nil) + // then valI is less. This will bubble invalid values to the end + if valJ.Kind() != reflect.String { + return true, nil + } + + if isDescending { + return strings.Compare(valI.String(), valJ.String()) > 0, nil + } else { + return strings.Compare(valI.String(), valJ.String()) <= 0, nil + } + case reflect.Pointer: + // 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.Bool: + // if valJ is not a bool (can occur if the other value is nil) + // then valI is less. This will bubble invalid values to the end + if valJ.Kind() != reflect.Bool { + return true, nil + } + + if isDescending { + return !valI.Bool(), nil + } else { + return valI.Bool(), nil + } + 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: + // unsortable value + return false, fmt.Errorf("unsortable value") + } +} + +// 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 +} + +// helper functions for special sorting fields + +// getTotalGpusCount calculates the number of gpus in the given instance type +func getTotalGpusCount(instanceType *instancetypes.Details) *int64 { + gpusInfo := instanceType.GpuInfo + + if gpusInfo == nil { + return nil + } + + total := aws.Int64(0) + for _, gpu := range gpusInfo.Gpus { + total = aws.Int64(*total + *gpu.Count) + } + + return total +} + +// getTotalAcceleratorsCount calculates the total number of inference accelerators +// in the given instance type +func getTotalAcceleratorsCount(instanceType *instancetypes.Details) *int64 { + acceleratorInfo := instanceType.InferenceAcceleratorInfo + + if acceleratorInfo == nil { + return nil + } + + total := aws.Int64(0) + for _, accel := range acceleratorInfo.Accelerators { + total = aws.Int64(*total + *accel.Count) + } + + return total +} diff --git a/pkg/sorter/sorter_test.go b/pkg/sorter/sorter_test.go new file mode 100644 index 0000000..776cf85 --- /dev/null +++ b/pkg/sorter/sorter_test.go @@ -0,0 +1,323 @@ +// 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_test + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/sorter" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" +) + +const ( + mockFilesPath = "../../test/static" + describeInstanceTypesPages = "DescribeInstanceTypesPages" +) + +// Helpers + +// getInstanceTypeDetails unmarshalls the json file in the given testing folder +// and returns a list of instance type details +func getInstanceTypeDetails(t *testing.T, file string) []*instancetypes.Details { + folder := "FilterVerbose" + mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, folder, file) + mockFile, err := ioutil.ReadFile(mockFilename) + h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) + + instanceTypes := []*instancetypes.Details{} + err = json.Unmarshal(mockFile, &instanceTypes) + h.Assert(t, err == nil, fmt.Sprintf("Error parsing mock json file contents %s. Error: %v", mockFilename, err)) + return instanceTypes +} + +// 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 +} + +// Tests + +func TestSort_JSONPath(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + sortField := ".EbsInfo.EbsOptimizedInfo.BaselineBandwidthInMbps" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "a1.large", + "a1.2xlarge", + "a1.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_SpecialCases(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "4_special_cases.json") + + // test gpus flag + sortField := "gpus" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "g3.4xlarge", + "g3.16xlarge", + "inf1.24xlarge", + "inf1.2xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected gpus order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) + + // test inference accelerators flag + sortField = "inference-accelerators" + + sortedInstances, err = sorter.Sort(instanceTypes, sortField, sortDirection) + + expectedResults = []string{ + "inf1.2xlarge", + "inf1.24xlarge", + "g3.16xlarge", + "g3.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected inference accelerators order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_OneElement(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "1_instance.json") + + sortField := ".MemoryInfo.SizeInMiB" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{"a1.2xlarge"} + + h.Ok(t, err) + h.Assert(t, len(sortedInstances) == 1, fmt.Sprintf("Should only have 1 instance, but have: %d", len(sortedInstances))) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_EmptyList(t *testing.T) { + instanceTypes := []*instancetypes.Details{} + + sortField := ".MemoryInfo.SizeInMiB" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + + h.Ok(t, err) + h.Assert(t, len(sortedInstances) == 0, fmt.Sprintf("Sorted instance types list should be empty but actually has %d elements", len(sortedInstances))) +} + +func TestSort_InvalidSortField(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + sortField := "fdsafdsafdjskalfjlsf #@" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedInstances == nil, "Returned sorter should be nil") +} + +func TestSort_InvalidDirection(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + sortField := ".MemoryInfo.SizeInMiB" + sortDirection := "fdsa hfd j2 $#21" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedInstances == nil, "Returned sorter should be nil") +} + +func TestSort_Number(t *testing.T) { + // All numbers (ints and floats) are evaluated as floats + // due to the way that json unmarshalling must be done + // in order to match json path library input format + + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + // test ascending + sortField := ".MemoryInfo.SizeInMiB" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "a1.large", + "a1.2xlarge", + "a1.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) + + // test descending + sortDirection = "desc" + + sortedInstances, err = sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults = []string{ + "a1.4xlarge", + "a1.2xlarge", + "a1.large", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_String(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + // test ascending + sortField := ".InstanceType" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "a1.2xlarge", + "a1.4xlarge", + "a1.large", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) + + // test descending + sortDirection = "desc" + + sortedInstances, err = sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults = []string{ + "a1.large", + "a1.4xlarge", + "a1.2xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_Invalid(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + // test ascending + sortField := ".SpotPrice" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "a1.large", + "a1.2xlarge", + "a1.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) + + // test descending + sortDirection = "desc" + + sortedInstances, err = sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults = []string{ + "a1.2xlarge", + "a1.large", + "a1.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} + +func TestSort_Unsortable(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + sortField := ".NetworkInfo" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedInstances == nil, "returned instances list should be nil") +} + +func TestSort_Pointer(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + sortField := ".EbsInfo" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + + h.Assert(t, err != nil, "An error should be returned") + h.Assert(t, sortedInstances == nil, "returned instances list should be nil") +} + +func TestSort_Bool(t *testing.T) { + instanceTypes := getInstanceTypeDetails(t, "3_instances.json") + + // test ascending + sortField := ".HibernationSupported" + sortDirection := "asc" + + sortedInstances, err := sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults := []string{ + "a1.4xlarge", + "a1.2xlarge", + "a1.large", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected ascending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) + + // test descending + sortDirection = "desc" + + sortedInstances, err = sorter.Sort(instanceTypes, sortField, sortDirection) + expectedResults = []string{ + "a1.large", + "a1.2xlarge", + "a1.4xlarge", + } + + h.Ok(t, err) + h.Assert(t, checkSortResults(sortedInstances, expectedResults), fmt.Sprintf("Expected descending order: [%s], but actual order: %s", strings.Join(expectedResults, ","), outputs.OneLineOutput(sortedInstances))) +} diff --git a/test/static/FilterVerbose/1_instance.json b/test/static/FilterVerbose/1_instance.json new file mode 100644 index 0000000..86257fb --- /dev/null +++ b/test/static/FilterVerbose/1_instance.json @@ -0,0 +1,89 @@ +[ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1750, + "BaselineIops": 10000, + "BaselineThroughputInMBps": 218.75, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 4, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 4, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": null, + "ValidThreadsPerCore": null + }, + "OndemandPricePerHour": 0.204, + "SpotPrice": 0.03939999999999999 + } +] diff --git a/test/static/FilterVerbose/3_instances.json b/test/static/FilterVerbose/3_instances.json new file mode 100644 index 0000000..3d1ff15 --- /dev/null +++ b/test/static/FilterVerbose/3_instances.json @@ -0,0 +1,263 @@ +[ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1750, + "BaselineIops": 10000, + "BaselineThroughputInMBps": 218.75, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 4, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 4, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": null, + "ValidThreadsPerCore": null + }, + "OndemandPricePerHour": 0.204, + "SpotPrice": 0.03939999999999999 + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 3500, + "BaselineIops": 20000, + "BaselineThroughputInMBps": 437.5, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.4xlarge", + "MemoryInfo": { + "SizeInMiB": 32768 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 8, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 8, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 16, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 16, + "ValidCores": null, + "ValidThreadsPerCore": null + }, + "OndemandPricePerHour": 0.408, + "SpotPrice": null + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 525, + "BaselineIops": 4000, + "BaselineThroughputInMBps": 65.625, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "a1.large", + "MemoryInfo": { + "SizeInMiB": 4096 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 10, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 3, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 3, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "arm64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": null, + "ValidThreadsPerCore": null + }, + "OndemandPricePerHour": 0.051, + "SpotPrice": 0.009819123023512438 + } +] diff --git a/test/static/FilterVerbose/4_special_cases.json b/test/static/FilterVerbose/4_special_cases.json new file mode 100644 index 0000000..6e29f26 --- /dev/null +++ b/test/static/FilterVerbose/4_special_cases.json @@ -0,0 +1,460 @@ +[ + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 19000, + "BaselineIops": 80000, + "BaselineThroughputInMBps": 2375, + "MaximumBandwidthInMbps": 19000, + "MaximumIops": 80000, + "MaximumThroughputInMBps": 2375 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": { + "Accelerators": [ + { + "Count": 16, + "Manufacturer": "AWS", + "Name": "Inferentia" + } + ] + }, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "inf1.24xlarge", + "MemoryInfo": { + "SizeInMiB": 196608 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": { + "MaximumEfaInterfaces": 1 + }, + "EfaSupported": true, + "EnaSupport": "required", + "EncryptionInTransitSupported": true, + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 11, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 11, + "NetworkCardIndex": 0, + "NetworkPerformance": "100 Gigabit" + } + ], + "NetworkPerformance": "100 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedBootModes": [ + "legacy-bios", + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 48, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 96, + "ValidCores": [ + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + }, + "OndemandPricePerHour": 4.721, + "SpotPrice": 1.4163 + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1190, + "BaselineIops": 6000, + "BaselineThroughputInMBps": 148.75, + "MaximumBandwidthInMbps": 4750, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 593.75 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "required" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "nitro", + "InferenceAcceleratorInfo": { + "Accelerators": [ + { + "Count": 1, + "Manufacturer": "AWS", + "Name": "Inferentia" + } + ] + }, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "inf1.2xlarge", + "MemoryInfo": { + "SizeInMiB": 16384 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "required", + "EncryptionInTransitSupported": true, + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 10, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 4, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 4, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 25 Gigabit" + } + ], + "NetworkPerformance": "Up to 25 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedBootModes": [ + "legacy-bios", + "uefi" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 8, + "ValidCores": [ + 2, + 4 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + }, + "OndemandPricePerHour": 0.362, + "SpotPrice": 0.10859999999999999 + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 14000, + "BaselineIops": 80000, + "BaselineThroughputInMBps": 1750, + "MaximumBandwidthInMbps": 14000, + "MaximumIops": 80000, + "MaximumThroughputInMBps": 1750 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": { + "Gpus": [ + { + "Count": 4, + "Manufacturer": "NVIDIA", + "MemoryInfo": { + "SizeInMiB": 8192 + }, + "Name": "M60" + } + ], + "TotalGpuMemoryInMiB": 32768 + }, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "g3.16xlarge", + "MemoryInfo": { + "SizeInMiB": 499712 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "supported", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 50, + "Ipv6AddressesPerInterface": 50, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 15, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 15, + "NetworkCardIndex": 0, + "NetworkPerformance": "25 Gigabit" + } + ], + "NetworkPerformance": "25 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.3 + }, + "SupportedBootModes": [ + "legacy-bios" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 32, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 64, + "ValidCores": [ + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + 24, + 26, + 28, + 30, + 32 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + }, + "OndemandPricePerHour": 4.56, + "SpotPrice": 1.368 + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": true, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 3500, + "BaselineIops": 20000, + "BaselineThroughputInMBps": 437.5, + "MaximumBandwidthInMbps": 3500, + "MaximumIops": 20000, + "MaximumThroughputInMBps": 437.5 + }, + "EbsOptimizedSupport": "default", + "EncryptionSupport": "supported", + "NvmeSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": { + "Gpus": [ + { + "Count": 1, + "Manufacturer": "NVIDIA", + "MemoryInfo": { + "SizeInMiB": 8192 + }, + "Name": "M60" + } + ], + "TotalGpuMemoryInMiB": 8192 + }, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "g3.4xlarge", + "MemoryInfo": { + "SizeInMiB": 124928 + }, + "NetworkInfo": { + "DefaultNetworkCardIndex": 0, + "EfaInfo": null, + "EfaSupported": false, + "EnaSupport": "supported", + "EncryptionInTransitSupported": false, + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkCards": 1, + "MaximumNetworkInterfaces": 8, + "NetworkCards": [ + { + "MaximumNetworkInterfaces": 8, + "NetworkCardIndex": 0, + "NetworkPerformance": "Up to 10 Gigabit" + } + ], + "NetworkPerformance": "Up to 10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.7 + }, + "SupportedBootModes": [ + "legacy-bios" + ], + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 16, + "ValidCores": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + }, + "OndemandPricePerHour": 1.14, + "SpotPrice": 0.34199999999999997 + } +] \ No newline at end of file