Skip to content

fix(providers/aws): fail loud on unrecognized RI term strings - #1207

Merged
cristim merged 3 commits into
mainfrom
fix/arch-04-fail-loud-ri-term
Jul 16, 2026
Merged

cristim merged 3 commits into
mainfrom
fix/arch-04-fail-loud-ri-term

Conversation

@cristim

@cristim cristim commented Jun 10, 2026 •

Copy link
Copy Markdown
Member

Problem

Closes #1192 (review finding ARCH-04, P2).

The term converters in five AWS RI clients (ElastiCache, MemoryDB, RDS, Redshift, OpenSearch) silently mapped any unrecognized or empty term string to a 1-year duration. The result feeds findOfferingID, which selects the offering that PurchaseCommitment buys, so a malformed or NULL term slipping past the boundary validators (for example a 0 Term DB row executed by the scheduler purchase path, which does not re-validate) would silently purchase a 1-year reservation when a 3-year commitment was intended. The savingsplans client in the same provider was already fixed to fail loud; these five were left inconsistently permissive.

Fix

Mirrors the savingsplans pattern in each client:

  • elasticache and rds: getDurationString now returns (string, error) and errors on anything outside 1yr, 1, 3yr, 3; the caller propagates the error before any API call.
  • memorydb: getDurationStringForAPI now returns (string, error); keeps the previously accepted month-count forms (12, 36) and errors on everything else.
  • redshift and opensearch: new requiredMonthsForTerm converts the term to the offering duration in months once, up front in findOfferingID (failing loud before pagination); the resolved month count is threaded into matchesDuration, removing the silent 12-month default from the per-offering matcher.

No behavior change for valid terms; unknown terms now surface an explicit error instead of buying the wrong commitment length.

Test evidence

  • Regression tests in all five packages assert that "", "0", "2yr", and "invalid" terms return an error (the empty term replicates the 0/NULL Term DB row scenario from the finding), plus positive cases for all accepted forms.
  • Pre-fix failure confirmed: with the client.go changes stashed, the updated tests fail to build (assignment mismatch: 2 variables but client.getDurationString returns 1 value); the old code cannot return the error the tests require.
  • go build ./... (root module) and go build ./... (providers/aws module): success.
  • go vet ./services/...: clean. gofmt -l: clean.
  • go test ./... in providers/aws: 848 passed in 11 packages, 0 failures.

The shared-purchase-core extraction that would deduplicate these converters across clients is tracked separately as ARCH-03.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reserved-instance term validation across AWS services (ElastiCache, MemoryDB, OpenSearch, RDS, Redshift): unsupported, empty or zero terms now return explicit errors instead of silently defaulting to a 1-year match, preventing incorrect offering selections.
  • Tests

    • Updated and added unit tests to assert correct parsing and error behavior for valid and invalid reservation terms.

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/high Significant harm urgency/this-quarter Within the quarter impact/few Limited audience effort/m Days type/chore Maintenance / non-user-visible labels Jun 10, 2026
@cristim

cristim commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 33 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: 37607cc6-4b28-4a68-8e1f-fa45a54bdb68

📥 Commits

Reviewing files that changed from the base of the PR and between 7d85fc2 and 0c3cba5.

📒 Files selected for processing (10)
  • 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
📝 Walkthrough

Walkthrough

Five AWS RI service clients (ElastiCache, MemoryDB, RDS, OpenSearch, Redshift) stop silently defaulting to a 1-year duration. Term-to-duration helpers now return (value, error) and offering-selection paths propagate errors to abort on unsupported/empty reservation terms.

Changes

Strict term validation across AWS RI service clients

Layer / File(s) Summary
ElastiCache term validation and error propagation
providers/aws/services/elasticache/client.go, providers/aws/services/elasticache/client_test.go
getDurationString now validates terms strictly and returns (string, error). paginateElastiCacheOfferings handles conversion errors and exits early. Tests cover valid terms and regression cases for invalid/empty/unsupported inputs.
MemoryDB term validation and error propagation
providers/aws/services/memorydb/client.go, providers/aws/services/memorydb/client_test.go
getDurationStringForAPI validates terms with error return. findOfferingID handles errors and aborts offering lookup immediately. New test function validates term normalization and error behavior for unsupported cases.
RDS term validation and error propagation
providers/aws/services/rds/client.go, providers/aws/services/rds/client_test.go
getDurationString validates terms with error return. paginateRDSOfferings handles errors and exits pagination early. Test table expanded to include expectErr flag and assertions for error messages; new test ensures fast-fail before API calls.
OpenSearch term-to-months conversion and duration matching
providers/aws/services/opensearch/client.go, providers/aws/services/opensearch/client_test.go
New requiredMonthsForTerm helper converts term strings to months with validation. findOfferingID calls the helper and exits on error. scanOpenSearchOfferingPage passes precomputed months to matchesDuration, which now compares offering duration against numeric month target. Tests refactored to use numeric months and new test validates term-to-months mapping and error cases.
Redshift term-to-months conversion and duration matching
providers/aws/services/redshift/client.go, providers/aws/services/redshift/client_test.go
New requiredMonthsForTerm helper converts term strings to months with validation. findOfferingID calls the helper and exits on error. scanRedshiftOfferingPage passes precomputed months to matchesDuration, which now compares offering duration against numeric month target. Tests refactored to use numeric months and new test validates term-to-months mapping and error cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • LeanerCloud/CUDly#1075: Modifies Redshift RI offering selection (providers/aws/services/redshift/client.go)—changes are adjacent and potentially related to payment-option/duration matching.

Suggested labels

priority/p1, urgency/now, impact/many, type/bug

Poem

🐰 I hopped through term strings one by one,
No longer defaulting when checks were undone.
Errors now shout where silence once lay,
Offerings match what the plans truly say.
Hooray — no surprise one-year buys today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the primary change: making term converters fail with explicit errors on unrecognized RI term strings instead of silently defaulting to 1-year.
Linked Issues check ✅ Passed All requirements from #1192/ARCH-04 are met: five AWS RI clients now fail loud on unrecognized/empty terms via error-returning converters, errors propagate before API calls, and regression tests validate both success and failure paths.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing the #1192/ARCH-04 requirement across five AWS clients; no unrelated modifications or auxiliary refactoring is present.

✏️ 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/arch-04-fail-loud-ri-term

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

@coderabbitai

coderabbitai Bot commented Jun 10, 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.

@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.

🧹 Nitpick comments (2)
providers/aws/services/elasticache/client_test.go (1)

398-400: ⚡ Quick win

Guard the error-message assertion to avoid panic on assertion failure.

assert.Error(t, err) does not stop execution; if it fails, err.Error() can panic and hide the real test failure. Gate the contains check on the assert result (or use require.Error).

Proposed test hardening
-			if tt.expectErr {
-				assert.Error(t, err)
-				assert.Contains(t, err.Error(), "unsupported ElastiCache reservation term")
-				return
-			}
+			if tt.expectErr {
+				if assert.Error(t, err) {
+					assert.Contains(t, err.Error(), "unsupported ElastiCache reservation term")
+				}
+				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/elasticache/client_test.go` around lines 398 - 400,
The test currently calls assert.Error(t, err) and then unconditionally uses
err.Error(), which can panic if the first assertion fails; either change
assert.Error to require.Error to stop execution on failure, or capture the
boolean result (e.g., ok := assert.Error(t, err)) and only call
assert.Contains(t, err.Error(), "unsupported ElastiCache reservation term") when
ok is true; apply this change around the assertion lines that perform the error
check and contains assertion in the ElastiCache client tests.
providers/aws/services/rds/client_test.go (1)

560-595: ⚡ Quick win

Add a call-path regression test for invalid term short-circuiting.

TestClient_GetDurationString validates the helper, but the ARCH-04 risk is in the offering lookup path. Add a findOfferingID invalid-term test that asserts no DescribeReservedDBInstancesOfferings call occurs.

Proposed test addition
+func TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall(t *testing.T) {
+	mockRDS := &MockRDSClient{}
+	client := &Client{client: mockRDS, region: "us-east-1"}
+
+	rec := common.Recommendation{
+		Service:       common.ServiceRelationalDB,
+		ResourceType:  "db.r5.large",
+		PaymentOption: "all-upfront",
+		Term:          "0",
+		Details: &common.DatabaseDetails{
+			Engine:   "mysql",
+			AZConfig: "single-az",
+		},
+	}
+
+	_, err := client.findOfferingID(context.Background(), rec, "")
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "unsupported RDS reservation term")
+	mockRDS.AssertNotCalled(t, "DescribeReservedDBInstancesOfferings", mock.Anything, mock.Anything)
+}
🤖 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/rds/client_test.go` around lines 560 - 595, Add a
regression unit test for findOfferingID that verifies invalid/empty/unsupported
terms short-circuit and do not call the AWS DescribeReservedDBInstancesOfferings
API: create a new test (e.g., TestClient_FindOfferingID_InvalidTerm) that
constructs a Client wired to a mocked RDS service, set the mock to expect NO
calls to DescribeReservedDBInstancesOfferings (using your test mock's
AssertNotCalled/ExpectNotCalled), call client.findOfferingID with an invalid
term like "invalid" (and other cases like "" or "2yr"/"0"), assert it returns
the expected error (contains "unsupported RDS reservation term") and finally
assert the mock DescribeReservedDBInstancesOfferings was not invoked; reference
the symbols Client.findOfferingID, Client.getDurationString (already covered),
and DescribeReservedDBInstancesOfferings to locate code.
🤖 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.

Nitpick comments:
In `@providers/aws/services/elasticache/client_test.go`:
- Around line 398-400: The test currently calls assert.Error(t, err) and then
unconditionally uses err.Error(), which can panic if the first assertion fails;
either change assert.Error to require.Error to stop execution on failure, or
capture the boolean result (e.g., ok := assert.Error(t, err)) and only call
assert.Contains(t, err.Error(), "unsupported ElastiCache reservation term") when
ok is true; apply this change around the assertion lines that perform the error
check and contains assertion in the ElastiCache client tests.

In `@providers/aws/services/rds/client_test.go`:
- Around line 560-595: Add a regression unit test for findOfferingID that
verifies invalid/empty/unsupported terms short-circuit and do not call the AWS
DescribeReservedDBInstancesOfferings API: create a new test (e.g.,
TestClient_FindOfferingID_InvalidTerm) that constructs a Client wired to a
mocked RDS service, set the mock to expect NO calls to
DescribeReservedDBInstancesOfferings (using your test mock's
AssertNotCalled/ExpectNotCalled), call client.findOfferingID with an invalid
term like "invalid" (and other cases like "" or "2yr"/"0"), assert it returns
the expected error (contains "unsupported RDS reservation term") and finally
assert the mock DescribeReservedDBInstancesOfferings was not invoked; reference
the symbols Client.findOfferingID, Client.getDurationString (already covered),
and DescribeReservedDBInstancesOfferings to locate code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 561e7d32-7692-47e0-b790-321a655d0345

📥 Commits

Reviewing files that changed from the base of the PR and between 006aba9 and cd10bf9.

📒 Files selected for processing (10)
  • 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

cristim added a commit that referenced this pull request Jun 11, 2026
Address CodeRabbit review feedback on PR #1207:

- Guard the error-message Contains assertions behind the result of
  assert.Error in all five term-converter table tests, so a failed
  error expectation reports cleanly instead of panicking on a nil
  error.
- Add TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall to the RDS
  client tests: an unrecognized term ("0", as produced by a 0/NULL
  Term DB row on the scheduler purchase path) must abort the offering
  lookup before any DescribeReservedDBInstancesOfferings call, which
  exercises the fail-loud path end to end rather than only the helper.

Part of #1192.
cristim added a commit that referenced this pull request Jun 19, 2026
Address CodeRabbit review feedback on PR #1207:

- Guard the error-message Contains assertions behind the result of
  assert.Error in all five term-converter table tests, so a failed
  error expectation reports cleanly instead of panicking on a nil
  error.
- Add TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall to the RDS
  client tests: an unrecognized term ("0", as produced by a 0/NULL
  Term DB row on the scheduler purchase path) must abort the offering
  lookup before any DescribeReservedDBInstancesOfferings call, which
  exercises the fail-loud path end to end rather than only the helper.

Part of #1192.
@cristim
cristim force-pushed the fix/arch-04-fail-loud-ri-term branch from 7d85fc2 to 30ed652 Compare June 19, 2026 14:48
@cristim

cristim commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 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 added a commit that referenced this pull request Jun 26, 2026
Address CodeRabbit review feedback on PR #1207:

- Guard the error-message Contains assertions behind the result of
  assert.Error in all five term-converter table tests, so a failed
  error expectation reports cleanly instead of panicking on a nil
  error.
- Add TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall to the RDS
  client tests: an unrecognized term ("0", as produced by a 0/NULL
  Term DB row on the scheduler purchase path) must abort the offering
  lookup before any DescribeReservedDBInstancesOfferings call, which
  exercises the fail-loud path end to end rather than only the helper.

Part of #1192.
@cristim
cristim force-pushed the fix/arch-04-fail-loud-ri-term branch from 30ed652 to 7a428c9 Compare June 26, 2026 17:06
@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

Rebased onto current main (was 5 commits behind, picking up #1261 migration-074-collision repair and #1085 SP fix already-merged context). Diff is back to the 10 service files only.

Out-of-scope follow-ups filed:

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

@cristim Thanks for the rebase/update — I’ll review the current 10-file diff against the latest main context and treat #1319 and #1320 as follow-up items outside this PR’s scope.

✅ 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 added 3 commits July 10, 2026 15:26
The term converters in the elasticache, memorydb, rds, redshift, and
opensearch RI clients silently mapped any unrecognized or empty term
to a 1-year duration before selecting the offering PurchaseCommitment
buys. A malformed or NULL term reaching the purchase path (for example
a 0 Term row executed by the scheduler, which does not re-validate)
would silently buy a 1-year reservation when 3-year was intended.

Mirror the savingsplans client fix: converters now return an explicit
error for any term outside the supported set, and the error propagates
from the offering lookup before any API call. Redshift and OpenSearch
validate the term once up front via requiredMonthsForTerm and thread
the resolved month count into matchesDuration. Regression tests cover
empty, "0", "2yr", and garbage terms in all five packages.

Closes #1192
Address CodeRabbit review feedback on PR #1207:

- Guard the error-message Contains assertions behind the result of
  assert.Error in all five term-converter table tests, so a failed
  error expectation reports cleanly instead of panicking on a nil
  error.
- Add TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall to the RDS
  client tests: an unrecognized term ("0", as produced by a 0/NULL
  Term DB row on the scheduler purchase path) must abort the offering
  lookup before any DescribeReservedDBInstancesOfferings call, which
  exercises the fail-loud path end to end rather than only the helper.

Part of #1192.
For the four remaining ARCH-04 clients (ElastiCache, MemoryDB, OpenSearch,
Redshift), add TestFindOfferingID_InvalidTerm_ErrorsBeforeAPICall tests that
assert an invalid/zero term aborts before any describe-offerings API call.

Mirrors the existing RDS call-path test added in the second commit on this PR.
Each test uses AssertNotCalled to verify the AWS API is never reached when the
term string is "0" (the scheduler-path 0/NULL Term DB row scenario from
issue #1192/ARCH-04).
@cristim
cristim force-pushed the fix/arch-04-fail-loud-ri-term branch from 7a428c9 to 0c3cba5 Compare July 10, 2026 13:29
@cristim

cristim commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 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 merged commit 69ea554 into main Jul 16, 2026
12 of 16 checks passed
@cristim
cristim deleted the fix/arch-04-fail-loud-ri-term branch July 16, 2026 16:51
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Merged to main after pre-merge adversarial review (verdict MERGE): all five RI service clients (elasticache, rds, memorydb, redshift, opensearch) now fail loud on every unrecognized/empty term string - no silent default-to-1yr remains, errors propagate before any API call, regression tests replicate real bad inputs ('', '0', '2yr', garbage) and the call-path tests assert the Describe API is never reached. Union with main green (299 tests). The same bug class found in the EC2 client during review is tracked as LeanerCloud/cloud-commitments-go#33.

cristim added a commit that referenced this pull request Jul 22, 2026
…ing to 1yr

getDurationValue fell through to OneYearSeconds for any unrecognized or
empty term string, missed by PR #1207's ARCH-04 sweep of the other five
provider term converters. A malformed term (e.g. an empty string from a
0/NULL Term DB row on the scheduler purchase path) would silently buy a
real 1-year EC2 RI instead of erroring, the same money-risk pattern
#1207 fixed everywhere else. Mirror the whitelist-switch idiom used by
the RDS/ElastiCache/MemoryDB/Redshift/OpenSearch converters: return an
explicit error on anything outside 1yr/1/3yr/3 and propagate it through
buildEC2QueryFromRec so findOfferingID aborts before any AWS API call.
cristim added a commit that referenced this pull request Jul 22, 2026
…ing to 1yr (follow-up to #1207) (#1481)

* fix(aws/ec2): reject unrecognized RI term instead of silently defaulting to 1yr

getDurationValue fell through to OneYearSeconds for any unrecognized or
empty term string, missed by PR #1207's ARCH-04 sweep of the other five
provider term converters. A malformed term (e.g. an empty string from a
0/NULL Term DB row on the scheduler purchase path) would silently buy a
real 1-year EC2 RI instead of erroring, the same money-risk pattern
#1207 fixed everywhere else. Mirror the whitelist-switch idiom used by
the RDS/ElastiCache/MemoryDB/Redshift/OpenSearch converters: return an
explicit error on anything outside 1yr/1/3yr/3 and propagate it through
buildEC2QueryFromRec so findOfferingID aborts before any AWS API call.

* fix(scheduler): drop recommendations with unparseable term instead of defaulting to 3yr

convertRecommendations coerced an empty or unparseable rec.Term to a
Term=3 RecommendationRecord, logging only a warning (and nothing at all
for an empty string). rec.Term round-trips through this int on the
purchase path (internal/purchase/execution.go formats it back as
"%dyr"), so the fabricated default let a malformed term launder into a
real, purchasable 3-year commitment, bypassing every provider client's
ARCH-04 term whitelist entirely since "3yr" is itself a valid value
those whitelists accept. Drop the recommendation instead, with an
ERROR-level log identifying the offending rec, rather than persisting a
fabricated term.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p2 Backlog-worthy severity/high Significant harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ARCH-04: Silent 1-year fallback for unrecognized term strings in 5 AWS RI clients

1 participant