Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion cmd/spotinfo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@
stdioTransport = "stdio"
sseTransport = "sse"
defaultMCPPort = "8080"

// Recommendation command defaults
defaultTopRecommendations = 3
)

//nolint:cyclop
Expand Down Expand Up @@ -240,9 +243,9 @@
switch ctx.String("output") {
case "number":
printAdvicesNumber(advices, printRegion, output)
case "text":

Check failure on line 246 in cmd/spotinfo/main.go

View workflow job for this annotation

GitHub Actions / test

string `text` has 2 occurrences, but such constant `outputText` already exists (goconst)
printAdvicesText(advices, printRegion, output)
case "json":

Check failure on line 248 in cmd/spotinfo/main.go

View workflow job for this annotation

GitHub Actions / test

string `json` has 2 occurrences, but such constant `outputJSON` already exists (goconst)
printAdvicesJSON(advices, output)
case "table":
printAdvicesTable(advices, false, printRegion, output)
Expand Down Expand Up @@ -680,8 +683,27 @@
},
Name: "spotinfo",
Usage: "explore AWS EC2 Spot instances",
Action: mainCmd,
Version: Version,
Commands: []*cli.Command{
{
Name: "recommend",
Usage: "recommend instances for your workload",
Action: recommendCmd,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "workload",
Usage: "workload type: web|batch|ml|ci",
Value: "batch",
},
&cli.IntFlag{
Name: "top",
Usage: "number of top recommendations to show",
Value: defaultTopRecommendations,
},
},
},
},
Action: mainCmd,

Check failure on line 706 in cmd/spotinfo/main.go

View workflow job for this annotation

GitHub Actions / test

File is not properly formatted (gci)
}
cli.VersionPrinter = func(_ *cli.Context) {
fmt.Printf("spotinfo %s\n", Version)
Expand Down
209 changes: 209 additions & 0 deletions cmd/spotinfo/recommend_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"

"github.com/jedib0t/go-pretty/v6/table"
"github.com/urfave/cli/v2"

"spotinfo/internal/spot"
)

// recommendCmd handles the recommend subcommand.
func recommendCmd(ctx *cli.Context) error {
client := spot.New()
return execRecommendCmd(ctx, context.Background(), client, os.Stdout)
}

//nolint:cyclop // Complex command with many branches for output formats
func execRecommendCmd(ctx *cli.Context, execCtx context.Context, client spotClient, output io.Writer) error {
// Parse flags
vCPU := ctx.Int("cpu")
memory := ctx.Int("memory")
regions := ctx.StringSlice("region")
workload := ctx.String("workload")
topN := ctx.Int("top")
outputFormat := ctx.String("output")
maxPrice := ctx.Float64("price")
sortBy := ctx.String("sort")
budget := ctx.String("budget")

// Validate workload
if !isValidWorkload(workload) {
return fmt.Errorf("invalid workload type: %s (valid: web, batch, ml, ci)", workload)
}

// Validate topN
if topN <= 0 {
topN = 3
}

// Build options for GetSpotSavings
var opts []spot.GetSpotSavingsOption

Check failure on line 48 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

File is not properly formatted (gci)
opts = append(opts, spot.WithRegions(regions))

if vCPU > 0 {
opts = append(opts, spot.WithCPU(vCPU))
}
if memory > 0 {
opts = append(opts, spot.WithMemory(memory))
}
if maxPrice > 0 {
opts = append(opts, spot.WithMaxPrice(maxPrice))
}

// Set sort order if specified
sortByType := spot.SortByRange // default
switch sortBy {
case sortPrice:
sortByType = spot.SortByPrice
case sortSavings:
sortByType = spot.SortBySavings
case sortRegion:
sortByType = spot.SortByRegion
case sortScore:
sortByType = spot.SortByScore
case sortInterruption:
sortByType = spot.SortByRange
}
opts = append(opts, spot.WithSort(sortByType, false))

// Get spot savings data
advices, err := client.GetSpotSavings(execCtx, opts...)
if err != nil {
return fmt.Errorf("failed to get spot savings: %w", err)
}

if len(advices) == 0 {
fmt.Fprintln(output, "No matching instances found")

Check failure on line 84 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

Error return value of `fmt.Fprintln` is not checked (errcheck)
return nil
}

// Create recommendation engine
engine := spot.NewRecommendationEngine(workload)

// Generate recommendations
rec := engine.Recommend(advices, topN)

// Filter by interruption tolerance if requested
filtered := engine.FilterByInterruptionTolerance(rec.Candidates)
if len(filtered) > 0 {
rec.Candidates = filtered
}

// Format and output
switch strings.ToLower(outputFormat) {
case "json":
return outputRecommendationJSON(rec, output)
case "table", "text":
return outputRecommendationTable(rec, output, workload, vCPU, memory, budget)
case "csv":
return outputRecommendationCSV(rec, output)
default:
return outputRecommendationTable(rec, output, workload, vCPU, memory, budget)
}
}

// outputRecommendationJSON outputs recommendations in JSON format.
func outputRecommendationJSON(rec *spot.Recommendation, output io.Writer) error {
data := map[string]interface{}{
"workload": rec.Workload,
"candidates": rec.Candidates,
"summary": rec.Summary,
}

encoder := json.NewEncoder(output)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}

// outputRecommendationTable outputs recommendations in human-readable table format.
func outputRecommendationTable(rec *spot.Recommendation, output io.Writer, workload string, vCPU, memory int, budget string) error {
const scorePercentage = 100
const interruptionPercentage = 100

// Print header
fmt.Fprintf(output, "\n")

Check failure on line 132 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

Error return value of `fmt.Fprintf` is not checked (errcheck)
fmt.Fprintf(output, "RECOMMENDATION for: %s", workload)

Check failure on line 133 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

Error return value of `fmt.Fprintf` is not checked (errcheck)
if vCPU > 0 {
fmt.Fprintf(output, " / %d vCPU", vCPU)

Check failure on line 135 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

Error return value of `fmt.Fprintf` is not checked (errcheck)
}
if memory > 0 {
fmt.Fprintf(output, " / %d GB", memory)
}
if budget != "" {
fmt.Fprintf(output, " / budget %s/hr", budget)
}
fmt.Fprintf(output, "\n\n")

// Create table
t := table.NewWriter()
t.SetOutputMirror(output)
t.AppendHeader(table.Row{"RANK", "INSTANCE", "SCORE", "PRICE", "SAVINGS", "INTERRUPTION", "WHY"})

for _, item := range rec.Candidates {
t.AppendRow(table.Row{
item.Rank,
item.Instance,
fmt.Sprintf("%.1f", item.Score*scorePercentage),
fmt.Sprintf("$%.3f", item.Price),
fmt.Sprintf("%d%%", item.Savings),
fmt.Sprintf("%.1f%%", item.InterruptionRate*interruptionPercentage),
item.Rationale,
})

// Alternate row colors for readability
if item.Rank%2 == 0 {
t.AppendRow(table.Row{"", "", "", "", "", "", ""})
}
}

t.SetStyle(table.StyleLight)
t.Render()

// Print summary
fmt.Fprintf(output, "\n%s\n\n", rec.Summary)

return nil
}

// outputRecommendationCSV outputs recommendations in CSV format.
func outputRecommendationCSV(rec *spot.Recommendation, output io.Writer) error {
const scorePercentage = 100
const interruptionPercentage = 100

// Header
fmt.Fprintln(output, "Rank,Instance,Score,Price,Savings,InterruptionRate,Rationale")

Check failure on line 182 in cmd/spotinfo/recommend_cmd.go

View workflow job for this annotation

GitHub Actions / test

Error return value of `fmt.Fprintln` is not checked (errcheck)

// Data rows
for _, item := range rec.Candidates {
fmt.Fprintf(output, "%d,%s,%.1f,%.3f,%d,%.1f,\"%s\"\n",
item.Rank,
item.Instance,
item.Score*scorePercentage,
item.Price,
item.Savings,
item.InterruptionRate*interruptionPercentage,
item.Rationale,
)
}

return nil
}

// isValidWorkload checks if the workload type is valid.
func isValidWorkload(workload string) bool {
validWorkloads := []string{"web", "batch", "ml", "ci"}
for _, valid := range validWorkloads {
if strings.EqualFold(workload, valid) {
return true
}
}
return false
}
Loading
Loading