Skip to content

fix(purchases): narrow Describe*Offerings + cap pagination (#688) - #690

Merged
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/688-narrow-offering-lookup
May 22, 2026
Merged

fix(purchases): narrow Describe*Offerings + cap pagination (#688)#690
cristim merged 5 commits into
feat/multicloud-web-frontendfrom
fix/688-narrow-offering-lookup

Conversation

@cristim

@cristim cristim commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the Lambda timeout regression (execution 9336a111) where EC2 findOfferingID paginated indefinitely through sparse empty DescribeReservedInstancesOfferings pages and burned the entire 60s budget.

  • EC2: add OfferingType direct field on DescribeReservedInstancesOfferingsInput to let AWS filter by payment option server-side; add convertEC2PaymentOption helper; verify returned offering type before returning (defense in depth); extract paginateEC2Offerings + scanEC2OfferingPage helpers to keep cyclomatic complexity within the gocyclo threshold
  • RDS: add pagination cap + ctx.Err() check + variant verification + per-page timing log; extract paginateRDSOfferings + scanRDSOfferingPage helpers (already had all filter fields server-side)
  • ElastiCache: same as RDS; extract paginateElastiCacheOfferings + scanElastiCacheOfferingPage
  • MemoryDB: add OfferingType + Duration direct fields to narrow at API level; replace client-side matchesDuration/matchesOfferingType loop with server-side filters; add cap + ctx.Err + variant verification
  • OpenSearch: API has no filter fields; add cap + ctx.Err + defense-in-depth variant check after client-side matching; extract scanOpenSearchOfferingPage
  • Redshift: API has no node-type/payment filter fields; add cap + ctx.Err + unknown-type guard; extract scanRedshiftOfferingPage
  • SavingsPlans: add pagination loop + cap + ctx.Err to lookupOfferingID (was a single non-paginated call)

All seven findOfferingID/lookupOfferingID functions now cap at maxOfferingPages = 5 pages (500 offerings at MaxResults/MaxRecords = 100) and return a diagnostic error instead of timing out.

Test plan

  • 21 new regression tests (3 per service): pagination cap fires, wrong-variant rejected, happy path
  • All existing tests pass: 310 tests across 7 packages
  • gocyclo hook passes (all findOfferingID complexity reduced to ≤10 via helper extraction)
  • go vet, gofmt, go mod tidy pass

Closes #688
Refs #684 #667 #632

Summary by CodeRabbit

  • Bug Fixes

    • Added strict pagination caps and improved per-page monitoring/logging for offering lookups to avoid long-running searches and return clearer diagnostic errors.
    • Strengthened validation to reject offerings with mismatched payment options or unexpected offering types; improved context-cancellation handling.
    • Canonicalized and normalized payment-option handling to reduce incorrect matches.
  • Tests

    • Added unit tests covering pagination caps, wrong-variant rejection, and successful offering selection across affected services.

Review Change Stack

Root cause: EC2 findOfferingID was missing the offering-type filter,
causing AWS to return all payment variants, triggering indefinite
pagination through sparse empty pages and burning the entire Lambda
60s budget (execution 9336a111).

Changes per service (all 7 purchase-path clients):
- EC2: add OfferingType direct field + convertEC2PaymentOption helper;
  verify returned offering type before returning; cap at 5 pages;
  check ctx.Err() between pages; add timing log per page
- RDS: add pagination cap + ctx.Err check + variant verification +
  per-page timing log (already had all filter fields)
- ElastiCache: same as RDS
- MemoryDB: add OfferingType + Duration direct fields to narrow at
  API level; replace client-side matchesDuration/matchesOfferingType
  loop with server-side filters; cap + ctx.Err + variant verification
- OpenSearch: API has no filter fields; add cap + ctx.Err + defense-
  in-depth variant check after client-side matching
- Redshift: API has no node-type/payment filter fields; add cap +
  ctx.Err + unknown-type guard
- SavingsPlans: add pagination loop + cap + ctx.Err to lookupOfferingID
  (was a single non-paginated call)

MaxResults/MaxRecords: EC2/Redshift/RDS/ElastiCache APIs have a hard
max of 100 per page; no change from current value. MemoryDB/OpenSearch
were already at 100.

Regression tests (3 per service = 21 new tests):
- Pagination cap fires after maxOfferingPages empty pages + next token
- Wrong-variant offering rejected by defense-in-depth guard
- Happy path returns correct offering ID on first page

Refs #684 #667 #632
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/now Drop other things impact/all-users Affects every user effort/m Days type/bug Defect labels May 22, 2026
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cristim has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c82f7c5-b375-4f96-a597-31a85b6625b3

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8f269 and 5f07744.

📒 Files selected for processing (11)
  • iac/federation/azure-target/terraform/.terraform.lock.hcl
  • providers/aws/services/ec2/client.go
  • providers/aws/services/elasticache/client.go
  • providers/aws/services/elasticache/client_test.go
  • providers/aws/services/opensearch/client.go
  • providers/aws/services/opensearch/client_test.go
  • providers/aws/services/rds/client.go
  • providers/aws/services/redshift/client.go
  • providers/aws/services/redshift/client_test.go
  • terraform/modules/database/azure/.terraform.lock.hcl
  • terraform/modules/frontend/gcp/.terraform.lock.hcl
📝 Walkthrough

Walkthrough

This PR caps and paginates Describe*Offerings across EC2, RDS, ElastiCache, MemoryDB, OpenSearch, Redshift, and SavingsPlans; adds payment-option mapping/validation and per-page ctx cancellation checks; and updates tests to cover pagination caps, wrong-variant rejection, and happy paths.

Changes

Reserved Instance / Node Offering Lookup Pagination and Validation

Layer / File(s) Summary
EC2 offering lookup with pagination cap and type conversion
providers/aws/services/ec2/client.go, providers/aws/services/ec2/client_test.go
Adds convertEC2PaymentOption to map payment slugs to OfferingTypeValues, introduces typed query builders and a paginated DescribeReservedInstancesOfferings walker with maxOfferingPages, and centralizes offering selection in scanEC2OfferingPage. Tests cover pagination-cap enforcement, variant rejection, and happy-path success.
ElastiCache offering lookup with pagination cap and page scanning
providers/aws/services/elasticache/client.go, providers/aws/services/elasticache/client_test.go
Adds maxOfferingPages cap and delegates to paginateElastiCacheOfferings with scanElastiCacheOfferingPage for per-page validation. Tests cover pagination-cap behavior, variant mismatch rejection, and happy path.
MemoryDB offering lookup with API-level filtering and duration conversion
providers/aws/services/memorydb/client.go, providers/aws/services/memorydb/client_test.go
Refactors findOfferingID to set NodeType, OfferingType, and Duration in requests, adds convertMemoryDBPaymentOption and getDurationStringForAPI, enforces maxOfferingPages, and replaces helper-focused tests with pagination/validation tests.
OpenSearch offering lookup with normalized payment-option validation
providers/aws/services/opensearch/client.go, providers/aws/services/opensearch/client_test.go
Refactors findOfferingID to enforce maxOfferingPages, adds scanOpenSearchOfferingPage and normalizeOpenSearchPaymentOption, and performs defense-in-depth payment-option checks. Tests verify pagination-cap, variant rejection, and happy path.
RDS offering lookup with capped pagination and per-page validation
providers/aws/services/rds/client.go, providers/aws/services/rds/client_test.go
Introduces maxOfferingPages, refactors findOfferingID to use a capped pagination walker and scanRDSOfferingPage, and updates error messages. Tests verify pagination-cap enforcement, variant rejection, and happy-path success.
Redshift offering lookup with pagination cap and enum validation
providers/aws/services/redshift/client.go, providers/aws/services/redshift/client_test.go
Enforces maxOfferingPages on DescribeReservedNodeOfferings, adds scanRedshiftOfferingPage with explicit enum validation, and updates errors. Tests cover pagination-cap, unknown-type rejection, and happy path.
SavingsPlans offering lookup with pagination cap and search-result iteration
providers/aws/services/savingsplans/client.go, providers/aws/services/savingsplans/client_test.go
Rewrites lookupOfferingID to paginate DescribeSavingsPlansOfferings, scan SearchResults per page, check ctx.Err() between pages, and return diagnostic errors for cap exhaustion or no offerings. Tests added for cap and happy path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • LeanerCloud/CUDly#599: Related EC2 tenancy/scope canonicalization logic and offering-filter normalization.

Poem

🐰 I hopped through pages, counted five,
Found mismatched variants—kept code alive.
Now lookups stop before they tire,
Payments checked, no wild purchase fire.
Happy hops and logs concise—
Safe buys, small budgets, oh so nice!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the primary change: narrowing Describe*Offerings API calls and capping pagination across multiple AWS services to fix the timeout issue.
Linked Issues check ✅ Passed All coding requirements from issue #688 are implemented: narrow filters applied across 7 services, payment-option verification added, pagination capped at maxOfferingPages=5, ctx.Err() checks added, and 21 regression tests covering all scenarios.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #688: narrowing offering lookups and capping pagination across EC2, RDS, ElastiCache, MemoryDB, OpenSearch, Redshift, and SavingsPlans services with appropriate test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 81.08% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/688-narrow-offering-lookup

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

@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

…ribe (#688)

Followup on the same #688 issue. The initial commit kept InstanceType,
ProductDescription, InstanceTenancy, duration, and OfferingClass packed
into DescribeReservedInstancesOfferingsInput.Filters[], with only
OfferingType promoted to the typed top-level field.

Live-AWS verification showed that the Filter[]-heavy shape is what causes
AWS to return empty pages with NextToken on sparse offering sets (e.g.
t4g.nano regional no-upfront convertible), walking until the Lambda
budget expires. The all-typed-fields shape returns the exact matching
offering immediately. Typed fields land on AWS's primary indices; Filter[]
is a generic fallback that does not always hit the same indices.

Changes:
- Move InstanceType, ProductDescription, InstanceTenancy, MinDuration,
  MaxDuration, OfferingClass to typed top-level fields on the input struct.
- Keep only scope as a Filter[] entry (no typed equivalent on the input).
- Use the typed enum constants (types.InstanceType, types.RIProductDescription,
  types.Tenancy, types.OfferingClassTypeConvertible) instead of stringly-typed
  filter values.
- Extract ec2OfferingQuery struct + buildEC2OfferingQuery / describeInputFromQuery
  helpers so findOfferingID stays under the gocyclo cap.
- Delete the now-unused buildOfferingFilters helper and the always-returns-
  "convertible" getOfferingClass helper.
- Change scanEC2OfferingPage from "hard error on variant mismatch" to "soft
  skip + log and continue scanning". With the typed OfferingType field set
  this should never fire; if AWS somehow returns a mismatched variant we
  want to keep looking on later pages rather than fail the rec.
- Update TestFindOfferingID_WrongVariantRejected to assert the new soft-skip
  behaviour (mismatched variant on the only page -> "no offerings found").
- Drop TestBuildOfferingFilters_LegacyCanonicalization: it exercised the
  deleted helper. The underlying canonicalizeEC2Tenancy / canonicalizeEC2Scope
  helpers retain their own direct unit tests.
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

Followup commit 5f8f2699d switches EC2 from Filter[]-packing to typed first-class fields (InstanceType, ProductDescription, InstanceTenancy, MinDuration/MaxDuration, OfferingClass) on DescribeReservedInstancesOfferingsInput. Verified against live AWS — the typed shape returns the t4g.nano regional no-upfront convertible offering immediately, while the Filter[]-heavy shape walked empty pages until the Lambda budget expired (the original #688 failure mode). Only scope remains in Filters[] (no typed equivalent on the input struct).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

@cristim Great context — using typed first-class fields on DescribeReservedInstancesOfferingsInput is the right approach since AWS can index/filter those fields server-side far more efficiently than the generic Filters[] slice. The live verification confirming immediate results vs. empty-page walking makes this a meaningful correctness fix, not just a style improvement.

Triggering a review of the latest commit now.

✅ Actions performed

Review triggered.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@providers/aws/services/ec2/client.go`:
- Around line 412-416: The pagination loop only treats result.NextToken == nil
as end-of-pagination; change the termination condition to also treat an empty
string as end-of-pagination by checking result.NextToken == nil ||
*result.NextToken == "" before breaking, otherwise assign nextToken =
result.NextToken; this ensures result.NextToken being "" stops the loop and
avoids redundant calls (refer to result.NextToken and nextToken in the EC2
client pagination code).

In `@providers/aws/services/elasticache/client.go`:
- Around line 270-271: The code currently calls
c.convertPaymentOption(rec.PaymentOption) and assigns offeringType which
silently coerces unknown values to "Partial Upfront"; update the flow in the
function that uses offeringType (before any pagination or downstream processing)
to detect unsupported payment options by validating the result of
c.convertPaymentOption(rec.PaymentOption) and return an explicit error instead
of proceeding—e.g., have convertPaymentOption return (string, error) or call a
validator after conversion, and if unsupported return an error immediately (do
not rely on getDurationString(rec.Term) or continue pagination).
- Around line 304-308: The pagination loop in client.go currently only breaks
when result.Marker == nil, so if AWS returns an empty string pointer
(aws.String("")) the loop will continue; update the loop that reads
result.Marker (the marker variable and the surrounding pagination logic) to
treat both nil and empty string as end-of-pagination by checking the pointer
value (e.g., use aws.StringValue(result.Marker) == "" or explicit nil check +
*result.Marker == "") and break when either condition is met.

In `@providers/aws/services/opensearch/client.go`:
- Around line 407-418: The explicit payment-option mismatch check is unreachable
because matchesPaymentOption(...) pre-filters and continues before the guard;
move the stronger validation earlier: before the continue, compute wantPayment
via normalizeOpenSearchPaymentOption(rec.PaymentOption) and gotPayment from
string(offering.PaymentOption) and if they differ return the fmt.Errorf (using
aws.ToString(offering.ReservedInstanceOfferingId), gotPayment, wantPayment,
rec.ResourceType, rec.PaymentOption); only after this stricter equality check
call matchesPaymentOption or keep the existing continue logic so the mismatch
branch can actually trigger and surface unexpected enum/representation
differences.
- Around line 387-391: The loop currently only stops when result.NextToken ==
nil; change the termination check to treat both nil and an empty string as
terminal: after receiving result.NextToken, if result.NextToken == nil OR
(result.NextToken != nil AND *result.NextToken == "") then break; otherwise
assign nextToken = result.NextToken and continue. Apply this change around the
pagination loop where NextToken and result.NextToken are used to prevent
redundant calls and false cap errors.

In `@providers/aws/services/rds/client.go`:
- Around line 339-342: The pagination loop currently only breaks when
result.Marker is nil, but a non-nil empty marker (e.g., aws SDK returning
pointer to empty string) should also be treated as the terminal page; update the
loop that uses result.Marker and the local marker variable so it breaks when
result.Marker == nil || *result.Marker == "" (or otherwise check for empty
string value) before assigning marker = result.Marker, ensuring you reference
and update the code paths around result.Marker and marker in the pagination
logic.

In `@providers/aws/services/redshift/client.go`:
- Around line 423-427: The loop currently only breaks when result.Marker is nil,
which can still issue an extra request if result.Marker is non-nil but empty;
update the pagination check to treat an empty string as end-of-pagination by
changing the condition to break when result.Marker == nil ||
aws.StringValue(result.Marker) == "" (or dereference and compare to "" if not
using aws helpers), and only assign marker = result.Marker when the marker is
non-empty so subsequent requests stop correctly; reference the variables
result.Marker and marker in the pagination loop.
- Around line 443-453: The explicit enum guard is unreachable because
c.matchesOfferingType(...) filters unknown ReservedNodeOfferingType values
first; move the enum validation so it runs before calling c.matchesOfferingType.
Specifically, read offering.ReservedNodeOfferingType into offeringTypeStr and
validate it equals "Regular" or "Upgradable" (returning the same fmt.Errorf with
aws.ToString(offering.ReservedNodeOfferingId) and rec.ResourceType on mismatch)
before invoking c.matchesOfferingType(rec.PaymentOption) so the diagnostic path
can ever be hit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8ef2669-aeb2-4521-a9d7-0c8c687995f2

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa3058 and 237c30f.

📒 Files selected for processing (14)
  • providers/aws/services/ec2/client.go
  • providers/aws/services/ec2/client_test.go
  • providers/aws/services/elasticache/client.go
  • providers/aws/services/elasticache/client_test.go
  • providers/aws/services/memorydb/client.go
  • providers/aws/services/memorydb/client_test.go
  • providers/aws/services/opensearch/client.go
  • providers/aws/services/opensearch/client_test.go
  • providers/aws/services/rds/client.go
  • providers/aws/services/rds/client_test.go
  • providers/aws/services/redshift/client.go
  • providers/aws/services/redshift/client_test.go
  • providers/aws/services/savingsplans/client.go
  • providers/aws/services/savingsplans/client_test.go

Comment thread providers/aws/services/ec2/client.go Outdated
Comment thread providers/aws/services/elasticache/client.go Outdated
Comment thread providers/aws/services/elasticache/client.go Outdated
Comment thread providers/aws/services/opensearch/client.go Outdated
Comment thread providers/aws/services/opensearch/client.go Outdated
Comment thread providers/aws/services/rds/client.go Outdated
Comment thread providers/aws/services/redshift/client.go Outdated
Comment thread providers/aws/services/redshift/client.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
providers/aws/services/ec2/client.go (1)

435-438: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat empty NextToken as end-of-pagination.

This loop still only stops on nil. If EC2 returns NextToken: "", you'll keep reissuing the request until maxOfferingPages and surface a false cap error instead of the expected exhaustion path.

Suggested fix
-		if result.NextToken == nil {
+		if result.NextToken == nil || aws.ToString(result.NextToken) == "" {
 			break
 		}
 		nextToken = result.NextToken
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/aws/services/ec2/client.go` around lines 435 - 438, The pagination
loop in providers/aws/services/ec2/client.go only breaks when result.NextToken
is nil, which misses the case where AWS returns an empty string; update the loop
around result.NextToken so it treats both nil and empty string as
end-of-pagination: check if result.NextToken == nil OR the dereferenced
*result.NextToken == "" and break in that case, otherwise assign nextToken =
result.NextToken; ensure any references to maxOfferingPages remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@providers/aws/services/ec2/client.go`:
- Around line 450-458: The scanEC2OfferingPage function currently returns an
empty string if it encounters a matching offering whose
ReservedInstancesOfferingId is nil/empty, which causes the caller to treat the
entire page as “no match” and skip later valid offerings; update
scanEC2OfferingPage so that inside the loop, after you verify o.OfferingType ==
wantType, you check if aws.ToString(o.ReservedInstancesOfferingId) is empty and,
if so, log/skip that offering and continue the loop instead of returning an
empty string, only returning a non-empty ID when found or returning "" after
scanning all offerings.

---

Duplicate comments:
In `@providers/aws/services/ec2/client.go`:
- Around line 435-438: The pagination loop in
providers/aws/services/ec2/client.go only breaks when result.NextToken is nil,
which misses the case where AWS returns an empty string; update the loop around
result.NextToken so it treats both nil and empty string as end-of-pagination:
check if result.NextToken == nil OR the dereferenced *result.NextToken == "" and
break in that case, otherwise assign nextToken = result.NextToken; ensure any
references to maxOfferingPages remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7fd3e9e1-31d0-40e0-9322-2300916c5277

📥 Commits

Reviewing files that changed from the base of the PR and between 237c30f and 5f8f269.

📒 Files selected for processing (2)
  • providers/aws/services/ec2/client.go
  • providers/aws/services/ec2/client_test.go

Comment on lines +450 to +458
func scanEC2OfferingPage(offerings []types.ReservedInstancesOffering, wantType types.OfferingTypeValues) string {
for _, o := range offerings {
if o.OfferingType != wantType {
log.Printf("EC2 findOfferingID skipping mismatched variant %s (got %q want %q)",
aws.ToString(o.ReservedInstancesOfferingId), o.OfferingType, wantType)
continue
}
return aws.ToString(o.ReservedInstancesOfferingId)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip empty offering IDs instead of aborting the page scan.

Line 457 returns "" when a matching offering has a nil/empty ReservedInstancesOfferingId. The caller treats that as “no match on this page”, so any later valid offering on the same page is never inspected.

Suggested fix
 	for _, o := range offerings {
 		if o.OfferingType != wantType {
 			log.Printf("EC2 findOfferingID skipping mismatched variant %s (got %q want %q)",
 				aws.ToString(o.ReservedInstancesOfferingId), o.OfferingType, wantType)
 			continue
 		}
-		return aws.ToString(o.ReservedInstancesOfferingId)
+		id := aws.ToString(o.ReservedInstancesOfferingId)
+		if id == "" {
+			log.Printf("EC2 findOfferingID skipping matching variant with empty offering ID (want %q)", wantType)
+			continue
+		}
+		return id
 	}
 	return ""
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func scanEC2OfferingPage(offerings []types.ReservedInstancesOffering, wantType types.OfferingTypeValues) string {
for _, o := range offerings {
if o.OfferingType != wantType {
log.Printf("EC2 findOfferingID skipping mismatched variant %s (got %q want %q)",
aws.ToString(o.ReservedInstancesOfferingId), o.OfferingType, wantType)
continue
}
return aws.ToString(o.ReservedInstancesOfferingId)
}
func scanEC2OfferingPage(offerings []types.ReservedInstancesOffering, wantType types.OfferingTypeValues) string {
for _, o := range offerings {
if o.OfferingType != wantType {
log.Printf("EC2 findOfferingID skipping mismatched variant %s (got %q want %q)",
aws.ToString(o.ReservedInstancesOfferingId), o.OfferingType, wantType)
continue
}
id := aws.ToString(o.ReservedInstancesOfferingId)
if id == "" {
log.Printf("EC2 findOfferingID skipping matching variant with empty offering ID (want %q)", wantType)
continue
}
return id
}
return ""
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/aws/services/ec2/client.go` around lines 450 - 458, The
scanEC2OfferingPage function currently returns an empty string if it encounters
a matching offering whose ReservedInstancesOfferingId is nil/empty, which causes
the caller to treat the entire page as “no match” and skip later valid
offerings; update scanEC2OfferingPage so that inside the loop, after you verify
o.OfferingType == wantType, you check if
aws.ToString(o.ReservedInstancesOfferingId) is empty and, if so, log/skip that
offering and continue the loop instead of returning an empty string, only
returning a non-empty ID when found or returning "" after scanning all
offerings.

cristim added 2 commits May 22, 2026 23:50
Three related cleanups on the offering-lookup pipeline:

1. EC2/RDS: normalize end-of-pagination check to guard against both nil
   and empty-string NextToken/Marker. AWS may return either form for the
   terminal page; only checking nil risks an extra empty-page request.
   EC2 uses a new isLastEC2Page helper so the intent is explicit.

2. OpenSearch: remove the post-match payment-option defense-in-depth
   guard from scanOpenSearchOfferingPage. matchesPaymentOption already
   filters mismatched variants in the same loop iteration; the redundant
   hard error can never fire in practice and makes the wrong-variant test
   harder to reason about. Soft outcome (loop exhausts -> "no offerings
   found") is safer for the caller.

3. Redshift: same rationale as OpenSearch -- remove the hard-error guard
   for unknown offering type from scanRedshiftOfferingPage.

Update TestFindOfferingID_WrongVariantRejected for both services to
assert the soft-failure shape (err != nil, id == "").
Category A (4 sites, nil-or-empty pagination terminator):
- elasticache: guard result.Marker against both nil and "" in
  paginateElastiCacheOfferings; avoid the redundant page that a
  non-nil empty-string marker would otherwise trigger
- opensearch, rds, redshift: same nil-or-empty guard on their
  respective NextToken/Marker fields (EC2 was already fixed in
  5f8f269)

Category B (ElastiCache payment option coercion):
- convertPaymentOption now returns (string, error); unknown values
  return an explicit "unsupported ElastiCache payment option" error
  instead of silently mapping to "Partial Upfront"
- findOfferingID propagates the error before any pagination begins
- TestClient_ConvertPaymentOption updated to assert the (result,
  error) return shape and verify the unknown-input error path

Category C (unreachable variant-mismatch guard):
- OpenSearch: replace the matchesPaymentOption pre-filter continue
  with a direct gotPayment != wantPayment check that returns an
  explicit "payment option mismatch" error; guard is now reachable
- Redshift: replace matchesOfferingType pre-filter continue with a
  direct "Regular"/"Upgradable" enum check that returns an explicit
  "unexpected type" error on first mismatch
- Test comments and assertions updated to match the new behaviour
  (mismatch surfaces as a diagnostic error, not a silent skip)

CR #690 review round 1
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

1 similar comment
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

1 similar comment
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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.

The previous commit accidentally included Terraform provider constraint
changes unrelated to the #688 offering-lookup fix. Restore the three
lock files to their state on feat/multicloud-web-frontend so this
branch stays focused on the purchase-path narrowing changes only.
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 merged commit b581fe7 into feat/multicloud-web-frontend May 22, 2026
4 checks passed
@cristim
cristim deleted the fix/688-narrow-offering-lookup branch May 22, 2026 22:11
cristim added a commit that referenced this pull request May 22, 2026
) (#693)

GetReservationPurchaseRecommendation, GetSavingsPlansPurchaseRecommendation,
and DescribeDBMajorEngineVersions each silently discarded every page after
the first. For large payer accounts, CE returns recommendations across
multiple pages, so callers were seeing a fraction of the real result set.

All three call sites now drive a token-driven pagination loop. The existing
rate-limiter retry loop stays as the inner (per-page) loop. ctx.Err() is
checked at the top of each iteration (feedback_ctx_cancel_terminal). The
loop terminates on nil OR empty-string token (PR #690 parity). Exceeding
maxRecommendationPages/maxEngineVersionPages = 20 returns a diagnostic
error citing the service and issue #692. gocyclo relief: fetchSPAllPages +
fetchSPPageWithRetry extracted for SP; queryMajorEngineVersionsWithClient +
fetchMajorEngineVersionsForEngine extracted for RDS (all stay under cap 15).

Three new test groups: multi-page accumulation (3 pages), empty-token
terminal (no extra call), and pagination cap error. Updated
TestGetRecommendations_ContextCancellation to assert context.Canceled
directly -- the new ctx.Err() guard at the top of the pagination loop fires
before the rate-limiter so the error path changed correctly.
cristim added a commit that referenced this pull request May 25, 2026
closes #695) (#696)

* feat(ri-exchange): target-offerings endpoint + UUID validation + AWS 4xx mapping (#695)

Three defects addressed:

Defect 1 (backend): add GET /api/ri-exchange/target-offerings backed by
ListTargetOfferings on ec2svc.Client. Uses typed-field approach from PR
#690 to enumerate valid convertible exchange targets for a source RI.
Introduces targetOfferingsEC2Client interface + factory in handler.go for
testability.

Defect 2: validate targets[].offering_id against a UUID regex at both the
getExchangeQuote and validateExecuteExchangeBody call sites. Returns 400
with a descriptive message when an instance type (e.g. "t3.medium") is
submitted instead of an offering UUID.

Defect 3: mapAWSExchangeError helper extracts smithy.APIError and maps
InvalidOfferingId/InvalidParameter/ValidationError/Invalid*.NotFound to
400 with the original AWS message; all other AWS errors stay 500. Applied
at the quote and execute call sites.

Tests: 27 new test cases covering the new endpoint (happy path, 404, auth),
UUID format rejection, and AWS 4xx/5xx error classification.

Closes #695

* feat(ri-exchange/ui): replace free-text offering ID with picker (#695)

Replace the "Target Offering ID" free-text input in the exchange modal
with a <select> picker backed by two sources:

- AWS-driven: GET /api/ri-exchange/target-offerings loads valid target
  offerings for the source RI from DescribeReservedInstancesOfferings
  (the new endpoint from the companion backend commit). Options are
  grouped under "AWS Target Offerings".

- CE Recommendations: alternativeTargets from reshape recommendations are
  listed as a second optgroup "CE Recommendations" with per-instance
  monthly cost displayed in the option label.

The picker drives a hidden <input type="hidden" class="modal-exchange-target">
that holds the resolved UUID. collectTargets() validates the value against
a UUID regex before submission, rejecting any non-UUID value with a
user-visible error message. This closes the path where a user could
accidentally submit an instance type (e.g. "t3.medium") instead of a UUID.

Tests: 5 new test cases covering the picker select rendering, CE options
population, AWS offerings after async load, select-to-hidden-input sync,
and non-UUID rejection at submission time. Updated 8 existing tests to
use offering_id UUIDs in place of instance types where they inject values
directly into the hidden offering input.

Closes #695
cristim added a commit that referenced this pull request May 31, 2026
…loses #691) (#800)

* fix(azure/gcp/aws): ctx.Err guards + page cap on 22 pagination loops (closes #691)

Add ctx.Err() early-return guards and maxPages budget caps to all unbounded
pagination loops across Azure (14 loops), GCP (6 loops), and AWS (2 loops).

Context cancellation now terminates loops immediately with a wrapped error
instead of either continuing or silently returning a partial result set,
matching the pattern from PR #690 and the feedback_ctx_cancel_terminal rule.

Fixes in collect* helpers (cache, cosmosdb) and the inline search loop that
previously used ctx.Err()+break (silently partial) -- changed to return the
error so callers propagate it. Also fixes two nil-context test calls in
cache/client_test.go that panicked after the ctx.Err() guard was introduced.

Regression tests added: Azure cache GetValidResourceTypes ctx-cancel + page
cap, GCP computeengine GetRecommendations ctx-cancel + page cap, AWS
fetchCoveragePaged ctx-cancel.

* fix(gcp): rename page-cap constant + counter to reflect item semantics

MachineTypesIterator.Next() yields one machine type per call, not one
page. Rename maxMachineTypesPages -> maxMachineTypeItems and
pageIdx -> itemIdx so the names and error message match the actual
iteration semantics.

* refactor: extract helpers to clear gocyclo threshold (#800)

The ctx.Err pagination guards added in this PR pushed two functions
just over the project's gocyclo > 10 ceiling, failing pre-commit:

- providers/azure/services/search/client.go GetValidResourceTypes (12)
- providers/aws/recommendations/utilization.go GetRIUtilization (11)

Each is split into a thin entry point plus a focused helper:
- GetValidResourceTypes -> resolveServicesPager + collectSKUsFromPager
- GetRIUtilization      -> buildUtilizations for the agg-to-slice tail

Behaviour unchanged; 323 tests pass across the two packages.
cristim added a commit that referenced this pull request Jun 5, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
cristim added a commit that referenced this pull request Jun 8, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
cristim added a commit that referenced this pull request Jun 19, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
cristim added a commit that referenced this pull request Jul 10, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
cristim added a commit that referenced this pull request Jul 17, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
cristim added a commit that referenced this pull request Jul 17, 2026
All 7 service clients (EC2, RDS, ElastiCache, MemoryDB, OpenSearch,
Redshift, SavingsPlans) pass the 6-item contract from PR #690: typed
fields, ctx.Err() at loop top, page cap, per-page log, variant skip,
nil+empty token terminator.

Fix three pre-existing test build failures: missing execID argument
in RDS/Redshift/SavingsPlans findOfferingID test call sites.

Add TestFindOfferingID_CtxCancelledBeforePage and
TestFindOfferingID_EmptyStringTokenEndsPagination to every service's
client_test.go (14 new tests) to pin contract items C2 and C6.

Add providers/aws/services/AUDIT.md with the 6-item contract, per-service
status table, watchlist items (MemoryDB Valkey engine split, OpenSearch
and Redshift Graviton dimension), and quarterly audit procedure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm 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