Skip to content

Commit fac4741

Browse files
authored
add caching for pricing and instance type data (#110)
1 parent e042985 commit fac4741

27 files changed

+1226
-729
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ jobs:
2929

3030
- name: Lints
3131
run: make spellcheck shellcheck
32-
33-
- name: Go Report Card Tests
34-
run: make go-report-card-test
3532

3633
- name: Brew Sync Dry run
3734
run: make homebrew-sync-dry-run

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM public.ecr.aws/bitnami/golang:1.17 as builder
1+
FROM golang:1.17 as builder
22

33
## GOLANG env
44
ARG GOPROXY="https://proxy.golang.org|direct"

Makefile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,6 @@ image:
6262
license-test:
6363
${MAKEFILE_PATH}/test/license-test/run-license-test.sh
6464

65-
go-report-card-test:
66-
${MAKEFILE_PATH}/test/go-report-card-test/run-report-card-test.sh
67-
6865
spellcheck:
6966
${MAKEFILE_PATH}/test/readme-test/run-readme-spellcheck
7067

@@ -110,7 +107,7 @@ build: compile
110107

111108
release: build-binaries build-docker-images push-docker-images upload-resources-to-github
112109

113-
test: spellcheck shellcheck unit-test license-test go-report-card-test e2e-test output-validation-test readme-codeblock-test
110+
test: spellcheck shellcheck unit-test license-test e2e-test output-validation-test readme-codeblock-test
114111

115112
help:
116113
@grep -E '^[a-zA-Z_-]+:.*$$' $(MAKEFILE_LIST) | sort

THIRD_PARTY_LICENSES

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Copyright 2014-2015 Stripe, Inc.
66
Copyright © 2015 Steve Francia <spf@spf13.com>
77
** go-yaml/yaml; version v2.2.2 -- https://github.com/go-yaml/yaml
88
Copyright 2011-2016 Canonical Ltd.
9-
** go-ini/ini; version v1.57.0 -- https://github.com/go-ini/ini
10-
9+
** gopkg.in/ini.v1; version v1.57.0 -- https://gopkg.in/ini.v1
10+
** gopkg.in/yaml.v2; version v2.3.0 -- https://gopkg.in/yaml.v2
1111

1212
Licensed under the Apache License, Version 2.0 (the "License");
1313
you may not use this file except in compliance with the License.
@@ -309,6 +309,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
309309

310310
** go-ghodss-yaml; version v1.0.0 -- https://github.com/ghodss/yaml
311311
Copyright (c) 2014 Sam Ghods
312+
** go-cache; version v2.1.0 -- https://github.com/patrickmn/go-cache
313+
Copyright (c) 2012-2019 Patrick Mylund Nielsen and the go-cache contributors
314+
** multierr; version v1.1.0 -- https://go.uber.org/multierr
315+
Copyright (c) 2017-2021 Uber Technologies, Inc.
316+
** atomic; version v1.4.0 -- https://go.uber.org/atomic
317+
Copyright (c) 2017-2021 Uber Technologies, Inc.
318+
312319

313320
The MIT License (MIT)
314321

cmd/main.go

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@ import (
1717
"fmt"
1818
"log"
1919
"os"
20+
"os/signal"
2021
"strings"
2122
"sync"
23+
"syscall"
24+
"time"
2225

2326
commandline "github.com/aws/amazon-ec2-instance-selector/v2/pkg/cli"
27+
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/env"
2428
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector"
2529
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector/outputs"
2630
"github.com/aws/aws-sdk-go/aws/session"
2731
homedir "github.com/mitchellh/go-homedir"
2832
"github.com/spf13/cobra"
33+
"go.uber.org/multierr"
2934
"gopkg.in/ini.v1"
3035
)
3136

@@ -35,6 +40,7 @@ const (
3540
defaultRegionEnvVar = "AWS_DEFAULT_REGION"
3641
defaultProfile = "default"
3742
awsConfigFile = "~/.aws/config"
43+
spotPricingDaysBack = 30
3844

3945
// cfnJSON is an output type
4046
cfnJSON = "cfn-json"
@@ -90,6 +96,8 @@ const (
9096
version = "version"
9197
region = "region"
9298
output = "output"
99+
cacheTTL = "cache-ttl"
100+
cacheDir = "cache-dir"
93101
)
94102

95103
var (
@@ -126,7 +134,7 @@ Full docs can be found at github.com/aws/amazon-` + binName
126134
cli.IntMinMaxRangeFlags(vcpus, cli.StringMe("c"), nil, "Number of vcpus available to the instance type.")
127135
cli.ByteQuantityMinMaxRangeFlags(memory, cli.StringMe("m"), nil, "Amount of Memory available (Example: 4 GiB)")
128136
cli.RatioFlag(vcpusToMemoryRatio, nil, nil, "The ratio of vcpus to GiBs of memory. (Example: 1:2)")
129-
cli.StringOptionsFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64/amd64, i386, or arm64]", []string{"x86_64", "amd64", "i386", "arm64"})
137+
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"})
130138
cli.IntMinMaxRangeFlags(gpus, cli.StringMe("g"), nil, "Total Number of GPUs (Example: 4)")
131139
cli.ByteQuantityMinMaxRangeFlags(gpuMemoryTotal, nil, nil, "Number of GPUs' total memory (Example: 4 GiB)")
132140
cli.StringOptionsFlag(placementGroupStrategy, nil, nil, "Placement group strategy: [cluster, partition, spread]", []string{"cluster", "partition", "spread"})
@@ -156,10 +164,12 @@ Full docs can be found at github.com/aws/amazon-` + binName
156164

157165
// Configuration Flags - These will be grouped at the bottom of the help flags
158166

159-
cli.ConfigIntFlag(maxResults, nil, cli.IntMe(20), "The maximum number of instance types that match your criteria to return")
167+
cli.ConfigIntFlag(maxResults, nil, env.WithDefaultInt("EC2_INSTANCE_SELECTOR_MAX_RESULTS", 20), "The maximum number of instance types that match your criteria to return")
160168
cli.ConfigStringFlag(profile, nil, nil, "AWS CLI profile to use for credentials and config", nil)
161169
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)
162170
cli.ConfigStringFlag(output, cli.StringMe("o"), nil, fmt.Sprintf("Specify the output format (%s)", strings.Join(cliOutputTypes, ", ")), nil)
171+
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.")
172+
cli.ConfigPathFlag(cacheDir, nil, env.WithDefaultString("EC2_INSTANCE_SELECTOR_CACHE_DIR", "~/.ec2-instance-selector/"), "Directory to save the pricing and instance type caches")
163173
cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs")
164174
cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help")
165175
cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version")
@@ -186,31 +196,36 @@ Full docs can be found at github.com/aws/amazon-` + binName
186196
os.Exit(1)
187197
}
188198
flags[region] = sess.Config.Region
189-
190-
instanceSelector := selector.New(sess)
199+
cacheTTLDuration := time.Hour * time.Duration(*cli.IntMe(flags[cacheTTL]))
200+
instanceSelector := selector.NewWithCache(sess, cacheTTLDuration, *cli.StringMe(flags[cacheDir]))
201+
shutdown := func() {
202+
if err := instanceSelector.Save(); err != nil {
203+
log.Printf("There was an error saving pricing caches: %v", err)
204+
}
205+
}
206+
registerShutdown(shutdown)
191207
outputFlag := cli.StringMe(flags[output])
192208
if outputFlag != nil && *outputFlag == tableWideOutput {
193209
// If output type is `table-wide`, simply print both prices for better comparison,
194210
// even if the actual filter is applied on any one of those based on usage class
195-
196-
// Save time by hydrating in parallel
197-
wg := &sync.WaitGroup{}
198-
wg.Add(2)
199-
go func(waitGroup *sync.WaitGroup) {
200-
defer waitGroup.Done()
201-
_ = instanceSelector.EC2Pricing.HydrateOndemandCache()
202-
}(wg)
203-
go func(waitGroup *sync.WaitGroup) {
204-
defer waitGroup.Done()
205-
_ = instanceSelector.EC2Pricing.HydrateSpotCache(30)
206-
}(wg)
207-
wg.Wait()
211+
// Save time by hydrating all caches in parallel
212+
if err := hydrateCaches(*instanceSelector); err != nil {
213+
log.Printf("%v", err)
214+
}
208215
} else if flags[pricePerHour] != nil {
209216
// Else, if price filters are applied, only hydrate the respective cache as we don't have to print the prices
210217
if flags[usageClass] == nil || *cli.StringMe(flags[usageClass]) == "on-demand" {
211-
_ = instanceSelector.EC2Pricing.HydrateOndemandCache()
218+
if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
219+
if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil {
220+
log.Printf("There was a problem refreshing the on-demand pricing cache: %v", err)
221+
}
222+
}
212223
} else {
213-
_ = instanceSelector.EC2Pricing.HydrateSpotCache(30)
224+
if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
225+
if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil {
226+
log.Printf("There was a problem refreshing the spot pricing cache: %v", err)
227+
}
228+
}
214229
}
215230
}
216231

@@ -290,6 +305,46 @@ Full docs can be found at github.com/aws/amazon-` + binName
290305
if itemsTruncated > 0 {
291306
log.Printf("%d entries were truncated, increase --%s to see more", itemsTruncated, maxResults)
292307
}
308+
shutdown()
309+
}
310+
311+
func hydrateCaches(instanceSelector selector.Selector) (errs error) {
312+
wg := &sync.WaitGroup{}
313+
hydrateTasks := []func(*sync.WaitGroup) error{
314+
func(waitGroup *sync.WaitGroup) error {
315+
defer waitGroup.Done()
316+
if instanceSelector.EC2Pricing.OnDemandCacheCount() == 0 {
317+
if err := instanceSelector.EC2Pricing.RefreshOnDemandCache(); err != nil {
318+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the on-demand pricing cache: %w", err))
319+
}
320+
}
321+
return nil
322+
},
323+
func(waitGroup *sync.WaitGroup) error {
324+
defer waitGroup.Done()
325+
if instanceSelector.EC2Pricing.SpotCacheCount() == 0 {
326+
if err := instanceSelector.EC2Pricing.RefreshSpotCache(spotPricingDaysBack); err != nil {
327+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the spot pricing cache: %w", err))
328+
}
329+
}
330+
return nil
331+
},
332+
func(waitGroup *sync.WaitGroup) error {
333+
defer waitGroup.Done()
334+
if instanceSelector.InstanceTypesProvider.CacheCount() == 0 {
335+
if _, err := instanceSelector.InstanceTypesProvider.Get(nil); err != nil {
336+
return multierr.Append(errs, fmt.Errorf("There was a problem refreshing the instance types cache: %w", err))
337+
}
338+
}
339+
return nil
340+
},
341+
}
342+
wg.Add(len(hydrateTasks))
343+
for _, task := range hydrateTasks {
344+
go task(wg)
345+
}
346+
wg.Wait()
347+
return errs
293348
}
294349

295350
func getOutputFn(outputFlag *string, currentFn selector.InstanceTypesOutputFn) selector.InstanceTypesOutputFn {
@@ -377,3 +432,12 @@ func getProfileRegion(profileName string) (string, error) {
377432
}
378433
return regionConfig.String(), nil
379434
}
435+
436+
func registerShutdown(shutdown func()) {
437+
sigs := make(chan os.Signal, 1)
438+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
439+
go func() {
440+
<-sigs
441+
shutdown()
442+
}()
443+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/hashicorp/hcl v1.0.0
1010
github.com/imdario/mergo v0.3.11
1111
github.com/mitchellh/go-homedir v1.1.0
12+
github.com/patrickmn/go-cache v2.1.0+incompatible
1213
github.com/spf13/cobra v0.0.7
1314
github.com/spf13/pflag v1.0.3
1415
go.uber.org/multierr v1.1.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
7575
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
7676
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
7777
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
78+
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
79+
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
7880
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
7981
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
8082
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

pkg/cli/flags.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/bytequantity"
11+
"github.com/mitchellh/go-homedir"
1112
"github.com/spf13/pflag"
1213
)
1314

@@ -90,6 +91,11 @@ func (cl *CommandLineInterface) RegexFlag(name string, shorthand *string, defaul
9091
cl.RegexFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description)
9192
}
9293

94+
// PathFlag creates and registers a flag accepting a string representing a path and validates that it is a valid path.
95+
func (cl *CommandLineInterface) PathFlag(name string, shorthand *string, defaultValue *string, description string) {
96+
cl.PathFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description)
97+
}
98+
9399
// StringOptionsFlag creates and registers a flag accepting a string and valid options for use in validation.
94100
func (cl *CommandLineInterface) StringOptionsFlag(name string, shorthand *string, defaultValue *string, description string, validOpts []string) {
95101
cl.StringOptionsFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description, validOpts)
@@ -107,11 +113,17 @@ func (cl *CommandLineInterface) ConfigStringFlag(name string, shorthand *string,
107113
}
108114

109115
// ConfigStringSliceFlag creates and registers a flag accepting a list of strings.
110-
// Suite flags will be grouped in the middle of the output --help
116+
// Config flags will be grouped at the bottom in the output of --help
111117
func (cl *CommandLineInterface) ConfigStringSliceFlag(name string, shorthand *string, defaultValue []string, description string) {
112118
cl.StringSliceFlagOnFlagSet(cl.Command.PersistentFlags(), name, shorthand, defaultValue, description)
113119
}
114120

121+
// ConfigPathFlag creates and registers a flag accepting a string representing a path and validates that it is a valid path.
122+
// Config flags will be grouped at the bottom in the output of --help
123+
func (cl *CommandLineInterface) ConfigPathFlag(name string, shorthand *string, defaultValue *string, description string) {
124+
cl.PathFlagOnFlagSet(cl.Command.PersistentFlags(), name, shorthand, defaultValue, description)
125+
}
126+
115127
// ConfigIntFlag creates and registers a flag accepting an Integer for configuration purposes.
116128
// Config flags will be grouped at the bottom in the output of --help
117129
func (cl *CommandLineInterface) ConfigIntFlag(name string, shorthand *string, defaultValue *int, description string) {
@@ -378,3 +390,25 @@ func (cl *CommandLineInterface) RegexFlagOnFlagSet(flagSet *pflag.FlagSet, name
378390
}
379391
cl.StringFlagOnFlagSet(flagSet, name, shorthand, defaultValue, description, regexProcessor, regexValidator)
380392
}
393+
394+
// PathFlagOnFlagSet creates and registers a flag accepting a string as a path
395+
func (cl *CommandLineInterface) PathFlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *string, description string) {
396+
invalidInputMsg := fmt.Sprintf("Invalid path input for --%s. ", name)
397+
pathProcessor := func(val interface{}) error {
398+
if val == nil {
399+
return nil
400+
}
401+
switch v := val.(type) {
402+
case *string:
403+
path, err := homedir.Expand(*v)
404+
if err != nil {
405+
return fmt.Errorf(invalidInputMsg + "Unable to expand path.")
406+
}
407+
cl.Flags[name] = &path
408+
default:
409+
return fmt.Errorf(invalidInputMsg + "Input type is unsupported.")
410+
}
411+
return nil
412+
}
413+
cl.StringFlagOnFlagSet(flagSet, name, shorthand, defaultValue, description, pathProcessor, nil)
414+
}

0 commit comments

Comments
 (0)