From 5fd2af202d614bc29d5aedd96c21f7cf51932bd4 Mon Sep 17 00:00:00 2001 From: Brandon Wagner Date: Sun, 13 Oct 2024 19:18:28 -0400 Subject: [PATCH] fetch spot current price to speed up loading data without caching --- .github/workflows/ci.yaml | 2 +- Dockerfile | 2 +- README.md | 12 +- THIRD_PARTY_LICENSES | 4 +- cmd/main.go | 17 +- go.mod | 67 ++++--- go.sum | 179 +++++++----------- pkg/ec2pricing/ec2pricing.go | 19 +- pkg/ec2pricing/ec2pricing_test.go | 9 +- pkg/ec2pricing/odpricing.go | 52 ++--- pkg/ec2pricing/spotpricing.go | 16 +- pkg/instancetypes/instancetypes.go | 29 ++- pkg/selector/comparators.go | 30 ++- .../outputs/bubbletea_internal_test.go | 8 +- pkg/selector/outputs/outputs.go | 2 +- pkg/selector/outputs/outputs_test.go | 4 +- pkg/selector/selector.go | 157 ++++++++------- pkg/selector/selector_test.go | 8 +- pkg/selector/services.go | 2 +- pkg/selector/types.go | 18 +- pkg/sorter/sorter_test.go | 4 +- test/license-test/Dockerfile | 2 +- test/readme-test/readme-codeblocks.go | 3 +- test/readme-test/spellcheck-Dockerfile | 2 +- 24 files changed, 333 insertions(+), 315 deletions(-) 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 @@

A CLI tool and go library which recommends instance types based on resource criteria like vcpus and memory.

- + go-version @@ -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..3136665 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. 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/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