Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- `--min-savings-pct` now rejects values below 0 or above 100 at validation.
Previously a negative value was accepted as no filter and a value above 100
silently dropped every recommendation (#2161).
- Replaying or retrying an `--input-csv` purchase no longer re-buys an
ElastiCache or MemoryDB reservation that the first run already bought: CSV
rows now carry the AWS provider, and MemoryDB rows use the engine AWS reports
for existing nodes whatever the CSV says, so the duplicate check matches
them. CSV Savings Plan rows also now match `--include-regions` and
`--exclude-regions` on their plan region (#2228).
- `configure-gcp`: skipping (or giving an unknown answer at) the "Create Service
Account" step no longer continues with the default
`cudly-service-account@<project>` email that was never created. The wizard now
Expand Down
4 changes: 3 additions & 1 deletion cmd/aws_fixture_stub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type awsFixtureStub struct {
t *testing.T
mu sync.Mutex
received map[string]int
forms map[string][]url.Values // request forms per "host operation", in arrival order
fixtures map[string]awsFixture
log *bytes.Buffer
}
Expand Down Expand Up @@ -81,6 +82,7 @@ func (s *awsFixtureStub) RoundTrip(r *http.Request) (*http.Response, error) {
op := awsOperation(r, form)
s.mu.Lock()
s.received[r.URL.Host+" "+op]++
s.forms[r.URL.Host+" "+op] = append(s.forms[r.URL.Host+" "+op], form)
s.mu.Unlock()
svc, _, _ := strings.Cut(r.URL.Host, ".")
fx, ok := s.fixtures[svc+" "+op]
Expand Down Expand Up @@ -108,7 +110,7 @@ func (s *awsFixtureStub) RoundTrip(r *http.Request) (*http.Response, error) {
// no environment.
func newAWSFixtureConfig(t *testing.T) (aws.Config, *awsFixtureStub) {
t.Helper()
stub := &awsFixtureStub{t: t, received: map[string]int{}, fixtures: maps.Clone(awsFixtures), log: captureAppLog(t)}
stub := &awsFixtureStub{t: t, received: map[string]int{}, forms: map[string][]url.Values{}, fixtures: maps.Clone(awsFixtures), log: captureAppLog(t)}
return aws.Config{
Region: "us-east-1",
HTTPClient: &http.Client{Transport: stub},
Expand Down
385 changes: 385 additions & 0 deletions cmd/csv_real_purchase_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cmd/multi_service_csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func parseCSVRecords(reader *csv.Reader, colIdx map[string]int) ([]common.Recomm

// parseCSVRecord parses a single CSV record into a Recommendation.
func parseCSVRecord(record []string, colIdx map[string]int) (common.Recommendation, error) {
rec := common.Recommendation{}
rec := common.Recommendation{Provider: common.ProviderAWS}

// Parse string fields
rec.Service = common.ServiceType(getCSVField(record, colIdx, "Service"))
Expand Down
7 changes: 7 additions & 0 deletions cmd/multi_service_csv_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strconv"

"github.com/LeanerCloud/cloud-commitments-go/pkg/common"
"github.com/LeanerCloud/cloud-commitments-go/providers/aws/services/memorydb"
"github.com/LeanerCloud/cloud-commitments-go/providers/aws/services/savingsplans"
)

Expand Down Expand Up @@ -49,6 +50,12 @@ func csvDetails(rec common.Recommendation, record []string, colIdx map[string]in
if _, isSP := savingsplans.PlanTypeForServiceType(rec.Service); isSP {
return csvSavingsPlanDetails(record, colIdx)
}
if rec.Service == common.ServiceMemoryDB {
// MemoryDB reservations are engine-agnostic for purchase and the
// inventory reports every node as ReservedNodeEngine, so the CSV engine
// (blank, valkey, ...) must not reach the dedupe key or a replay re-buys.
return &common.CacheDetails{Engine: memorydb.ReservedNodeEngine, NodeType: rec.ResourceType}, nil
}
engine := getCSVField(record, colIdx, "Engine")
if engine == "" {
return nil, nil
Expand Down
6 changes: 3 additions & 3 deletions cmd/multi_service_filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ func shouldIncludePoolSize(rec *common.Recommendation, cfg *Config) bool {
// Plans are fetched once, not per region (determineRegionsForService), so
// --regions never reaches them through the fetch loop. An unknown region
// does not match, so it is dropped rather than bought outside the scope.
// Not applied in --input-csv mode: CSV rows carry no Provider or
// CommitmentType, so their Details.Region is not readable here, and the rows
// are used as supplied.
// In --input-csv mode, rows carry Provider (AWS) but no CommitmentType, so
// EffectiveRegion reads Details.Region for --include/--exclude-regions.
// --regions is not applied to CSV Savings Plans, which are used as supplied.
func shouldIncludeRecommendationRegion(rec *common.Recommendation, cfg *Config) bool {
if awsprovider.IsRegionAgnostic(*rec) {
return true
Expand Down
34 changes: 34 additions & 0 deletions cmd/multi_service_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,37 @@ savings-plans-ec2instance,,,1,1yr,no-upfront,EC2Instance,1.5,m5,us-east-1
got := applyFilters(recs, &cfg, nil, nil, "", common.NewDropSummary())
assert.Len(t, got, 3)
}

// TestApplyFilters_SavingsPlansIncludeRegionsCSVMode pins that a CSV Savings
// Plan row with an empty Region is matched by --include/--exclude-regions on
// its plan region (Details.Region), as on the main path, while --regions alone
// still leaves it alone (#2228).
func TestApplyFilters_SavingsPlansIncludeRegionsCSVMode(t *testing.T) {
csvPath := filepath.Join(t.TempDir(), "sp.csv")
content := `Service,Region,ResourceType,Count,Term,PaymentOption,PlanType,HourlyCommitment,InstanceFamily,DetailsRegion
savings-plans-ec2instance,,,1,1yr,no-upfront,EC2Instance,1.5,m5,us-east-1
`
require.NoError(t, os.WriteFile(csvPath, []byte(content), 0o600))
recs, err := loadRecommendationsFromCSV(csvPath)
require.NoError(t, err)
require.Len(t, recs, 1)
require.Empty(t, recs[0].Region)

tests := []struct {
name string
cfg Config
want int
}{
{"include another region drops it", Config{IncludeRegions: []string{"eu-west-1"}}, 0},
{"include its region keeps it", Config{IncludeRegions: []string{"us-east-1"}}, 1},
{"exclude its region drops it", Config{ExcludeRegions: []string{"us-east-1"}}, 0},
{"--regions alone does not drop it", Config{Regions: []string{"eu-west-1"}}, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.cfg.CSVInput = csvPath
got := applyFilters(recs, &tt.cfg, nil, nil, "", common.NewDropSummary())
assert.Len(t, got, tt.want)
})
}
}
2 changes: 1 addition & 1 deletion cmd/savings_plan_purchase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ func TestProcessCSVRegionPurchases_SavingsPlanRowFromWrittenCSV(t *testing.T) {
require.NoError(t, err)
require.Len(t, recs, 1)
require.Empty(t, recs[0].Region)
require.Empty(t, recs[0].Provider)
require.Equal(t, common.ProviderAWS, recs[0].Provider)

_, results, ok, _ := processCSVRegionPurchases(context.Background(), awsCfg, recs[0].Service, recs[0].Region, recs, false, cfg, "run-1")

Expand Down
Loading