Skip to content

Commit

Permalink
support multiple|all regions query
Browse files Browse the repository at this point in the history
  • Loading branch information
alexei-led committed Apr 27, 2021
1 parent be030cb commit 5ce5774
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 77 deletions.
60 changes: 41 additions & 19 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
)

const (
regionColumn = "Region"
instanceTypeColumn = "Instance Info"
vCpuColumn = "vCPU"
memoryColumn = "Memory GiB"
Expand All @@ -45,7 +46,7 @@ func mainCmd(c *cli.Context) error {
if v := mainCtx.Value("key"); v != nil {
log.Printf("context value = %v", v)
}
region := c.String("region")
regions := c.StringSlice("region")
instanceOS := c.String("os")
instance := c.String("type")
cpu := c.Int("cpu")
Expand All @@ -64,45 +65,58 @@ func mainCmd(c *cli.Context) error {
sort = spot.SortBySavings
case "price":
sort = spot.SortByPrice
case "region":
sort = spot.SortByRegion
default:
sort = spot.SortByRange
}
// get spot savings
advices, err := spot.GetSpotSavings(instance, region, instanceOS, cpu, memory, maxPrice, sort, sortDesc)
advices, err := spot.GetSpotSavings(regions, instance, instanceOS, cpu, memory, maxPrice, sort, sortDesc)
// decide if region should be printed
printRegion := len(regions) > 1 || (len(regions) == 1 && regions[0] == "all")
if err != nil {
return err
}
switch c.String("output") {
case "number":
printAdvicesNumber(advices)
printAdvicesNumber(advices, printRegion)
case "text":
printAdvicesText(advices)
printAdvicesText(advices, printRegion)
case "json":
printAdvicesJson(advices)
case "table":
printAdvicesTable(advices, false)
printAdvicesTable(advices, false, printRegion)
case "csv":
printAdvicesTable(advices, true)
printAdvicesTable(advices, true, printRegion)
default:
printAdvicesNumber(advices)
printAdvicesNumber(advices, printRegion)
}
return nil
}

func printAdvicesText(advices []spot.Advice) {
func printAdvicesText(advices []spot.Advice, region bool) {
for _, advice := range advices {
fmt.Printf("%s vCPU=%d, memory=%vGiB, saving=%d%%, interruption='%s'\n",
advice.Instance, advice.Info.Cores, advice.Info.Ram, advice.Savings, advice.Range.Label)
if region {
fmt.Printf("region=%s, type=%s, vCPU=%d, memory=%vGiB, saving=%d%%, interruption='%s', price=%.2f\n",
advice.Region, advice.Instance, advice.Info.Cores, advice.Info.Ram, advice.Savings, advice.Range.Label, advice.Price)
} else {
fmt.Printf("type=%s, vCPU=%d, memory=%vGiB, saving=%d%%, interruption='%s', price=%.2f\n",
advice.Instance, advice.Info.Cores, advice.Info.Ram, advice.Savings, advice.Range.Label, advice.Price)
}
}
}

func printAdvicesNumber(advices []spot.Advice) {
func printAdvicesNumber(advices []spot.Advice, region bool) {
if len(advices) == 1 {
fmt.Println(advices[0].Savings)
return
}
for _, advice := range advices {
fmt.Printf("%s: %d\n", advice.Instance, advice.Savings)
if region {
fmt.Printf("%s/%s: %d\n", advice.Region, advice.Instance, advice.Savings)
} else {
fmt.Printf("%s: %d\n", advice.Instance, advice.Savings)
}
}
}

Expand All @@ -117,12 +131,20 @@ func printAdvicesJson(advices interface{}) {
fmt.Println(txt)
}

func printAdvicesTable(advices []spot.Advice, csv bool) {
func printAdvicesTable(advices []spot.Advice, csv, region bool) {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{instanceTypeColumn, vCpuColumn, memoryColumn, savingsColumn, interruptionColumn, priceColumn})
header := table.Row{instanceTypeColumn, vCpuColumn, memoryColumn, savingsColumn, interruptionColumn, priceColumn}
if region {
header = append(table.Row{regionColumn}, header...)
}
t.AppendHeader(header)
for _, advice := range advices {
t.AppendRow([]interface{}{advice.Instance, advice.Info.Cores, advice.Info.Ram, advice.Savings, advice.Range.Label, advice.Price})
row := table.Row{advice.Instance, advice.Info.Cores, advice.Info.Ram, advice.Savings, advice.Range.Label, advice.Price}
if region {
row = append(table.Row{advice.Region}, row...)
}
t.AppendRow(row)
}
// render as CSV
if csv {
Expand Down Expand Up @@ -174,10 +196,10 @@ func main() {
Usage: "instance operating system (windows/linux)",
Value: "linux",
},
&cli.StringFlag{
&cli.StringSliceFlag{
Name: "region",
Usage: "AWS region",
Value: "us-east-1",
Usage: "set one or more AWS regions, use \"all\" for all AWS regions",
Value: cli.NewStringSlice("us-east-1"),
},
&cli.StringFlag{
Name: "output",
Expand All @@ -198,7 +220,7 @@ func main() {
},
&cli.StringFlag{
Name: "sort",
Usage: "sort results by interruption|type|savings|price",
Usage: "sort results by interruption|type|savings|price|region",
Value: "interruption",
},
&cli.StringFlag{
Expand Down
130 changes: 80 additions & 50 deletions public/spot/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
SortByInstance = iota
SortBySavings = iota
SortByPrice = iota
SortByRegion = iota
spotAdvisorJsonUrl = "https://spot-bid-advisor.s3.amazonaws.com/spot-advisor-data.json"
)

Expand Down Expand Up @@ -76,11 +77,13 @@ type TypeInfo instanceType

// Advice - spot price advice: interruption range and savings
type Advice struct {
Instance string
Range Range
Savings int
Info TypeInfo
Price float64
Region string
Instance string
Range Range
Savings int
Info TypeInfo
Price float64
ZonePrice map[string]float64
}

// ByRange implements sort.Interface based on the Range.Min field
Expand All @@ -94,23 +97,30 @@ func (a ByRange) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type ByInstance []Advice

func (a ByInstance) Len() int { return len(a) }
func (a ByInstance) Less(i, j int) bool { return strings.Compare(a[i].Instance, a[j].Instance) <= 0 }
func (a ByInstance) Less(i, j int) bool { return strings.Compare(a[i].Instance, a[j].Instance) == -1 }
func (a ByInstance) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// By implements sort.Interface based on the Savings field
// BySavings implements sort.Interface based on the Savings field
type BySavings []Advice

func (a BySavings) Len() int { return len(a) }
func (a BySavings) Less(i, j int) bool { return a[i].Savings < a[j].Savings }
func (a BySavings) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// By implements sort.Interface based on the Price field
// ByPrice implements sort.Interface based on the Price field
type ByPrice []Advice

func (a ByPrice) Len() int { return len(a) }
func (a ByPrice) Less(i, j int) bool { return a[i].Price < a[j].Price }
func (a ByPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// ByRegion implements sort.Interface based on the Region field
type ByRegion []Advice

func (a ByRegion) Len() int { return len(a) }
func (a ByRegion) Less(i, j int) bool { return strings.Compare(a[i].Region, a[j].Region) == -1 }
func (a ByRegion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

func dataLazyLoad(url string, timeout time.Duration, fallbackData string) (*advisorData, error) {
var result advisorData
// try to load new data
Expand Down Expand Up @@ -142,61 +152,79 @@ fallback:
return &result, nil
}

func GetSpotSavings(pattern, region, instanceOS string, cpu, memory int, price float64, sortBy int, sortDesc bool) ([]Advice, error) {
func GetSpotSavings(regions []string, pattern, instanceOS string, cpu, memory int, price float64, sortBy int, sortDesc bool) ([]Advice, error) {
var err error
loadDataOnce.Do(func() {
data, err = dataLazyLoad(spotAdvisorJsonUrl, 10*time.Second, embeddedSpotData)
})
if err != nil {
return nil, errors.Wrap(err, "failed to load spot data")
}
r, ok := data.Regions[region]
if !ok {
return nil, fmt.Errorf("no spot price for region %s", region)
}
var advices map[string]advice
if strings.EqualFold("windows", instanceOS) {
advices = r.Windows
} else if strings.EqualFold("linux", instanceOS) {
advices = r.Linux
} else {
return nil, errors.New("invalid instance OS, must be windows/linux")
// special case: "all" regions (slice with single element)
if len(regions) == 1 && regions[0] == "all" {
// replace regions with all available regions
regions = make([]string, 0, len(data.Regions))
for k := range data.Regions {
regions = append(regions, k)
}
}

// construct advices result
// get advices for specified regions
var result []Advice
for instance, adv := range advices {
// match instance type name
matched, err := regexp.MatchString(pattern, instance)
if err != nil {
return nil, errors.Wrap(err, "failed to match instance type")
for _, region := range regions {
r, ok := data.Regions[region]
if !ok {
return nil, fmt.Errorf("no spot price for region %s", region)
}
if !matched { // skip not matched
continue
var advices map[string]advice
if strings.EqualFold("windows", instanceOS) {
advices = r.Windows
} else if strings.EqualFold("linux", instanceOS) {
advices = r.Linux
} else {
return nil, errors.New("invalid instance OS, must be windows/linux")
}
// filter by min vCPU and memory
info := data.InstanceTypes[instance]
if (cpu != 0 && info.Cores < cpu) || (memory != 0 && info.Ram < float32(memory)) {
continue
}
// get price details
spotPrice, err := getSpotInstancePrice(instance, region, instanceOS, false)
if err != nil {
// skip this error
}
// filter by max price
if price != 0 && spotPrice > price {
continue
}
// prepare record
rng := Range{
Label: data.Ranges[adv.Range].Label,
Max: data.Ranges[adv.Range].Max,
Min: minRange[data.Ranges[adv.Range].Max],

// construct advices result
for instance, adv := range advices {
// match instance type name
matched, err := regexp.MatchString(pattern, instance)
if err != nil {
return nil, errors.Wrap(err, "failed to match instance type")
}
if !matched { // skip not matched
continue
}
// filter by min vCPU and memory
info := data.InstanceTypes[instance]
if (cpu != 0 && info.Cores < cpu) || (memory != 0 && info.Ram < float32(memory)) {
continue
}
// get price details
spotPrice, err := getSpotInstancePrice(instance, region, instanceOS, false)
if err != nil {
// skip this error
}
// filter by max price
if price != 0 && spotPrice > price {
continue
}
// prepare record
rng := Range{
Label: data.Ranges[adv.Range].Label,
Max: data.Ranges[adv.Range].Max,
Min: minRange[data.Ranges[adv.Range].Max],
}
result = append(result, Advice{
Region: region,
Instance: instance,
Range: rng,
Savings: adv.Savings,
Info: TypeInfo(info),
Price: spotPrice,
})
}
result = append(result, Advice{instance, rng, adv.Savings, TypeInfo(info), spotPrice})
}
// sort by - range (default)
// sort results by - range (default)
var data sort.Interface
switch sortBy {
case SortByRange:
Expand All @@ -207,6 +235,8 @@ func GetSpotSavings(pattern, region, instanceOS string, cpu, memory int, price f
data = BySavings(result)
case SortByPrice:
data = ByPrice(result)
case SortByRegion:
data = ByRegion(result)
default:
data = ByRange(result)
}
Expand Down
Loading

0 comments on commit 5ce5774

Please sign in to comment.