Skip to content

Commit 8fea12d

Browse files
authored
Support AZs list (#8)
* add availability-zones to perform an intersection of availabile instance types across multiple zones * refactor location validation * refactor location count increment
1 parent 93964b8 commit 8fea12d

File tree

8 files changed

+273
-60
lines changed

8 files changed

+273
-60
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,12 @@ Usage:
130130
ec2-instance-selector [flags]
131131
132132
Examples:
133-
ec2-instance-selector --vcpus 4 --region us-east-2 --availability-zone us-east-2b
133+
ec2-instance-selector --vcpus 4 --region us-east-2 --availability-zones us-east-2b
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-
-z, --availability-zone string Availability zone or zone id to check only EC2 capacity offered in a specific AZ
137+
--availability-zone string [DEPRECATED] use --availability-zones instead
138+
-z, --availability-zones strings Availability zones or zone ids to check EC2 capacity offered in specific AZs
138139
--baremetal Bare Metal instance types (.metal instances)
139140
-b, --burst-support Burstable instance types
140141
-a, --cpu-architecture string CPU architecture [x86_64, i386, or arm64]

cmd/main.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656
burstSupport = "burst-support"
5757
hypervisor = "hypervisor"
5858
availabilityZone = "availability-zone"
59+
availabilityZones = "availability-zones"
5960
currentGeneration = "current-generation"
6061
networkInterfaces = "network-interfaces"
6162
networkPerformance = "network-performance"
@@ -90,7 +91,7 @@ func main() {
9091
longUsage := binName + ` is a CLI tool to filter EC2 instance types based on resource criteria.
9192
Filtering allows you to select all the instance types that match your application requirements.
9293
Full docs can be found at github.com/aws/amazon-` + binName
93-
examples := fmt.Sprintf(`%s --vcpus 4 --region us-east-2 --availability-zone us-east-2b
94+
examples := fmt.Sprintf(`%s --vcpus 4 --region us-east-2 --availability-zones us-east-2b
9495
%s --memory-min 4096 --memory-max 8192 --vcpus-min 4 --vcpus-max 8 --region us-east-2`, binName, binName)
9596

9697
runFunc := func(cmd *cobra.Command, args []string) {}
@@ -120,7 +121,8 @@ Full docs can be found at github.com/aws/amazon-` + binName
120121
cli.BoolFlag(fpgaSupport, cli.StringMe("f"), nil, "FPGA instance types")
121122
cli.BoolFlag(burstSupport, cli.StringMe("b"), nil, "Burstable instance types")
122123
cli.StringFlag(hypervisor, nil, nil, "Hypervisor: [xen or nitro]", nil)
123-
cli.StringFlag(availabilityZone, cli.StringMe("z"), nil, "Availability zone or zone id to check only EC2 capacity offered in a specific AZ", nil)
124+
cli.StringFlag(availabilityZone, nil, nil, "[DEPRECATED] use --availability-zones instead", nil)
125+
cli.StringSliceFlag(availabilityZones, cli.StringMe("z"), nil, "Availability zones or zone ids to check EC2 capacity offered in specific AZs")
124126
cli.BoolFlag(currentGeneration, nil, nil, "Current generation instance types (explicitly set this to false to not return current generation instance types)")
125127
cli.IntMinMaxRangeFlags(networkInterfaces, nil, nil, "Number of network interfaces (ENIs) that can be attached to the instance")
126128
cli.IntMinMaxRangeFlags(networkPerformance, nil, nil, "Bandwidth in Gib/s of network performance (Example: 100)")
@@ -164,6 +166,15 @@ Full docs can be found at github.com/aws/amazon-` + binName
164166
sessOpts.Profile = *cli.StringMe(flags[profile])
165167
}
166168

169+
if flags[availabilityZone] != nil {
170+
log.Printf("You are using a deprecated flag --%s which will be removed in future versions, switch to --%s to avoid issues.\n", availabilityZone, availabilityZones)
171+
if flags[availabilityZones] != nil {
172+
flags[availabilityZones] = append(*cli.StringSliceMe(flags[availabilityZones]), *cli.StringMe(flags[availabilityZone]))
173+
} else {
174+
flags[availabilityZones] = []string{*cli.StringMe(flags[availabilityZone])}
175+
}
176+
}
177+
167178
sess := session.Must(session.NewSessionWithOptions(sessOpts))
168179

169180
instanceSelector := selector.New(sess)
@@ -185,7 +196,7 @@ Full docs can be found at github.com/aws/amazon-` + binName
185196
Fpga: cli.BoolMe(flags[fpgaSupport]),
186197
Burstable: cli.BoolMe(flags[burstSupport]),
187198
Region: cli.StringMe(flags[region]),
188-
AvailabilityZone: cli.StringMe(flags[availabilityZone]),
199+
AvailabilityZones: cli.StringSliceMe(flags[availabilityZones]),
189200
CurrentGeneration: cli.BoolMe(flags[currentGeneration]),
190201
MaxResults: cli.IntMe(flags[maxResults]),
191202
NetworkInterfaces: cli.IntRangeMe(flags[networkInterfaces]),

pkg/selector/selector.go

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package selector
1717
import (
1818
"fmt"
1919
"reflect"
20-
"regexp"
2120
"sort"
2221
"strings"
2322

@@ -34,12 +33,6 @@ var (
3433
)
3534

3635
const (
37-
// regionNameRegex Matches strings like: us-east-1 or us-east-2
38-
regionNameRegex = `^[a-z]{2,3}\-([a-z]{1,10}\-)?[a-z]{1,10}\-[1-9]`
39-
// zoneIDRegex Matches strings like: use1-az1 or use2-az3
40-
zoneIDRegex = `^[a-z]{3}[1-9]{1}\-az[1-9]$`
41-
// zoneNameRegex Matches strings like: us-east-1a or us-east-2c
42-
zoneNameRegex = `^[a-z]{2,3}\-([a-z]{1,10}\-)?[a-z]{1,10}\-[1-9][a-z]$`
4336
locationFilterKey = "location"
4437
zoneIDLocationType = "availability-zone-id"
4538
zoneNameLocationType = "availability-zone"
@@ -175,13 +168,23 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error)
175168
if err != nil {
176169
return nil, err
177170
}
178-
var location string
171+
var locations []string
172+
173+
// Support the deprecated singular availabilityZone filter in favor of the plural
179174
if filters.AvailabilityZone != nil {
180-
location = *filters.AvailabilityZone
175+
if filters.AvailabilityZones != nil {
176+
*filters.AvailabilityZones = append(*filters.AvailabilityZones, *filters.AvailabilityZone)
177+
} else {
178+
filters.AvailabilityZones = &[]string{*filters.AvailabilityZone}
179+
}
180+
}
181+
182+
if filters.AvailabilityZones != nil {
183+
locations = *filters.AvailabilityZones
181184
} else if filters.Region != nil {
182-
location = *filters.Region
185+
locations = []string{*filters.Region}
183186
}
184-
locationInstanceOfferings, err := itf.RetrieveInstanceTypesSupportedInLocation(location)
187+
locationInstanceOfferings, err := itf.RetrieveInstanceTypesSupportedInLocations(locations)
185188
if err != nil {
186189
return nil, err
187190
}
@@ -329,41 +332,68 @@ func (itf Selector) executeFilters(filterToInstanceSpecMapping map[string]filter
329332
return true, nil
330333
}
331334

332-
// RetrieveInstanceTypesSupportedInLocation returns a map of instance type -> AZ or Region for all instance types supported in the location passed in
335+
// RetrieveInstanceTypesSupportedInLocations returns a map of instance type -> AZ or Region for all instance types supported in the intersected locations passed in
333336
// The location can be a zone-id (ie. use1-az1), a zone-name (us-east-1a), or a region name (us-east-1).
334337
// Note that zone names are not necessarily the same across accounts
335-
func (itf Selector) RetrieveInstanceTypesSupportedInLocation(zone string) (map[string]string, error) {
336-
if zone == "" {
338+
func (itf Selector) RetrieveInstanceTypesSupportedInLocations(locations []string) (map[string]string, error) {
339+
if len(locations) == 0 {
337340
return nil, nil
338341
}
339-
availableInstanceTypes := map[string]string{}
340-
instanceTypeOfferingsInput := &ec2.DescribeInstanceTypeOfferingsInput{
341-
Filters: []*ec2.Filter{
342-
{
343-
Name: aws.String(locationFilterKey),
344-
Values: []*string{aws.String(zone)},
342+
availableInstanceTypes := map[string]int{}
343+
for _, location := range locations {
344+
instanceTypeOfferingsInput := &ec2.DescribeInstanceTypeOfferingsInput{
345+
Filters: []*ec2.Filter{
346+
{
347+
Name: aws.String(locationFilterKey),
348+
Values: []*string{aws.String(location)},
349+
},
345350
},
346-
},
347-
}
348-
if isZoneID, _ := regexp.MatchString(zoneIDRegex, zone); isZoneID {
349-
instanceTypeOfferingsInput.SetLocationType(zoneIDLocationType)
350-
} else if isZoneName, _ := regexp.MatchString(zoneNameRegex, zone); isZoneName {
351-
instanceTypeOfferingsInput.SetLocationType(zoneNameLocationType)
352-
} else if isRegion, _ := regexp.MatchString(regionNameRegex, zone); isRegion {
353-
instanceTypeOfferingsInput.SetLocationType(regionNameLocationType)
354-
} else {
355-
return nil, fmt.Errorf("The location passed in (%s) is not a valid zone-id, zone-name, or region name", zone)
351+
}
352+
locationType, err := itf.getLocationType(location)
353+
if err != nil {
354+
return nil, err
355+
}
356+
instanceTypeOfferingsInput.SetLocationType(locationType)
357+
358+
err = itf.EC2.DescribeInstanceTypeOfferingsPages(instanceTypeOfferingsInput, func(page *ec2.DescribeInstanceTypeOfferingsOutput, lastPage bool) bool {
359+
for _, instanceType := range page.InstanceTypeOfferings {
360+
if i, ok := availableInstanceTypes[*instanceType.InstanceType]; !ok {
361+
availableInstanceTypes[*instanceType.InstanceType] = 1
362+
} else {
363+
availableInstanceTypes[*instanceType.InstanceType] = i + 1
364+
}
365+
}
366+
return true
367+
})
368+
if err != nil {
369+
return nil, fmt.Errorf("Encountered an error when describing instance type offerings: %w", err)
370+
}
356371
}
357-
err := itf.EC2.DescribeInstanceTypeOfferingsPages(instanceTypeOfferingsInput, func(page *ec2.DescribeInstanceTypeOfferingsOutput, lastPage bool) bool {
358-
for _, instanceType := range page.InstanceTypeOfferings {
359-
availableInstanceTypes[*instanceType.InstanceType] = *instanceType.Location
372+
availableInstanceTypesAllLocations := map[string]string{}
373+
for instanceType, locationsSupported := range availableInstanceTypes {
374+
if locationsSupported == len(locations) {
375+
availableInstanceTypesAllLocations[instanceType] = ""
360376
}
361-
return true
362-
})
377+
}
378+
379+
return availableInstanceTypesAllLocations, nil
380+
}
381+
382+
func (itf Selector) getLocationType(location string) (string, error) {
383+
azs, err := itf.EC2.DescribeAvailabilityZones(&ec2.DescribeAvailabilityZonesInput{})
363384
if err != nil {
364-
return nil, fmt.Errorf("Encountered an error when describing instance type offerings: %w", err)
385+
return "", err
386+
}
387+
for _, zone := range azs.AvailabilityZones {
388+
if location == *zone.RegionName {
389+
return regionNameLocationType, nil
390+
} else if location == *zone.ZoneName {
391+
return zoneNameLocationType, nil
392+
} else if location == *zone.ZoneId {
393+
return zoneIDLocationType, nil
394+
}
365395
}
366-
return availableInstanceTypes, nil
396+
return "", fmt.Errorf("The location passed in (%s) is not a valid zone-id, zone-name, or region name", location)
367397
}
368398

369399
func isSupportedInLocation(instanceOfferings map[string]string, instanceType string) bool {

0 commit comments

Comments
 (0)