Skip to content

Commit 8308c36

Browse files
authored
allow-list and deny-list implemented (#11)
* allow-list and deny-list impl * update usage with allow-list and deny-list * improved test failure text * fixed test failure text
1 parent 934973d commit 8308c36

File tree

10 files changed

+207
-5
lines changed

10 files changed

+207
-5
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,14 @@ ec2-instance-selector --vcpus 4 --region us-east-2 --availability-zones us-east-
134134
ec2-instance-selector --memory-min 4096 --memory-max 8192 --vcpus-min 4 --vcpus-max 8 --region us-east-2
135135
136136
Filter Flags:
137+
--allow-list string List of allowed instance types to select from w/ regex syntax (Example: m[3-5]\.*)
137138
--availability-zone string [DEPRECATED] use --availability-zones instead
138139
-z, --availability-zones strings Availability zones or zone ids to check EC2 capacity offered in specific AZs
139140
--baremetal Bare Metal instance types (.metal instances)
140141
-b, --burst-support Burstable instance types
141142
-a, --cpu-architecture string CPU architecture [x86_64, i386, or arm64]
142143
--current-generation Current generation instance types (explicitly set this to false to not return current generation instance types)
144+
--deny-list string List of instance types which should be excluded w/ regex syntax (Example: m[1-2]\.*)
143145
-e, --ena-support Instance types where ENA is supported or required
144146
-f, --fpga-support FPGA instance types
145147
--gpu-memory-total int Number of GPUs' total memory in MiB (Example: 4096) (sets --gpu-memory-total-min and -max to the same value)

cmd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const (
6767
currentGeneration = "current-generation"
6868
networkInterfaces = "network-interfaces"
6969
networkPerformance = "network-performance"
70+
allowList = "allow-list"
71+
denyList = "deny-list"
7072
)
7173

7274
// Aggregate Filter Flags
@@ -133,6 +135,8 @@ Full docs can be found at github.com/aws/amazon-` + binName
133135
cli.BoolFlag(currentGeneration, nil, nil, "Current generation instance types (explicitly set this to false to not return current generation instance types)")
134136
cli.IntMinMaxRangeFlags(networkInterfaces, nil, nil, "Number of network interfaces (ENIs) that can be attached to the instance")
135137
cli.IntMinMaxRangeFlags(networkPerformance, nil, nil, "Bandwidth in Gib/s of network performance (Example: 100)")
138+
cli.RegexFlag(allowList, nil, nil, "List of allowed instance types to select from w/ regex syntax (Example: m[3-5]\\.*)")
139+
cli.RegexFlag(denyList, nil, nil, "List of instance types which should be excluded w/ regex syntax (Example: m[1-2]\\.*)")
136140

137141
// Suite Flags - higher level aggregate filters that return opinionated result
138142

@@ -204,6 +208,8 @@ Full docs can be found at github.com/aws/amazon-` + binName
204208
MaxResults: cli.IntMe(flags[maxResults]),
205209
NetworkInterfaces: cli.IntRangeMe(flags[networkInterfaces]),
206210
NetworkPerformance: cli.IntRangeMe(flags[networkPerformance]),
211+
AllowList: cli.RegexMe(flags[allowList]),
212+
DenyList: cli.RegexMe(flags[denyList]),
207213
InstanceTypeBase: cli.StringMe(flags[instanceTypeBase]),
208214
}
209215

pkg/cli/cli_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,23 @@ func TestParseAndValidateFlags(t *testing.T) {
293293
_, err := cli.ParseAndValidateFlags()
294294
h.Nok(t, err)
295295
}
296+
297+
func TestParseAndValidateRegexFlag(t *testing.T) {
298+
flagName := "test-regex-flag"
299+
flagArg := fmt.Sprintf("--%s", flagName)
300+
301+
cli := getTestCLI()
302+
cli.RegexFlag(flagName, nil, nil, "Test with validation")
303+
os.Args = []string{"ec2-instance-selector", flagArg, "c4.*"}
304+
flags, err := cli.ParseAndValidateFlags()
305+
h.Ok(t, err)
306+
h.Assert(t, len(flags) == 1, "1 flag should have been processed")
307+
_, err = cli.ParseAndValidateFlags()
308+
h.Ok(t, err)
309+
310+
cli = getTestCLI()
311+
cli.RegexFlag(flagName, nil, nil, "Test with validation")
312+
os.Args = []string{"ec2-instance-selector", flagArg, "(("}
313+
_, err = cli.ParseAndValidateFlags()
314+
h.Nok(t, err)
315+
}

pkg/cli/flags.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ func (cl *CommandLineInterface) RatioFlag(name string, shorthand *string, defaul
2121
}
2222
if shorthand != nil {
2323
cl.Flags[name] = cl.Command.Flags().StringP(name, string(*shorthand), *defaultValue, description)
24-
return nil
24+
} else {
25+
cl.Flags[name] = cl.Command.Flags().String(name, *defaultValue, description)
2526
}
26-
cl.Flags[name] = cl.Command.Flags().String(name, *defaultValue, description)
27+
2728
cl.validators[name] = func(val interface{}) error {
2829
if val == nil {
2930
return nil
@@ -62,11 +63,15 @@ func (cl *CommandLineInterface) StringFlag(name string, shorthand *string, defau
6263
}
6364

6465
// StringSliceFlag creates and registers a flag accepting a list of strings.
65-
// Suite flags will be grouped in the middle of the output --help
6666
func (cl *CommandLineInterface) StringSliceFlag(name string, shorthand *string, defaultValue []string, description string) {
6767
cl.StringSliceFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description)
6868
}
6969

70+
// RegexFlag creates and registers a flag accepting a string and validates that it is a valid regex.
71+
func (cl *CommandLineInterface) RegexFlag(name string, shorthand *string, defaultValue *string, description string) {
72+
cl.RegexFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description)
73+
}
74+
7075
// StringOptionsFlag creates and registers a flag accepting a string and valid options for use in validation.
7176
func (cl *CommandLineInterface) StringOptionsFlag(name string, shorthand *string, defaultValue *string, description string, validOpts []string) {
7277
cl.StringOptionsFlagOnFlagSet(cl.Command.Flags(), name, shorthand, defaultValue, description, validOpts)
@@ -188,9 +193,9 @@ func (cl *CommandLineInterface) StringFlagOnFlagSet(flagSet *pflag.FlagSet, name
188193
if shorthand != nil {
189194
cl.Flags[name] = flagSet.StringP(name, string(*shorthand), *defaultValue, description)
190195
cl.validators[name] = validationFn
191-
return
196+
} else {
197+
cl.Flags[name] = flagSet.String(name, *defaultValue, description)
192198
}
193-
cl.Flags[name] = flagSet.String(name, *defaultValue, description)
194199
cl.validators[name] = validationFn
195200
}
196201

@@ -223,3 +228,36 @@ func (cl *CommandLineInterface) StringSliceFlagOnFlagSet(flagSet *pflag.FlagSet,
223228
}
224229
cl.Flags[name] = flagSet.StringSlice(name, defaultValue, description)
225230
}
231+
232+
// RegexFlagOnFlagSet creates and registers a flag accepting a string slice of regular expressions.
233+
func (cl *CommandLineInterface) RegexFlagOnFlagSet(flagSet *pflag.FlagSet, name string, shorthand *string, defaultValue *string, description string) {
234+
if defaultValue == nil {
235+
cl.nilDefaults[name] = true
236+
defaultValue = cl.StringMe("")
237+
}
238+
if shorthand != nil {
239+
cl.Flags[name] = flagSet.StringP(name, string(*shorthand), *defaultValue, description)
240+
} else {
241+
cl.Flags[name] = flagSet.String(name, *defaultValue, description)
242+
}
243+
cl.validators[name] = func(val interface{}) error {
244+
if val == nil {
245+
return nil
246+
}
247+
regexStringVal := ""
248+
switch v := val.(type) {
249+
case *string:
250+
regexStringVal = *v
251+
case *regexp.Regexp:
252+
return nil
253+
default:
254+
return fmt.Errorf("Invalid regex input for --%s", name)
255+
}
256+
regexVal, err := regexp.Compile(regexStringVal)
257+
if err != nil {
258+
return fmt.Errorf("Invalid regex input for --%s", name)
259+
}
260+
cl.Flags[name] = regexVal
261+
return nil
262+
}
263+
}

pkg/cli/flags_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,19 @@ func TestIntMinMaxRangeFlags(t *testing.T) {
150150
h.Assert(t, len(cli.Flags) == 3, "Should contain 3 flags w/ no shorthand")
151151
h.Assert(t, ok, "Should contain %s flag w/ no shorthand", flagName)
152152
}
153+
154+
func TestRegexFlag(t *testing.T) {
155+
cli := getTestCLI()
156+
for _, flagFn := range []func(string, *string, *string, string){cli.RegexFlag} {
157+
flagName := "test-regex"
158+
flagFn(flagName, cli.StringMe("t"), nil, "Test Regex")
159+
_, ok := cli.Flags[flagName]
160+
h.Assert(t, len(cli.Flags) == 1, "Should contain 1 flag")
161+
h.Assert(t, ok, "Should contain %s flag", flagName)
162+
163+
cli = getTestCLI()
164+
flagFn(flagName, nil, nil, "Test Regex")
165+
h.Assert(t, len(cli.Flags) == 1, "Should contain 1 flag w/ no shorthand")
166+
h.Assert(t, ok, "Should contain %s flag w/ no shorthand", flagName)
167+
}
168+
}

pkg/cli/types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package cli
1616

1717
import (
1818
"log"
19+
"regexp"
1920

2021
"github.com/aws/amazon-ec2-instance-selector/pkg/selector"
2122
"github.com/spf13/cobra"
@@ -166,3 +167,20 @@ func (*CommandLineInterface) StringSliceMe(i interface{}) *[]string {
166167
return nil
167168
}
168169
}
170+
171+
// RegexMe takes an interface and returns a pointer to a regex
172+
// If the underlying interface kind is not regexp.Regexp or *regexp.Regexp then nil is returned
173+
func (*CommandLineInterface) RegexMe(i interface{}) *regexp.Regexp {
174+
if i == nil {
175+
return nil
176+
}
177+
switch v := i.(type) {
178+
case *regexp.Regexp:
179+
return v
180+
case regexp.Regexp:
181+
return &v
182+
default:
183+
log.Printf("%s cannot be converted to a regexp", i)
184+
return nil
185+
}
186+
}

pkg/cli/types_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package cli_test
1515

1616
import (
1717
"reflect"
18+
"regexp"
1819
"testing"
1920

2021
"github.com/aws/amazon-ec2-instance-selector/pkg/selector"
@@ -105,3 +106,17 @@ func TestIntRangeMe(t *testing.T) {
105106
val = cli.IntRangeMe(nil)
106107
h.Assert(t, val == nil, "Should return nil if nil is passed in")
107108
}
109+
110+
func TestRegexMe(t *testing.T) {
111+
cli := getTestCLI()
112+
regexVal, err := regexp.Compile("c4.*")
113+
h.Ok(t, err)
114+
val := cli.RegexMe(*regexVal)
115+
h.Assert(t, val.String() == regexVal.String(), "Should return %s from passed in regex value", regexVal)
116+
val = cli.RegexMe(regexVal)
117+
h.Assert(t, val.String() == regexVal.String(), "Should return %s from passed in regex pointer", regexVal)
118+
val = cli.RegexMe(true)
119+
h.Assert(t, val == nil, "Should return nil from other data type passed in")
120+
val = cli.RegexMe(nil)
121+
h.Assert(t, val == nil, "Should return nil if nil is passed in")
122+
}

pkg/selector/selector.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package selector
1717
import (
1818
"fmt"
1919
"reflect"
20+
"regexp"
2021
"sort"
2122
"strings"
2223

@@ -59,6 +60,8 @@ const (
5960
currentGeneration = "currentGeneration"
6061
networkInterfaces = "networkInterfaces"
6162
networkPerformance = "networkPerformance"
63+
allowList = "allowList"
64+
denyList = "denyList"
6265

6366
// AggregateLowPercentile is the default lower percentile for resource ranges on similar instance type comparisons
6467
AggregateLowPercentile = 0.8
@@ -223,6 +226,10 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error)
223226
networkPerformance: {filters.NetworkPerformance, getNetworkPerformance(instanceTypeInfo.NetworkInfo.NetworkPerformance)},
224227
}
225228

229+
if isInDenyList(filters.DenyList, instanceTypeName) || !isInAllowList(filters.AllowList, instanceTypeName) {
230+
delete(instanceTypeCandidates, instanceTypeName)
231+
}
232+
226233
if !isSupportedInLocation(locationInstanceOfferings, instanceTypeName) {
227234
delete(instanceTypeCandidates, instanceTypeName)
228235
}
@@ -403,3 +410,17 @@ func isSupportedInLocation(instanceOfferings map[string]string, instanceType str
403410
_, ok := instanceOfferings[instanceType]
404411
return ok
405412
}
413+
414+
func isInDenyList(denyRegex *regexp.Regexp, instanceTypeName string) bool {
415+
if denyRegex == nil {
416+
return false
417+
}
418+
return denyRegex.MatchString(instanceTypeName)
419+
}
420+
421+
func isInAllowList(allowRegex *regexp.Regexp, instanceTypeName string) bool {
422+
if allowRegex == nil {
423+
return true
424+
}
425+
return allowRegex.MatchString(instanceTypeName)
426+
}

pkg/selector/selector_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io/ioutil"
2121
"log"
22+
"regexp"
2223
"strconv"
2324
"testing"
2425

@@ -496,3 +497,60 @@ func TestRetrieveInstanceTypesSupportedInAZs_DescribeAZErr(t *testing.T) {
496497
_, err := itf.RetrieveInstanceTypesSupportedInLocations([]string{"us-east-2a"})
497498
h.Nok(t, err)
498499
}
500+
501+
func TestFilter_AllowList(t *testing.T) {
502+
ec2Mock := mockedEC2{
503+
DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp,
504+
DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp,
505+
}
506+
itf := selector.Selector{
507+
EC2: ec2Mock,
508+
}
509+
allowRegex, err := regexp.Compile("c4.large")
510+
h.Ok(t, err)
511+
filters := selector.Filters{
512+
AllowList: allowRegex,
513+
}
514+
results, err := itf.Filter(filters)
515+
h.Ok(t, err)
516+
h.Assert(t, len(results) == 1, "Allow List Regex: 'c4.large' should return 1 instance type")
517+
}
518+
519+
func TestFilter_DenyList(t *testing.T) {
520+
ec2Mock := mockedEC2{
521+
DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp,
522+
DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp,
523+
}
524+
itf := selector.Selector{
525+
EC2: ec2Mock,
526+
}
527+
denyRegex, err := regexp.Compile("c4.large")
528+
h.Ok(t, err)
529+
filters := selector.Filters{
530+
DenyList: denyRegex,
531+
}
532+
results, err := itf.Filter(filters)
533+
h.Ok(t, err)
534+
h.Assert(t, len(results) == 24, "Deny List Regex: 'c4.large' should return 24 instance type matching regex but returned %d", len(results))
535+
}
536+
537+
func TestFilter_AllowAndDenyList(t *testing.T) {
538+
ec2Mock := mockedEC2{
539+
DescribeInstanceTypesPagesResp: setupMock(t, describeInstanceTypesPages, "25_instances.json").DescribeInstanceTypesPagesResp,
540+
DescribeInstanceTypeOfferingsResp: setupMock(t, describeInstanceTypeOfferings, "us-east-2a.json").DescribeInstanceTypeOfferingsResp,
541+
}
542+
itf := selector.Selector{
543+
EC2: ec2Mock,
544+
}
545+
allowRegex, err := regexp.Compile("c4.*")
546+
h.Ok(t, err)
547+
denyRegex, err := regexp.Compile("c4.large")
548+
h.Ok(t, err)
549+
filters := selector.Filters{
550+
AllowList: allowRegex,
551+
DenyList: denyRegex,
552+
}
553+
results, err := itf.Filter(filters)
554+
h.Ok(t, err)
555+
h.Assert(t, len(results) == 4, "Allow/Deny List Regex: 'c4.large' should return 4 instance types matching the regex but returned %d", len(results))
556+
}

pkg/selector/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
package selector
1515

1616
import (
17+
"regexp"
18+
1719
"github.com/aws/aws-sdk-go/service/ec2"
1820
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
1921
)
@@ -133,6 +135,12 @@ type Filters struct {
133135
// VcpusToMemoryRatio is a ratio of vcpus to memory expressed as a floating point
134136
VCpusToMemoryRatio *float64
135137

138+
// AllowList is a regex of allowed instance types
139+
AllowList *regexp.Regexp
140+
141+
// DenyList is a regex of excluded instance types
142+
DenyList *regexp.Regexp
143+
136144
// InstanceTypeBase is a base instance type which is used to retrieve similarly spec'd instance types
137145
InstanceTypeBase *string
138146
}

0 commit comments

Comments
 (0)