diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ceecfba..873dfba 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,7 +3,7 @@ name: EC2 Instance Selector CI and Release on: [push, pull_request, workflow_dispatch] env: - DEFAULT_GO_VERSION: ^1.18 + DEFAULT_GO_VERSION: ^1.23 GITHUB_USERNAME: ${{ secrets.EC2_BOT_GITHUB_USERNAME }} GITHUB_TOKEN: ${{ secrets.EC2_BOT_GITHUB_TOKEN }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/Dockerfile b/Dockerfile index 58e9b17..9195e80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20 as builder +FROM golang:1.23 as builder ## GOLANG env ARG GOPROXY="https://proxy.golang.org|direct" diff --git a/README.md b/README.md index fb985c8..b66225a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
- + @@ -25,7 +25,7 @@ ## Summary -There are over 270 different instance types available on EC2 which can make the process of selecting appropriate instance types difficult. Instance Selector helps you select compatible instance types for your application to run on. The command line interface can be passed resource criteria like vcpus, memory, network performance, and much more and then return the available, matching instance types. +There are over 800 different instance types available on EC2 which can make the process of selecting appropriate instance types difficult. Instance Selector helps you select compatible instance types for your application to run on. The command line interface can be passed resource criteria like vcpus, memory, network performance, and much more and then return the available, matching instance types. If you are using spot instances to save on costs, it is a best practice to use multiple instances types within your auto-scaling group (ASG) to ensure your application doesn't experience downtime due to one instance type being interrupted. Instance Selector will help to find a set of instance types that your application can run on. @@ -313,7 +313,7 @@ Filter Flags: -z, --availability-zones strings Availability zones or zone ids to check EC2 capacity offered in specific AZs --baremetal Bare Metal instance types (.metal instances) -b, --burst-support Burstable instance types - -a, --cpu-architecture string CPU architecture [x86_64/amd64, x86_64_mac, i386, or arm64] + -a, --cpu-architecture string CPU architecture [x86_64, amd64, x86_64_mac, i386, or arm64] --cpu-manufacturer string CPU manufacturer [amd, intel, aws] --current-generation Current generation instance types (explicitly set this to false to not return current generation instance types) --dedicated-hosts Dedicated Hosts supported @@ -334,6 +334,9 @@ Filter Flags: -e, --ena-support Instance types where ENA is supported or required -f, --fpga-support FPGA instance types --free-tier Free Tier supported + --generation int Generation of the instance type (i.e. c7i.xlarge is 7) (sets --generation-min and -max to the same value) + --generation-max int Maximum Generation of the instance type (i.e. c7i.xlarge is 7) If --generation-min is not specified, the lower bound will be 0 + --generation-min int Minimum Generation of the instance type (i.e. c7i.xlarge is 7) If --generation-max is not specified, the upper bound will be infinity --gpu-manufacturer string GPU Manufacturer name (Example: NVIDIA) --gpu-memory-total string Number of GPUs' total memory (Example: 4 GiB) (sets --gpu-memory-total-min and -max to the same value) --gpu-memory-total-max string Maximum Number of GPUs' total memory (Example: 4 GiB) If --gpu-memory-total-min is not specified, the lower bound will be 0 @@ -385,7 +388,8 @@ 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) + --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. + --debug Debug - prints debug log messages -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, interactive) diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index c88ce95..a6b00ed 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -818,8 +818,8 @@ THE SOFTWARE. ------ -** github.com/imdario/mergo; version v0.3.11 -- -https://github.com/imdario/mergo +** dario.cat/mergo; version v1.0.1 -- +dario.cat/mergo Copyright (c) 2013 Dario Castañé. All rights reserved. Copyright (c) 2012 The Go Authors. All rights reserved. @@ -1502,3 +1502,265 @@ 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. + +------ + +** github.com/charmbracelet/x/term; v0.2.0 -- +github.com/charmbracelet/x/term + +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +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. + + +------ + +** github.com/charmbracelet/x/ansi; v0.2.3 -- +github.com/charmbracelet/x/ansi + +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +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. + +------ + +** github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding; v1.12.0 -- +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index e2490d0..a0d2f6f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -44,7 +44,9 @@ const ( defaultRegionEnvVar = "AWS_DEFAULT_REGION" defaultProfile = "default" awsConfigFile = "~/.aws/config" - spotPricingDaysBack = 30 + // 0 means the last price + // increasing this results in a lot more API calls to EC2 which can slow things down + spotPricingDaysBack = 0 tableOutput = "table" tableWideOutput = "table-wide" @@ -100,6 +102,8 @@ const ( freeTier = "free-tier" autoRecovery = "auto-recovery" dedicatedHosts = "dedicated-hosts" + debug = "debug" + generation = "generation" ) // Aggregate Filter Flags @@ -166,7 +170,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.Int32MinMaxRangeFlags(vcpus, cli.StringMe("c"), nil, "Number of vcpus available to the instance type.") cli.ByteQuantityMinMaxRangeFlags(memory, cli.StringMe("m"), nil, "Amount of Memory available (Example: 4 GiB)") cli.RatioFlag(vcpusToMemoryRatio, nil, nil, "The ratio of vcpus to GiBs of memory. (Example: 1:2)") - cli.StringOptionsFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64/amd64, x86_64_mac, i386, or arm64]", []string{"x86_64", "x86_64_mac", "amd64", "i386", "arm64"}) + cli.StringOptionsFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64, amd64, x86_64_mac, i386, or arm64]", []string{"x86_64", "x86_64_mac", "amd64", "i386", "arm64"}) cli.StringOptionsFlag(cpuManufacturer, nil, nil, "CPU manufacturer [amd, intel, aws]", []string{"amd", "intel", "aws"}) cli.Int32MinMaxRangeFlags(gpus, cli.StringMe("g"), nil, "Total Number of GPUs (Example: 4)") cli.ByteQuantityMinMaxRangeFlags(gpuMemoryTotal, nil, nil, "Number of GPUs' total memory (Example: 4 GiB)") @@ -206,6 +210,7 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.BoolFlag(freeTier, nil, nil, "Free Tier supported") cli.BoolFlag(autoRecovery, nil, nil, "EC2 Auto-Recovery supported") cli.BoolFlag(dedicatedHosts, nil, nil, "Dedicated Hosts supported") + cli.IntMinMaxRangeFlags(generation, nil, nil, "Generation of the instance type (i.e. c7i.xlarge is 7)") // Suite Flags - higher level aggregate filters that return opinionated result @@ -219,9 +224,10 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil) cli.ConfigStringFlag(region, cli.StringMe("r"), nil, "AWS Region to use for API requests (NOTE: if not passed in, uses AWS SDK default precedence)", nil) cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil) - cli.ConfigIntFlag(cacheTTL, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_CACHE_TTL", 168), "Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches.") + cli.ConfigIntFlag(cacheTTL, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_CACHE_TTL", 0), "Cache TTLs in hours for pricing and instance type caches. Setting the cache to 0 will turn off caching and cleanup any on-disk caches.") cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches") cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs") + cli.ConfigBoolFlag("debug", nil, nil, "Debug - prints debug log messages") cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help") cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version") cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sorter.SortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections) @@ -273,6 +279,10 @@ Full docs can be found at github.com/aws/amazon-` + binName fmt.Printf("An error occurred when initialising the ec2 selector: %v", err) os.Exit(1) } + if flags[debug] != nil { + debugLogger := log.New(os.Stdout, time.Now().UTC().Format(time.RFC3339)+" DEBUG ", 0) + instanceSelector.SetLogger(debugLogger) + } shutdown := func() { if err := instanceSelector.Save(); err != nil { log.Printf("There was an error saving pricing caches: %v", err) @@ -417,6 +427,7 @@ Full docs can be found at github.com/aws/amazon-` + binName FreeTier: cli.BoolMe(flags[freeTier]), AutoRecovery: cli.BoolMe(flags[autoRecovery]), DedicatedHosts: cli.BoolMe(flags[dedicatedHosts]), + Generation: cli.IntRangeMe(flags[generation]), } if flags[verbose] != nil { diff --git a/go.mod b/go.mod index 8742f9f..f730ad0 100644 --- a/go.mod +++ b/go.mod @@ -1,56 +1,55 @@ module github.com/aws/amazon-ec2-instance-selector/v2 -go 1.20 +go 1.23 require ( - github.com/aws/aws-sdk-go v1.46.6 - github.com/aws/aws-sdk-go-v2 v1.24.0 - github.com/aws/aws-sdk-go-v2/config v1.26.1 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.128.0 - github.com/aws/aws-sdk-go-v2/service/pricing v1.21.6 + dario.cat/mergo v1.0.1 + github.com/aws/aws-sdk-go-v2 v1.32.2 + github.com/aws/aws-sdk-go-v2/config v1.27.43 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0 + github.com/aws/aws-sdk-go-v2/service/pricing v1.32.2 github.com/blang/semver/v4 v4.0.0 - github.com/charmbracelet/bubbles v0.16.1 - github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.7.1 - github.com/evertras/bubble-table v0.15.2 - github.com/imdario/mergo v0.3.16 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.1 + github.com/charmbracelet/lipgloss v0.13.0 + github.com/evertras/bubble-table v0.17.0 github.com/mitchellh/go-homedir v1.1.0 github.com/muesli/termenv v0.15.2 github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 go.uber.org/multierr v1.11.0 ) require ( github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect - github.com/aws/smithy-go v1.19.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/sahilm/fuzzy v0.1.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.4.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index c8af1d4..debeb04 100644 --- a/go.sum +++ b/go.sum @@ -1,87 +1,75 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go v1.46.6 h1:6wFnNC9hETIZLMf6SOTN7IcclrOGwp/n9SLp8Pjt6E8= -github.com/aws/aws-sdk-go v1.46.6/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= -github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= -github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= -github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= -github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= -github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= -github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.128.0 h1:JCUTmTs7W1yvUCOdONMX7Hjgn7N9pj57y4/ibU4KFp4= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.128.0/go.mod h1:raUdIDoNuDPn9dMG3cCmIm8RoWOmZUqQPzuw8xpmB8Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= -github.com/aws/aws-sdk-go-v2/service/pricing v1.21.6 h1:k/f3T13s7wx/By6aKovlVsjdNkRVT0QRR2RlZEvaTGg= -github.com/aws/aws-sdk-go-v2/service/pricing v1.21.6/go.mod h1:9n3tkRCngy3+Iw/8vK3C69iXh22SCGsy3yn16nTxH+s= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= -github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= -github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= -github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI= +github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/config v1.27.43 h1:p33fDDihFC390dhhuv8nOmX419wjOSDQRb+USt20RrU= +github.com/aws/aws-sdk-go-v2/config v1.27.43/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0 h1:LaeziEhHZ/SJZYBK223QVzl3ucHvA9IP4tQMcxGrc9I= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.182.0/go.mod h1:kYXaB4FzyhEJjvrJ84oPnMElLiEAjGxxUunVW2tBSng= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0= +github.com/aws/aws-sdk-go-v2/service/pricing v1.32.2 h1:eBKzA9Te6JHD1TfVjuja7pa8iEdXVzW5z0QPcbrPhNs= +github.com/aws/aws-sdk-go-v2/service/pricing v1.32.2/go.mod h1:2Sg8KGFKp9zzUbY+XdUUEn7xjCzuRt8Zx4PHMwGzRvs= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= -github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/evertras/bubble-table v0.15.2 h1:hVj27V9tk5TD5p6mVv0RK/KJu2sHq0U+mBMux/HptkU= -github.com/evertras/bubble-table v0.15.2/go.mod h1:SPOZKbIpyYWPHBNki3fyNpiPBQkvkULAtOT7NTD5fKY= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/evertras/bubble-table v0.17.0 h1:qQU4bi3IRxuZ5+Fvm3esyU/ucH9ufRXWhWL0fFuMn9c= +github.com/evertras/bubble-table v0.17.0/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= @@ -92,60 +80,31 @@ github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjG 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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/ec2pricing/ec2pricing.go b/pkg/ec2pricing/ec2pricing.go index 18e7f5f..527713b 100644 --- a/pkg/ec2pricing/ec2pricing.go +++ b/pkg/ec2pricing/ec2pricing.go @@ -15,6 +15,7 @@ package ec2pricing import ( "context" + "log" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -37,6 +38,7 @@ var ( type EC2Pricing struct { ODPricing *OnDemandPricing SpotPricing *SpotPricing + logger *log.Logger } // EC2PricingIface is the EC2Pricing interface mainly used to mock out ec2pricing during testing @@ -48,23 +50,20 @@ type EC2PricingIface interface { OnDemandCacheCount() int SpotCacheCount() int Save() error + SetLogger(*log.Logger) } // use us-east-1 since pricing only has endpoints in us-east-1 and ap-south-1 // TODO: In the future we may want to allow the client to select which endpoint is used through some mechanism -// but that would likely happen through overriding this entire function as its signature is fixed +// +// but that would likely happen through overriding this entire function as its signature is fixed func modifyPricingRegion(opt *pricing.Options) { opt.Region = "us-east-1" } // New creates an instance of instance-selector EC2Pricing func New(ctx context.Context, cfg aws.Config) (*EC2Pricing, error) { - pricingClient := pricing.NewFromConfig(cfg, modifyPricingRegion) - ec2Client := ec2.NewFromConfig(cfg) - return &EC2Pricing{ - ODPricing: LoadODCacheOrNew(ctx, pricingClient, cfg.Region, 0, ""), - SpotPricing: LoadSpotCacheOrNew(ctx, ec2Client, cfg.Region, 0, "", DefaultSpotDaysBack), - }, nil + return NewWithCache(ctx, cfg, 0, "") } func NewWithCache(ctx context.Context, cfg aws.Config, ttl time.Duration, cacheDir string) (*EC2Pricing, error) { @@ -76,6 +75,12 @@ func NewWithCache(ctx context.Context, cfg aws.Config, ttl time.Duration, cacheD }, nil } +func (p *EC2Pricing) SetLogger(logger *log.Logger) { + p.logger = logger + p.ODPricing.SetLogger(logger) + p.SpotPricing.SetLogger(logger) +} + // OnDemandCacheCount returns the number of items in the OD cache func (p *EC2Pricing) OnDemandCacheCount() int { return p.ODPricing.Count() diff --git a/pkg/ec2pricing/ec2pricing_test.go b/pkg/ec2pricing/ec2pricing_test.go index b1d67c9..cb695ab 100644 --- a/pkg/ec2pricing/ec2pricing_test.go +++ b/pkg/ec2pricing/ec2pricing_test.go @@ -17,10 +17,11 @@ import ( "context" "encoding/json" "fmt" - ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "io/ioutil" + "os" "testing" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" "github.com/aws/aws-sdk-go-v2/service/ec2" @@ -57,7 +58,7 @@ func (m mockedSpotEC2) DescribeSpotPriceHistory(ctx context.Context, input *ec2. func setupOdMock(t *testing.T, api string, file string) mockedPricing { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) switch api { case getProducts: @@ -77,7 +78,7 @@ func setupOdMock(t *testing.T, api string, file string) mockedPricing { func setupEc2Mock(t *testing.T, api string, file string) mockedSpotEC2 { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) switch api { case describeSpotPriceHistory: diff --git a/pkg/ec2pricing/odpricing.go b/pkg/ec2pricing/odpricing.go index 19cde3c..7d1a0ee 100644 --- a/pkg/ec2pricing/odpricing.go +++ b/pkg/ec2pricing/odpricing.go @@ -18,19 +18,17 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "log" "os" "path/filepath" "strconv" - "strings" "sync" "time" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/aws/aws-sdk-go-v2/service/pricing" pricingtypes "github.com/aws/aws-sdk-go-v2/service/pricing/types" - "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/mitchellh/go-homedir" "github.com/patrickmn/go-cache" "go.uber.org/multierr" @@ -46,6 +44,7 @@ type OnDemandPricing struct { DirectoryPath string cache *cache.Cache pricingClient pricing.GetProductsAPIClient + logger *log.Logger sync.RWMutex } @@ -96,6 +95,7 @@ func LoadODCacheOrNew(ctx context.Context, pricingClient pricing.GetProductsAPIC DirectoryPath: directoryPath, cache: cache.New(fullRefreshTTL, fullRefreshTTL), pricingClient: pricingClient, + logger: log.New(io.Discard, "", 0), } } odPricing := &OnDemandPricing{ @@ -104,6 +104,7 @@ func LoadODCacheOrNew(ctx context.Context, pricingClient pricing.GetProductsAPIC DirectoryPath: expandedDirPath, pricingClient: pricingClient, cache: cache.New(fullRefreshTTL, fullRefreshTTL), + logger: log.New(io.Discard, "", 0), } if fullRefreshTTL <= 0 { odPricing.Clear() @@ -152,6 +153,10 @@ func odCacheRefreshJob(ctx context.Context, odPricing *OnDemandPricing) { } } +func (c *OnDemandPricing) SetLogger(logger *log.Logger) { + c.logger = logger +} + func (c *OnDemandPricing) Refresh(ctx context.Context) error { c.Lock() defer c.Unlock() @@ -198,7 +203,7 @@ func (c *OnDemandPricing) Save() error { if err := os.Mkdir(c.DirectoryPath, 0755); err != nil && !errors.Is(err, os.ErrExist) { return err } - return ioutil.WriteFile(getODCacheFilePath(c.Region, c.DirectoryPath), cacheBytes, 0644) + return os.WriteFile(getODCacheFilePath(c.Region, c.DirectoryPath), cacheBytes, 0644) } func (c *OnDemandPricing) Clear() error { @@ -212,6 +217,11 @@ func (c *OnDemandPricing) Clear() error { // // or, if instanceType is specified, it can request a specific instance type pricing func (c *OnDemandPricing) fetchOnDemandPricing(ctx context.Context, instanceType ec2types.InstanceType) (map[string]float64, error) { + start := time.Now() + calls := 0 + defer func() { + c.logger.Printf("Took %s and %d calls to collect OD pricing", time.Since(start), calls) + }() odPricing := map[string]float64{} productInput := pricing.GetProductsInput{ ServiceCode: c.StringMe(serviceCode), @@ -222,9 +232,10 @@ func (c *OnDemandPricing) fetchOnDemandPricing(ctx context.Context, instanceType p := pricing.NewGetProductsPaginator(c.pricingClient, &productInput) for p.HasMorePages() { + calls++ pricingOutput, err := p.NextPage(ctx) if err != nil { - return nil, fmt.Errorf("failed to get a page, %w", err) + return nil, fmt.Errorf("failed to get next OD pricing page, %w", err) } for _, priceDoc := range pricingOutput.PriceList { @@ -241,7 +252,7 @@ func (c *OnDemandPricing) fetchOnDemandPricing(ctx context.Context, instanceType // StringMe takes an interface and returns a pointer to a string value // If the underlying interface kind is not string or *string then nil is returned -func (*OnDemandPricing) StringMe(i interface{}) *string { +func (c *OnDemandPricing) StringMe(i interface{}) *string { if i == nil { return nil } @@ -251,17 +262,16 @@ func (*OnDemandPricing) StringMe(i interface{}) *string { case string: return &v default: - log.Printf("%s cannot be converted to a string", i) + c.logger.Printf("%s cannot be converted to a string", i) return nil } } func (c *OnDemandPricing) getProductsInputFilters(instanceType ec2types.InstanceType) []pricingtypes.Filter { - regionDescription := c.getRegionForPricingAPI() filters := []pricingtypes.Filter{ {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("ServiceCode"), Value: c.StringMe(serviceCode)}, {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("operatingSystem"), Value: c.StringMe("linux")}, - {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("location"), Value: c.StringMe(regionDescription)}, + {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("regionCode"), Value: c.StringMe(c.Region)}, {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("capacitystatus"), Value: c.StringMe("used")}, {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("preInstalledSw"), Value: c.StringMe("NA")}, {Type: pricingtypes.FilterTypeTermMatch, Field: c.StringMe("tenancy"), Value: c.StringMe("shared")}, @@ -272,30 +282,6 @@ func (c *OnDemandPricing) getProductsInputFilters(instanceType ec2types.Instance return filters } -// getRegionForPricingAPI attempts to retrieve the region description based on the AWS session used to create -// the ec2pricing struct. It then uses the endpoints package in the aws sdk to retrieve the region description -// This is necessary because the pricing API uses the region description rather than a region ID -func (c *OnDemandPricing) getRegionForPricingAPI() string { - endpointResolver := endpoints.DefaultResolver() - partitions := endpointResolver.(endpoints.EnumPartitions).Partitions() - - // use us-east-1 as the default - regionDescription := "US East (N. Virginia)" - for _, partition := range partitions { - regions := partition.Regions() - if region, ok := regions[c.Region]; ok { - regionDescription = region.Description() - } - } - - // endpoints package returns European regions with the word "Europe," but the pricing API expects the word "EU." - // This formatting mismatch is only present with European regions. - // So replace "Europe" with "EU" if it exists in the regionDescription string. - regionDescription = strings.ReplaceAll(regionDescription, "Europe", "EU") - - return regionDescription -} - // parseOndemandUnitPrice takes a priceList from the pricing API and parses its weirdness func (c *OnDemandPricing) parseOndemandUnitPrice(priceList string) (string, float64, error) { var productPriceList PricingList diff --git a/pkg/ec2pricing/spotpricing.go b/pkg/ec2pricing/spotpricing.go index cbf56e9..1f4d85d 100644 --- a/pkg/ec2pricing/spotpricing.go +++ b/pkg/ec2pricing/spotpricing.go @@ -18,6 +18,7 @@ import ( "encoding/gob" "errors" "fmt" + "io" "log" "math" "os" @@ -44,6 +45,7 @@ type SpotPricing struct { DirectoryPath string cache *cache.Cache ec2Client ec2.DescribeSpotPriceHistoryAPIClient + logger *log.Logger sync.RWMutex } @@ -63,6 +65,7 @@ func LoadSpotCacheOrNew(ctx context.Context, ec2Client ec2.DescribeSpotPriceHist DirectoryPath: directoryPath, cache: cache.New(fullRefreshTTL, fullRefreshTTL), ec2Client: ec2Client, + logger: log.New(io.Discard, "", 0), } } spotPricing := &SpotPricing{ @@ -71,6 +74,7 @@ func LoadSpotCacheOrNew(ctx context.Context, ec2Client ec2.DescribeSpotPriceHist DirectoryPath: expandedDirPath, ec2Client: ec2Client, cache: cache.New(fullRefreshTTL, fullRefreshTTL), + logger: log.New(io.Discard, "", 0), } if fullRefreshTTL <= 0 { spotPricing.Clear() @@ -121,6 +125,10 @@ func spotCacheRefreshJob(ctx context.Context, spotPricing *SpotPricing, days int } } +func (c *SpotPricing) SetLogger(logger *log.Logger) { + c.logger = logger +} + func (c *SpotPricing) Refresh(ctx context.Context, days int) error { c.Lock() defer c.Unlock() @@ -241,6 +249,11 @@ func (c *SpotPricing) Clear() error { // fetchSpotPricingTimeSeries makes a bulk request to the ec2 api to retrieve all spot instance type pricing for the past n days // If instanceType is empty, it will fetch for all instance types func (c *SpotPricing) fetchSpotPricingTimeSeries(ctx context.Context, instanceType ec2types.InstanceType, days int) (map[string][]*spotPricingEntry, error) { + start := time.Now() + calls := 0 + defer func() { + c.logger.Printf("Took %s and %d calls to collect Spot pricing", time.Since(start), calls) + }() spotTimeSeries := map[string][]*spotPricingEntry{} endTime := time.Now().UTC() startTime := endTime.Add(time.Hour * time.Duration(24*-1*days)) @@ -258,9 +271,10 @@ func (c *SpotPricing) fetchSpotPricingTimeSeries(ctx context.Context, instanceTy // Iterate through the Amazon S3 object pages. for p.HasMorePages() { + calls++ spotHistoryOutput, err := p.NextPage(ctx) if err != nil { - return nil, fmt.Errorf("failed to get a page, %w", err) + return nil, fmt.Errorf("failed to get a spot pricing page, %w", err) } for _, history := range spotHistoryOutput.SpotPriceHistory { diff --git a/pkg/instancetypes/instancetypes.go b/pkg/instancetypes/instancetypes.go index f38512d..fa44f53 100644 --- a/pkg/instancetypes/instancetypes.go +++ b/pkg/instancetypes/instancetypes.go @@ -18,7 +18,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "log" "os" "path/filepath" @@ -48,8 +48,10 @@ type Provider struct { lastFullRefresh *time.Time ec2Client ec2.DescribeInstanceTypesAPIClient cache *cache.Cache + logger *log.Logger } +// NewProvider creates a new Instance Types provider used to fetch Instance Type information from EC2 func NewProvider(directoryPath string, region string, ttl time.Duration, ec2Client ec2.DescribeInstanceTypesAPIClient) *Provider { expandedDirPath, err := homedir.Expand(directoryPath) if err != nil { @@ -61,9 +63,11 @@ func NewProvider(directoryPath string, region string, ttl time.Duration, ec2Clie FullRefreshTTL: ttl, ec2Client: ec2Client, cache: cache.New(ttl, ttl), + logger: log.New(io.Discard, "", 0), } } +// NewProvider creates a new Instance Types provider used to fetch Instance Type information from EC2 and optionally cache func LoadFromOrNew(directoryPath string, region string, ttl time.Duration, ec2Client ec2.DescribeInstanceTypesAPIClient) *Provider { expandedDirPath, err := homedir.Expand(directoryPath) if err != nil { @@ -87,6 +91,7 @@ func LoadFromOrNew(directoryPath string, region string, ttl time.Duration, ec2Cl DirectoryPath: expandedDirPath, ec2Client: ec2Client, cache: itCache, + logger: log.New(io.Discard, "", 0), } } @@ -107,7 +112,17 @@ func getCacheFilePath(region string, expandedDirPath string) string { return filepath.Join(expandedDirPath, fmt.Sprintf("%s-%s", region, CacheFileName)) } +func (p *Provider) SetLogger(logger *log.Logger) { + p.logger = logger +} + func (p *Provider) Get(ctx context.Context, instanceTypes []ec2types.InstanceType) ([]*Details, error) { + p.logger.Printf("Getting instance types %v", instanceTypes) + start := time.Now() + calls := 0 + defer func() { + p.logger.Printf("Took %s and %d calls to collect Instance Types", time.Since(start), calls) + }() instanceTypeDetails := []*Details{} describeInstanceTypeOpts := &ec2.DescribeInstanceTypesInput{} if len(instanceTypes) != 0 { @@ -120,6 +135,10 @@ func (p *Provider) Get(ctx context.Context, instanceTypes []ec2types.InstanceTyp describeInstanceTypeOpts.InstanceTypes = append(describeInstanceTypeOpts.InstanceTypes, instanceType) } } + // if we were able to retrieve all from cache, return here, else continue to do a remote lookup + if len(describeInstanceTypeOpts.InstanceTypes) == 0 { + return instanceTypeDetails, nil + } } else if p.lastFullRefresh != nil && !p.isFullRefreshNeeded() { for _, item := range p.cache.Items() { instanceTypeDetails = append(instanceTypeDetails, item.Object.(*Details)) @@ -127,14 +146,14 @@ func (p *Provider) Get(ctx context.Context, instanceTypes []ec2types.InstanceTyp return instanceTypeDetails, nil } - s := ec2.NewDescribeInstanceTypesPaginator(p.ec2Client, &ec2.DescribeInstanceTypesInput{}) + s := ec2.NewDescribeInstanceTypesPaginator(p.ec2Client, describeInstanceTypeOpts) for s.HasMorePages() { + calls++ instanceTypeOutput, err := s.NextPage(ctx) if err != nil { - return nil, fmt.Errorf("failed to get a page, %w", err) + return nil, fmt.Errorf("failed to get next instance types page, %w", err) } - for _, instanceTypeInfo := range instanceTypeOutput.InstanceTypes { itDetails := &Details{InstanceTypeInfo: instanceTypeInfo} instanceTypeDetails = append(instanceTypeDetails, itDetails) @@ -167,7 +186,7 @@ func (p *Provider) Save() error { if err := os.Mkdir(p.DirectoryPath, 0755); err != nil && !errors.Is(err, os.ErrExist) { return err } - return ioutil.WriteFile(getCacheFilePath(p.Region, p.DirectoryPath), cacheBytes, 0644) + return os.WriteFile(getCacheFilePath(p.Region, p.DirectoryPath), cacheBytes, 0644) } func (p *Provider) Clear() error { diff --git a/pkg/selector/comparators.go b/pkg/selector/comparators.go index eb639ec..94236bf 100644 --- a/pkg/selector/comparators.go +++ b/pkg/selector/comparators.go @@ -14,7 +14,6 @@ package selector import ( - "log" "math" "reflect" "regexp" @@ -30,7 +29,11 @@ const ( required = "required" ) -var amdRegex = regexp.MustCompile("[a-zA-Z0-9]+a\\.[a-zA-Z0-9]") +var ( + amdRegex = regexp.MustCompile(`[a-zA-Z0-9]+a\\.[a-zA-Z0-9]`) + networkPerfRE = regexp.MustCompile(`[0-9]+ Gigabit`) + generationRE = regexp.MustCompile(`[a-zA-Z]+([0-9]+)`) +) func isSupportedFromString(instanceTypeValue *string, target *string) bool { if target == nil { @@ -302,12 +305,7 @@ func getNetworkPerformance(networkPerformance *string) *int { if networkPerformance == nil { return aws.Int(-1) } - re, err := regexp.Compile(`[0-9]+ Gigabit`) - if err != nil { - log.Printf("Unable to compile regexp to parse network performance: %s\n", *networkPerformance) - return nil - } - networkBandwidth := re.FindString(*networkPerformance) + networkBandwidth := networkPerfRE.FindString(*networkPerformance) if networkBandwidth == "" { return aws.Int(-1) } @@ -392,6 +390,22 @@ func getCPUManufacturer(instanceTypeInfo *ec2types.InstanceTypeInfo) CPUManufact return CPUManufacturerIntel } +// getInstanceTypeGeneration returns the generation from an instance type name +// i.e. c7i.xlarge -> 7 +// if any error occurs, 0 will be returned +func getInstanceTypeGeneration(instanceTypeName string) *int { + zero := 0 + matches := generationRE.FindStringSubmatch(instanceTypeName) + if len(matches) < 2 { + return &zero + } + gen, err := strconv.Atoi(matches[1]) + if err != nil { + return &zero + } + return &gen +} + // supportSyntaxToBool takes an instance spec field that uses ["unsupported", "supported", "required", or "default"] // and transforms it to a *bool to use in filter execution func supportSyntaxToBool(instanceTypeSupport *string) *bool { diff --git a/pkg/selector/outputs/bubbletea_internal_test.go b/pkg/selector/outputs/bubbletea_internal_test.go index 495b45a..8decdb4 100644 --- a/pkg/selector/outputs/bubbletea_internal_test.go +++ b/pkg/selector/outputs/bubbletea_internal_test.go @@ -16,7 +16,7 @@ package outputs import ( "encoding/json" "fmt" - "io/ioutil" + "os" "strings" "testing" @@ -36,7 +36,7 @@ const ( 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) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) instanceTypes := []*instancetypes.Details{} @@ -133,7 +133,7 @@ func TestNewBubbleTeaModel_SpotPricing(t *testing.T) { model := NewBubbleTeaModel(instanceTypes) rows := model.tableModel.table.GetVisibleRows() expectedODPrice := "$1.368" - actualODPrice := fmt.Sprintf("%v", rows[0].Data["Spot Price/Hr (30d avg)"]) + actualODPrice := fmt.Sprintf("%v", rows[0].Data["Spot Price/Hr"]) h.Assert(t, actualODPrice == expectedODPrice, "Actual spot price should be %s, but is actually %s", expectedODPrice, actualODPrice) @@ -142,7 +142,7 @@ func TestNewBubbleTeaModel_SpotPricing(t *testing.T) { model = NewBubbleTeaModel(instanceTypes) rows = model.tableModel.table.GetVisibleRows() expectedODPrice = "-Not Fetched-" - actualODPrice = fmt.Sprintf("%v", rows[0].Data["Spot Price/Hr (30d avg)"]) + actualODPrice = fmt.Sprintf("%v", rows[0].Data["Spot Price/Hr"]) h.Assert(t, actualODPrice == expectedODPrice, "Actual spot price should be %s, but is actually %s", expectedODPrice, actualODPrice) } diff --git a/pkg/selector/outputs/outputs.go b/pkg/selector/outputs/outputs.go index 319cf9f..dd16ec7 100644 --- a/pkg/selector/outputs/outputs.go +++ b/pkg/selector/outputs/outputs.go @@ -45,7 +45,7 @@ type wideColumnsData struct { gpuMemory string `column:"GPU Mem (GiB)"` gpuInfo string `column:"GPU Info"` odPrice string `column:"On-Demand Price/Hr"` - spotPrice string `column:"Spot Price/Hr (30d avg)"` + spotPrice string `column:"Spot Price/Hr"` } // SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names diff --git a/pkg/selector/outputs/outputs_test.go b/pkg/selector/outputs/outputs_test.go index 50d24fa..a5fbe93 100644 --- a/pkg/selector/outputs/outputs_test.go +++ b/pkg/selector/outputs/outputs_test.go @@ -16,7 +16,7 @@ package outputs_test import ( "encoding/json" "fmt" - "io/ioutil" + "os" "strings" "testing" @@ -33,7 +33,7 @@ const ( func getInstanceTypes(t *testing.T, file string) []*instancetypes.Details { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, describeInstanceTypes, file) - mockFile, err := ioutil.ReadFile(mockFilename) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) dito := ec2.DescribeInstanceTypesOutput{} err = json.Unmarshal(mockFile, &dito) diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 897864c..91ead91 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -17,6 +17,7 @@ package selector import ( "context" "fmt" + "io" "log" "reflect" "regexp" @@ -91,6 +92,7 @@ const ( freeTier = "freeTier" autoRecovery = "autoRecovery" dedicatedHosts = "dedicatedHosts" + generation = "generation" cpuArchitectureAMD64 = "amd64" @@ -101,24 +103,10 @@ const ( // New creates an instance of Selector provided an aws session func New(ctx context.Context, cfg aws.Config) (*Selector, error) { - serviceRegistry := NewRegistry() - serviceRegistry.RegisterAWSServices() - ec2Client := ec2.NewFromConfig(cfg, func(options *ec2.Options) { - options.APIOptions = append(options.APIOptions, middleware.AddUserAgentKeyValue(sdkName, versionID)) - }) - pricingClient, err := ec2pricing.New(ctx, cfg) - if err != nil { - return nil, err - } - - return &Selector{ - EC2: ec2Client, - EC2Pricing: pricingClient, - InstanceTypesProvider: instancetypes.LoadFromOrNew("", cfg.Region, 0, ec2Client), - ServiceRegistry: serviceRegistry, - }, nil + return NewWithCache(ctx, cfg, 0, "") } +// NewWithCache creates an instance of Selector backed by an on-disk cache provided an aws session and cache configuration parameters func NewWithCache(ctx context.Context, cfg aws.Config, ttl time.Duration, cacheDir string) (*Selector, error) { serviceRegistry := NewRegistry() serviceRegistry.RegisterAWSServices() @@ -135,54 +123,56 @@ func NewWithCache(ctx context.Context, cfg aws.Config, ttl time.Duration, cacheD EC2Pricing: pricingClient, InstanceTypesProvider: instancetypes.LoadFromOrNew(cacheDir, cfg.Region, ttl, ec2Client), ServiceRegistry: serviceRegistry, + Logger: log.New(io.Discard, "", 0), }, nil } -func (itf Selector) Save() error { - return multierr.Append(itf.EC2Pricing.Save(), itf.InstanceTypesProvider.Save()) +// SetLogger can be called to log more detailed logs about what selector is doing +// including things like API timings +// If SetLogger is not called, no logs will be displayed +func (s *Selector) SetLogger(logger *log.Logger) { + s.Logger = logger + s.InstanceTypesProvider.SetLogger(logger) + s.EC2Pricing.SetLogger(logger) +} + +// Save persists the selector cache data to disk if caching is configured +func (s Selector) Save() error { + return multierr.Append(s.EC2Pricing.Save(), s.InstanceTypesProvider.Save()) } // Filter accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a simple list of instance type strings -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and -// OutputInstanceTypes() in the next major version. -func (itf Selector) Filter(ctx context.Context, filters Filters) ([]string, error) { +func (s Selector) Filter(ctx context.Context, filters Filters) ([]string, error) { outputFn := InstanceTypesOutputFn(outputs.SimpleInstanceTypeOutput) - output, _, err := itf.FilterWithOutput(ctx, filters, outputFn) + output, _, err := s.FilterWithOutput(ctx, filters, outputFn) return output, err } // FilterVerbose accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a list instanceTypeInfo -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() in the next -// major version. -func (itf Selector) FilterVerbose(ctx context.Context, filters Filters) ([]*instancetypes.Details, error) { - instanceTypeInfoSlice, err := itf.rawFilter(ctx, filters) +func (s Selector) FilterVerbose(ctx context.Context, filters Filters) ([]*instancetypes.Details, error) { + instanceTypeInfoSlice, err := s.rawFilter(ctx, filters) if err != nil { return nil, err } - instanceTypeInfoSlice, _ = itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) + instanceTypeInfoSlice, _ = s.truncateResults(filters.MaxResults, instanceTypeInfoSlice) return instanceTypeInfoSlice, nil } // FilterWithOutput accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns a list of strings based on the custom outputFn -// -// Deprecated: This function will be replaced with GetFilteredInstanceTypes() and -// OutputInstanceTypes() in the next major version. -func (itf Selector) FilterWithOutput(ctx context.Context, filters Filters, outputFn InstanceTypesOutput) ([]string, int, error) { - instanceTypeInfoSlice, err := itf.rawFilter(ctx, filters) +func (s Selector) FilterWithOutput(ctx context.Context, filters Filters, outputFn InstanceTypesOutput) ([]string, int, error) { + instanceTypeInfoSlice, err := s.rawFilter(ctx, filters) if err != nil { return nil, 0, err } - instanceTypeInfoSlice, numOfItemsTruncated := itf.truncateResults(filters.MaxResults, instanceTypeInfoSlice) + instanceTypeInfoSlice, numOfItemsTruncated := s.truncateResults(filters.MaxResults, instanceTypeInfoSlice) output := outputFn.Output(instanceTypeInfoSlice) return output, numOfItemsTruncated, nil } -func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int) { +func (s Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*instancetypes.Details) ([]*instancetypes.Details, int) { if maxResults == nil { return instanceTypeInfoSlice, 0 } @@ -194,11 +184,11 @@ func (itf Selector) truncateResults(maxResults *int, instanceTypeInfoSlice []*in } // AggregateFilterTransform takes higher level filters which are used to affect multiple raw filters in an opinionated way. -func (itf Selector) AggregateFilterTransform(ctx context.Context, filters Filters) (Filters, error) { +func (s Selector) AggregateFilterTransform(ctx context.Context, filters Filters) (Filters, error) { transforms := []FiltersTransform{ - TransformFn(itf.TransformBaseInstanceType), - TransformFn(itf.TransformFlexible), - TransformFn(itf.TransformForService), + TransformFn(s.TransformBaseInstanceType), + TransformFn(s.TransformFlexible), + TransformFn(s.TransformForService), } var err error for _, transform := range transforms { @@ -212,8 +202,8 @@ func (itf Selector) AggregateFilterTransform(ctx context.Context, filters Filter // rawFilter accepts a Filters struct which is used to select the available instance types // matching the criteria within Filters and returns the detailed specs of matching instance types -func (itf Selector) rawFilter(ctx context.Context, filters Filters) ([]*instancetypes.Details, error) { - filters, err := itf.AggregateFilterTransform(ctx, filters) +func (s Selector) rawFilter(ctx context.Context, filters Filters) ([]*instancetypes.Details, error) { + filters, err := s.AggregateFilterTransform(ctx, filters) if err != nil { return nil, err } @@ -231,12 +221,12 @@ func (itf Selector) rawFilter(ctx context.Context, filters Filters) ([]*instance } else if filters.Region != nil { locations = []string{*filters.Region} } - locationInstanceOfferings, err := itf.RetrieveInstanceTypesSupportedInLocations(ctx, locations) + locationInstanceOfferings, err := s.RetrieveInstanceTypesSupportedInLocations(ctx, locations) if err != nil { return nil, err } - instanceTypeDetails, err := itf.InstanceTypesProvider.Get(ctx, nil) + instanceTypeDetails, err := s.InstanceTypesProvider.Get(ctx, nil) if err != nil { return nil, err } @@ -247,33 +237,35 @@ func (itf Selector) rawFilter(ctx context.Context, filters Filters) ([]*instance wg.Add(1) go func(instanceTypeInfo instancetypes.Details) { defer wg.Done() - it, err := itf.prepareFilter(ctx, filters, instanceTypeInfo, availabilityZones, locationInstanceOfferings) + it, err := s.prepareFilter(ctx, filters, instanceTypeInfo, availabilityZones, locationInstanceOfferings) if err != nil { - log.Println(err) + s.Logger.Printf("Unable to prepare filter for %s, %v", instanceTypeInfo.InstanceType, err) } if it != nil { instanceTypes <- it } }(*instanceTypeInfo) } - wg.Wait() - close(instanceTypes) + go func() { + wg.Wait() + close(instanceTypes) + }() for it := range instanceTypes { filteredInstanceTypes = append(filteredInstanceTypes, it) } return sortInstanceTypeInfo(filteredInstanceTypes), nil } -func (itf Selector) prepareFilter(ctx context.Context, filters Filters, instanceTypeInfo instancetypes.Details, availabilityZones []string, locationInstanceOfferings map[ec2types.InstanceType]string) (*instancetypes.Details, error) { +func (s Selector) prepareFilter(ctx context.Context, filters Filters, instanceTypeInfo instancetypes.Details, availabilityZones []string, locationInstanceOfferings map[ec2types.InstanceType]string) (*instancetypes.Details, error) { instanceTypeName := instanceTypeInfo.InstanceType isFpga := instanceTypeInfo.FpgaInfo != nil var instanceTypeHourlyPriceForFilter float64 // Price used to filter based on usage class var instanceTypeHourlyPriceOnDemand, instanceTypeHourlyPriceSpot *float64 // If prices are fetched, populate the fields irrespective of the price filters - if itf.EC2Pricing.OnDemandCacheCount() > 0 { - price, err := itf.EC2Pricing.GetOnDemandInstanceTypeCost(ctx, instanceTypeName) + if s.EC2Pricing.OnDemandCacheCount() > 0 { + price, err := s.EC2Pricing.GetOnDemandInstanceTypeCost(ctx, instanceTypeName) if err != nil { - log.Printf("Could not retrieve instantaneous hourly on-demand price for instance type %s - %s\n", instanceTypeName, err) + s.Logger.Printf("Could not retrieve instantaneous hourly on-demand price for instance type %s - %s\n", instanceTypeName, err) } else { instanceTypeHourlyPriceOnDemand = &price instanceTypeInfo.OndemandPricePerHour = instanceTypeHourlyPriceOnDemand @@ -287,10 +279,10 @@ func (itf Selector) prepareFilter(ctx context.Context, filters Filters, instance } } - if itf.EC2Pricing.SpotCacheCount() > 0 && isSpotUsageClass { - price, err := itf.EC2Pricing.GetSpotInstanceTypeNDayAvgCost(ctx, instanceTypeName, availabilityZones, 30) + if s.EC2Pricing.SpotCacheCount() > 0 && isSpotUsageClass { + price, err := s.EC2Pricing.GetSpotInstanceTypeNDayAvgCost(ctx, instanceTypeName, availabilityZones, 30) if err != nil { - log.Printf("Could not retrieve 30 day avg hourly spot price for instance type %s\n", instanceTypeName) + s.Logger.Printf("Could not retrieve 30 day avg hourly spot price for instance type %s\n", instanceTypeName) } else { instanceTypeHourlyPriceSpot = &price instanceTypeInfo.SpotPrice = instanceTypeHourlyPriceSpot @@ -352,6 +344,7 @@ func (itf Selector) prepareFilter(ctx context.Context, filters Filters, instance inferenceAcceleratorManufacturer: {filters.InferenceAcceleratorManufacturer, getInferenceAcceleratorManufacturers(instanceTypeInfo.InferenceAcceleratorInfo)}, inferenceAcceleratorModel: {filters.InferenceAcceleratorModel, getInferenceAcceleratorModels(instanceTypeInfo.InferenceAcceleratorInfo)}, dedicatedHosts: {filters.DedicatedHosts, instanceTypeInfo.DedicatedHostsSupported}, + generation: {filters.Generation, getInstanceTypeGeneration(string(instanceTypeInfo.InstanceType))}, } if isInDenyList(filters.DenyList, instanceTypeName) || !isInAllowList(filters.AllowList, instanceTypeName) { @@ -363,7 +356,7 @@ func (itf Selector) prepareFilter(ctx context.Context, filters Filters, instance } var isInstanceSupported bool - isInstanceSupported, err := itf.executeFilters(ctx, filterToInstanceSpecMappingPairs, instanceTypeName) + isInstanceSupported, err := s.executeFilters(ctx, filterToInstanceSpecMappingPairs, instanceTypeName) if err != nil { return nil, err } @@ -388,9 +381,9 @@ func sortInstanceTypeInfo(instanceTypeInfoSlice []*instancetypes.Details) []*ins // executeFilters accepts a mapping of filter name to filter pairs which are iterated through // to determine if the instance type matches the filter values. -func (itf Selector) executeFilters(ctx context.Context, filterToInstanceSpecMapping map[string]filterPair, instanceType ec2types.InstanceType) (bool, error) { +func (s Selector) executeFilters(ctx context.Context, filterToInstanceSpecMapping map[string]filterPair, instanceType ec2types.InstanceType) (bool, error) { verdict := make(chan bool, len(filterToInstanceSpecMapping)+1) - errs := make(chan error) + errs := make(chan error, len(filterToInstanceSpecMapping)) ctx, cancel := context.WithCancel(ctx) defer cancel() var wg sync.WaitGroup @@ -432,6 +425,8 @@ func (itf Selector) executeFilters(ctx context.Context, filterToInstanceSpecMapp } } +// exec executes a specific filterPair (user value & instance spec) with a specific instance type +// If the filterPair matches, true is returned func exec(instanceType ec2types.InstanceType, filterName string, filter filterPair) (bool, error) { filterVal := filter.filterValue instanceSpec := filter.instanceSpec @@ -443,7 +438,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa instanceSpecType := reflect.ValueOf(instanceSpec).Type() filterType := filterValReflection.Type() filterDetailsMsg := fmt.Sprintf("filter (%s: %s => %s) corresponding to instance spec (%s => %s) for instance type %s", filterName, filterVal, filterType, instanceSpec, instanceSpecType, instanceType) - invalidInstanceSpecTypeMsg := fmt.Sprintf("Unable to process for %s", filterDetailsMsg) + errInvalidInstanceSpec := fmt.Errorf("unable to process for %s", filterDetailsMsg) // Determine appropriate filter comparator by switching on filter type switch filter := filterVal.(type) { @@ -458,7 +453,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *bool: switch iSpec := instanceSpec.(type) { @@ -467,7 +462,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *IntRangeFilter: switch iSpec := instanceSpec.(type) { @@ -480,7 +475,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *Int32RangeFilter: switch iSpec := instanceSpec.(type) { @@ -489,7 +484,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *Float64RangeFilter: switch iSpec := instanceSpec.(type) { @@ -498,7 +493,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ByteQuantityRangeFilter: mibRange := Uint64RangeFilter{ @@ -528,7 +523,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *float64: switch iSpec := instanceSpec.(type) { @@ -537,7 +532,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ec2types.ArchitectureType: switch iSpec := instanceSpec.(type) { @@ -546,7 +541,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ec2types.UsageClassType: switch iSpec := instanceSpec.(type) { @@ -555,7 +550,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *CPUManufacturer: switch iSpec := instanceSpec.(type) { @@ -564,7 +559,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ec2types.VirtualizationType: switch iSpec := instanceSpec.(type) { @@ -573,7 +568,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ec2types.InstanceTypeHypervisor: switch iSpec := instanceSpec.(type) { @@ -582,7 +577,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *ec2types.RootDeviceType: switch iSpec := instanceSpec.(type) { @@ -591,7 +586,7 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } case *[]string: switch iSpec := instanceSpec.(type) { @@ -607,10 +602,10 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa return false, nil } default: - return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + return false, errInvalidInstanceSpec } default: - return false, fmt.Errorf("No filter handler found for %s", filterDetailsMsg) + return false, fmt.Errorf("no filter handler found for %s", filterDetailsMsg) } return true, nil } @@ -618,13 +613,13 @@ func exec(instanceType ec2types.InstanceType, filterName string, filter filterPa // RetrieveInstanceTypesSupportedInLocations returns a map of instance type -> AZ or Region for all instance types supported in the intersected locations passed in // The location can be a zone-id (ie. use1-az1), a zone-name (us-east-1a), or a region name (us-east-1). // Note that zone names are not necessarily the same across accounts -func (itf Selector) RetrieveInstanceTypesSupportedInLocations(ctx context.Context, locations []string) (map[ec2types.InstanceType]string, error) { +func (s Selector) RetrieveInstanceTypesSupportedInLocations(ctx context.Context, locations []string) (map[ec2types.InstanceType]string, error) { if len(locations) == 0 { return nil, nil } availableInstanceTypes := map[ec2types.InstanceType]int{} for _, location := range locations { - locationType, err := itf.getLocationType(ctx, location) + locationType, err := s.getLocationType(ctx, location) if err != nil { return nil, err } @@ -639,12 +634,12 @@ func (itf Selector) RetrieveInstanceTypesSupportedInLocations(ctx context.Contex }, } - p := ec2.NewDescribeInstanceTypeOfferingsPaginator(itf.EC2, instanceTypeOfferingsInput) + p := ec2.NewDescribeInstanceTypeOfferingsPaginator(s.EC2, instanceTypeOfferingsInput) for p.HasMorePages() { instanceTypeOfferings, err := p.NextPage(ctx) if err != nil { - return nil, fmt.Errorf("Encountered an error when describing instance type offerings: %w", err) + return nil, fmt.Errorf("encountered an error when describing instance type offerings: %w", err) } for _, instanceType := range instanceTypeOfferings.InstanceTypeOfferings { @@ -666,8 +661,8 @@ func (itf Selector) RetrieveInstanceTypesSupportedInLocations(ctx context.Contex return availableInstanceTypesAllLocations, nil } -func (itf Selector) getLocationType(ctx context.Context, location string) (ec2types.LocationType, error) { - azs, err := itf.EC2.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{}) +func (s Selector) getLocationType(ctx context.Context, location string) (ec2types.LocationType, error) { + azs, err := s.EC2.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{}) if err != nil { return "", err } @@ -680,7 +675,7 @@ func (itf Selector) getLocationType(ctx context.Context, location string) (ec2ty return zoneIDLocationType, nil } } - return "", fmt.Errorf("The location passed in (%s) is not a valid zone-id, zone-name, or region name", location) + return "", fmt.Errorf("the location passed in (%s) is not a valid zone-id, zone-name, or region name", location) } func isSupportedInLocation(instanceOfferings map[ec2types.InstanceType]string, instanceType ec2types.InstanceType) bool { diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index 034424d..22fca82 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -18,7 +18,8 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "log" + "os" "regexp" "strconv" "testing" @@ -85,7 +86,7 @@ func mockMultiRespDescribeInstanceTypesOfferings(t *testing.T, locationToFile ma locationToResp := map[string]ec2.DescribeInstanceTypeOfferingsOutput{} for zone, file := range locationToFile { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) ditoo := ec2.DescribeInstanceTypeOfferingsOutput{} err = json.Unmarshal(mockFile, &ditoo) @@ -102,7 +103,7 @@ func mockMultiRespDescribeInstanceTypesOfferings(t *testing.T, locationToFile ma func setupMock(t *testing.T, api string, file string) mockedEC2 { mockFilename := fmt.Sprintf("%s/%s/%s", mockFilesPath, api, file) - mockFile, err := ioutil.ReadFile(mockFilename) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) switch api { case describeInstanceTypes: @@ -587,6 +588,7 @@ func (p *ec2PricingMock) SpotCacheCount() int { func (p *ec2PricingMock) Save() error { return nil } +func (p *ec2PricingMock) SetLogger(_ *log.Logger) {} func TestFilter_PricePerHour(t *testing.T) { itf := getSelector(setupMock(t, describeInstanceTypes, "t3_micro.json")) diff --git a/pkg/selector/services.go b/pkg/selector/services.go index b46495c..c9d460e 100644 --- a/pkg/selector/services.go +++ b/pkg/selector/services.go @@ -17,7 +17,7 @@ import ( "fmt" "strings" - "github.com/imdario/mergo" + "dario.cat/mergo" ) // Service is used to write custom service filter transforms diff --git a/pkg/selector/types.go b/pkg/selector/types.go index 60395eb..dd0900b 100644 --- a/pkg/selector/types.go +++ b/pkg/selector/types.go @@ -15,9 +15,11 @@ package selector import ( "encoding/json" + "log" + "regexp" + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/awsapi" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" - "regexp" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity" "github.com/aws/amazon-ec2-instance-selector/v2/pkg/ec2pricing" @@ -44,6 +46,7 @@ type Selector struct { EC2Pricing ec2pricing.EC2PricingIface InstanceTypesProvider *instancetypes.Provider ServiceRegistry ServiceRegistry + Logger *log.Logger } // IntRangeFilter holds an upper and lower bound int @@ -271,6 +274,13 @@ type Filters struct { // DedicatedHosts filters on instance types that support dedicated hosts tenancy DedicatedHosts *bool + + // Generation filters on the instance type generation + // i.e. c7i.xlarge is 7 + // NOTE that generation is only comparable per instance family + // For example, i3 and c5 are both 5th generation, but the Generation filter will + // only filter on the number in the instance type name. + Generation *IntRangeFilter } type CPUManufacturer string @@ -287,9 +297,9 @@ const ( // ordering of this slice is not guaranteed to be stable across updates. func (CPUManufacturer) Values() []CPUManufacturer { return []CPUManufacturer{ - "aws", - "amd", - "intel", + CPUManufacturerAWS, + CPUManufacturerAMD, + CPUManufacturerIntel, } } diff --git a/pkg/sorter/sorter_test.go b/pkg/sorter/sorter_test.go index 75e5c38..8a59b0b 100644 --- a/pkg/sorter/sorter_test.go +++ b/pkg/sorter/sorter_test.go @@ -16,7 +16,7 @@ package sorter_test import ( "encoding/json" "fmt" - "io/ioutil" + "os" "strings" "testing" @@ -38,7 +38,7 @@ const ( 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) + mockFile, err := os.ReadFile(mockFilename) h.Assert(t, err == nil, "Error reading mock file "+string(mockFilename)) instanceTypes := []*instancetypes.Details{} diff --git a/test/e2e/run-test b/test/e2e/run-test index 74b6f5d..2320688 100755 --- a/test/e2e/run-test +++ b/test/e2e/run-test @@ -93,7 +93,7 @@ params=( ) echo "${expected[*]}" | execute_test "24 VCPUs" "${params[@]}" -expected=(g2.8xlarge g3.16xlarge g4dn.12xlarge p3.8xlarge) +expected=(g3.16xlarge g4ad.16xlarge g4dn.12xlarge g5.12xlarge g5.24xlarge g6.12xlarge g6.24xlarge g6e.12xlarge g6e.24xlarge p3.8xlarge) params=( "--gpus=4" "--gpus-min=4 --gpus-max=4" diff --git a/test/license-test/Dockerfile b/test/license-test/Dockerfile index 2b346b0..08ed3f9 100644 --- a/test/license-test/Dockerfile +++ b/test/license-test/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/golang:1.17 +FROM public.ecr.aws/docker/library/golang:1.23 WORKDIR /app diff --git a/test/readme-test/readme-codeblocks.go b/test/readme-test/readme-codeblocks.go index 32cea4a..0fadb5f 100644 --- a/test/readme-test/readme-codeblocks.go +++ b/test/readme-test/readme-codeblocks.go @@ -5,7 +5,6 @@ import ( "encoding/json" "flag" "fmt" - "io/ioutil" "log" "os" "strings" @@ -59,7 +58,7 @@ func main() { } func compareBlockWithFile(codeBlock string, codePath string) bool { - fileContents, err := ioutil.ReadFile(codePath) + fileContents, err := os.ReadFile(codePath) if err != nil { log.Fatalf("Unable to read file contents at %s", codePath) } diff --git a/test/readme-test/spellcheck-Dockerfile b/test/readme-test/spellcheck-Dockerfile index cd36641..974f58a 100644 --- a/test/readme-test/spellcheck-Dockerfile +++ b/test/readme-test/spellcheck-Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.16 +FROM golang:1.23 RUN go install github.com/client9/misspell/cmd/misspell@v0.3.4