Skip to content

fix(aws/savingsplans): purchase EC2Instance SP for the recommended family/region, fail loud on ambiguity (adversarial-review C1) - #1450

Merged
cristim merged 1 commit into
mainfrom
fix/sp-purchase-wrong-family
Jul 17, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/sp-purchase-wrong-family

Conversation

@cristim

@cristim cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member

Money Impact

An EC2Instance Savings Plan purchase could silently commit to the wrong instance family on a 1-3 year commitment. When Cost Explorer recommended (for example) an m5 SP in us-east-1, the purchase path discarded the family/region metadata and returned the lexicographically-smallest offering ID across all instance families in the client's configured region -- potentially buying a c6g commitment instead, or in a different region if the client region differed from the CE recommendation.

This is a real-money, silent defect: the API returns 200/OK, the commitment is created, and the audit log shows "success" -- all against the wrong workload.

Root Cause

Three gaps in the code path, each independently necessary for the bug:

  1. Parser drops CE's routing data: parseSavingsPlanDetail (parser_sp.go) discarded detail.SavingsPlansDetails entirely, so InstanceFamily, Region, and OfferingId were never persisted onto common.SavingsPlanDetails.

  2. No instanceFamily filter at purchase: buildSPOfferingsInput applied only a region filter (and used the client's configured region, not the rec's region), so DescribeSavingsPlansOfferings returned offerings for all instance families.

  3. Silent winner selection: lookupOfferingID returned the lexicographically-smallest ID from all results -- no validation that a single family/region was returned.

Fix

  • pkg/common/types.go: Add InstanceFamily, Region, OfferingID fields to SavingsPlanDetails (omitempty; backward-compatible).

  • providers/aws/recommendations/parser_sp.go: Extract and persist all three fields from CE's SavingsPlansDetails for EC2Instance SPs. Compute/SageMaker/Database SPs stay family-agnostic (zero values).

  • providers/aws/services/savingsplans/client.go:

    • findOfferingID: when CE provides OfferingID, use it directly (safest path, no API call needed).
    • buildSPOfferingsInput: apply instanceFamily + region filters for EC2Instance SPs using the rec's own values from CE (not the client's configured region).
    • New lookupEC2OfferingIDStrict: collects full offering structs, extracts Properties, validates the result set spans exactly one instance family and one region -- errors loud if ambiguous or empty. Never picks silently.
    • lookupOfferingID (Compute/SageMaker/Database path): unchanged behavior, now delegates pagination to shared collectSPOfferings helper.

Regression Tests (all must fail pre-fix, pass post-fix)

  • TestEC2InstanceSP_ResolvesCorrectFamilyAndRegion: mock only matches when request carries BOTH instanceFamily=m5 AND region=us-east-1 filters. Pre-fix: mock never matches (no instanceFamily filter sent).
  • TestEC2InstanceSP_MultiFamilyResponseFailsLoud: multi-family API response now errors; pre-fix silently returned the lex-smallest (c6g).
  • TestEC2InstanceSP_MultiRegionResponseFailsLoud: same for multi-region ambiguity.
  • TestEC2InstanceSP_CEProvidedOfferingIDUsedDirectly: OfferingID fast path; pre-fix the field didn't exist.
  • TestParseSavingsPlanDetail_EC2InstanceFieldsCaptured: 4 sub-cases verifying the parser captures all three CE fields correctly.

Test plan

  • go build ./... in providers/aws and pkg modules: clean
  • go vet ./...: clean
  • gocyclo -over 10: clean (all functions under threshold)
  • go test ./... in providers/aws: 1097 pass
  • go test ./... in pkg: 704 pass
  • All 5 new C1 regression tests pass
  • All pre-existing savingsplans client tests (66) pass unchanged

🤖 Generated with claude-flow

…mily/region, fail loud on ambiguity (adversarial-review C1)

Prior to this fix, an EC2Instance Savings Plan purchase could silently commit
to the wrong instance family. The Cost Explorer SavingsPlansDetails struct
carries InstanceFamily, Region, and OfferingId per-recommendation, but
parseSavingsPlanDetail discarded all three. At purchase time,
buildSPOfferingsInput applied only a region filter (from the client's
configured region, which can differ from the rec's region), with no
instanceFamily filter, so lookupOfferingID returned the
lexicographically-smallest offering ID across ALL instance families in the
region -- potentially buying c6g when CE recommended m5 on a 1-3 year
commitment.

Fix:
- pkg/common: add InstanceFamily, Region, OfferingID fields to SavingsPlanDetails
- parser_sp.go: extract and persist those fields from detail.SavingsPlansDetails
  for EC2Instance SPs only (Compute/SageMaker/Database remain family-agnostic)
- client.go (savingsplans):
  * findOfferingID: when CE provides OfferingID, use it directly (safest path)
  * buildSPOfferingsInput: accept instanceFamily + recRegion; apply both as
    DescribeSavingsPlansOfferings filters for EC2Instance SPs, using the
    rec's region (not client config region)
  * new lookupEC2OfferingIDStrict: validates result set spans exactly one
    family and one region; fails loud (error, no purchase) if ambiguous or empty
  * Compute/SageMaker/Database SP path unchanged (lookupOfferingID)

Regression tests:
- TestEC2InstanceSP_ResolvesCorrectFamilyAndRegion: mock only matches when
  both instanceFamily and region filters are present -- fails pre-fix
- TestEC2InstanceSP_MultiFamilyResponseFailsLoud: multi-family response
  now errors instead of silently picking lex-smallest -- fails pre-fix
- TestEC2InstanceSP_MultiRegionResponseFailsLoud: same for multi-region
- TestEC2InstanceSP_CEProvidedOfferingIDUsedDirectly: direct OfferingID path
- TestParseSavingsPlanDetail_EC2InstanceFieldsCaptured: 4 sub-cases
  verifying parser captures all three fields -- fails pre-fix
@cristim cristim added triaged Item has been triaged priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens urgency/now Drop other things impact/many Affects most users effort/m Days type/bug Defect labels Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a4b19ba-ed72-482d-8118-246791e0d262

📥 Commits

Reviewing files that changed from the base of the PR and between 7465a46 and 77aec79.

📒 Files selected for processing (5)
  • pkg/common/types.go
  • providers/aws/recommendations/parser_sp.go
  • providers/aws/recommendations/parser_sp_test.go
  • providers/aws/services/savingsplans/client.go
  • providers/aws/services/savingsplans/client_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sp-purchase-wrong-family

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 58 minutes.

@cristim
cristim merged commit f63400a into main Jul 17, 2026
19 checks passed
@cristim
cristim deleted the fix/sp-purchase-wrong-family branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p0 Drop everything; same-day fix severity/critical Major harm when it happens triaged Item has been triaged type/bug Defect urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant