Skip to content

sec(azure): harden HTTP clients + fix fabricated reservation prices in managedredis/savingsplans and 5 pricing clients - #1045

Merged
cristim merged 9 commits into
feat/multicloud-web-frontendfrom
fix/azure-pricing-httpclient
Jun 7, 2026
Merged

cristim merged 9 commits into
feat/multicloud-web-frontendfrom
fix/azure-pricing-httpclient

Conversation

@cristim

@cristim cristim commented Jun 7, 2026 •

Copy link
Copy Markdown
Member

Summary

  • H1 (sec): managedredis and savingsplans both built bare &http.Client{Timeout:30s} instead of httpclient.New(), bypassing the blockIMDSDialer that prevents SSRF credential exfiltration via the Azure IMDS endpoint. Both NewClient and NewClientWithHTTP (nil fallback) are now hardened.

  • H2 (sec): managedredis.getRedisPricing hand-rolled a for nextURL != "" pagination loop without the seen-URL guard, max-pages cap, or per-page timeout that pricing.FetchAll enforces. Replaced with pricing.FetchAll[redisPriceItem] (named type required to satisfy the generic constraint).

  • H3 (fix): savingsplans.fetchOnDemandRate interpolated the $filter string directly into the URL with only partial QueryEscape, relied on a single-page read, and returned (0, nil) on not-found -- conflating "rate is zero" with "lookup found nothing". Rewritten to use url.Values{}.Encode(), pricing.FetchAll, and error-on-not-found.

  • H4 (fix): compute, cache, cosmosdb, database, search each fabricated a reservation price from a hardcoded discount multiplier (0.62 / 0.45 / 0.35 / 0.65 / 0.65) and surfaced it as a real TotalCost/SavingsPercentage when the Retail Prices API returned no reservation line. All five now return an error matching the pattern managedredis already uses.

Test plan

  • go build ./... in providers/azure passes (verified)
  • go test ./... in providers/azure passes: 685 tests, 0 failures (up from 667 pre-fix; 18 new regression tests added)
  • Regression tests added: IMDS-block assertion for both managedredis/savingsplans constructors; TestFetchOnDemandRate_NotFound / TestFetchOnDemandRate_URLEncoding; NoReservationPricing test for all 5 H4 services; TestGetOfferingDetails_Paginated already existed in managedredis
  • The 5 *_3YearTerm tests that were previously passing via the fabricated fallback now pass with real 3-year pricing data added to their mock responses

Scope expanded

Two additional commits fold in the remaining Medium/Low/Nit findings from the providers/azure code review (FOLD-1045 from docs/code-review/16-remaining-findings-plan.md):

  • M4: lifted ParseTermYears into services/internal/reservations as the canonical erroring parser. Five clients (compute, cache, cosmosdb, database, search) previously used a literal rec.Term == "3yr" || rec.Term == "3" check that silently coerced unknown terms to 1yr. The private parsers in managedredis and synapse are removed in favour of the shared one.

  • M6: added rec.Count <= 0 guard at the top of every PurchaseCommitment implementation. An Advisor recommendation with a missing qty field defaults to 0; without this guard a zero-quantity purchase reaches Azure and produces a confusing 400.

  • M3: checkAndRegisterCapacityProvider now checks resp.StatusCode before decoding the body. A 403/429/500 previously decoded to an empty RegistrationState, triggering a spurious register POST; the register POST response status is also now checked.

  • M1/M2: added comment to ExtractedFields.Scope documenting that the field is populated but not yet threaded into the purchase body. The recommendation filter already constrains scope to Shared so the hardcoded appliedScopeType: "Shared" is always correct; the comment preserves the field for future wiring.

  • M5: documented the sync.Once sticky-failure contract on IsConfigured so operators know a transient auth failure at startup requires a process restart.

  • N2: lowercased leading word of error strings in provider.go per Go revive error-strings convention.

  • N3: inlined the contains() one-line alias; call strings.Contains directly at the 4 call sites.

  • L2: added comment at convertToExchangeableReservation documenting that callers MUST filter empty ReservationOrderID entries before initiating an exchange.

  • L3: added TestMergeServiceResults_OrderIsStable to pin the canonical compute->database->cache->cosmosdb->savingsplans->advisor merge order.

  • L4: included accumulated recommendation count in the Advisor pagination error Warnf so operators can assess completeness of partial results.

Closes #1021
Also closes #1055
Addresses the Azure portion of #1020 (the GCP half is handled by a separate agent/PR).

Deep money-path verification pass (Azure provider)

A full money-path correctness verification of the Azure reservation recommendation -> purchase path was run across all services (compute, cache, cosmosdb, database, search, managedredis, savingsplans, reservations), applying the GCP money-path bug classes (invalid SDK enum literals; per-hour-vs-term savings unit mismatch; term not propagated; PaymentOption not honoured; fabricated prices; non-idempotent purchase; under-counted commitments; hardcoded magic ratios; silent money-affecting defaults) as the probe list.

Result: two real defects found and fixed; all other classes verified CORRECT.

  • Savings/pricing unit consistency: CORRECT. Unlike the GCP SKU (per-hour rate), the Azure Retail Prices API returns a reservation line's retailPrice as the full-term price keyed by reservationTerm == "N Years"; the on-demand unitPrice is per-hour and is scaled to the same window via hoursInTerm = 8760 * termYears. Both sides of (onDemandPrice*hoursInTerm - reservationPrice) are full-term. The ~99.99%-bogus-savings GCP class is not present in Azure.
  • Term / SDK enums / fabricated prices / pagination / concurrency / magic values: CORRECT. Term parsing errors on unknown input (reservations.ParseTermYears); savingsplans uses real SDK enum constants (no MEMORY_MB-class literal; ARM REST literals like VirtualMachines/P%dY/Shared are valid); the H4 hardcoded-discount fallback is fully removed; GetExistingCommitments and the reservation-order lookup paginate fully; errgroup uses per-leaf semaphore + post-Wait ctx.Err(); 8760 * termYears is a correct calendar constant (no fabricated memory/vCPU ratio); go test -race is clean.
  • Idempotency: DEFECT 1 (fixed). savingsplans.PurchaseCommitment discarded PurchaseOptions and named the order alias with time.Now().UnixNano(). Since the alias is created with an HTTP PUT (the name is the idempotency handle), a re-drive double-bought a savings plan. Fixed by deriving the alias name from opts.IdempotencyToken via common.ReservationOrderID, mirroring the reservation clients. Regression tests fail pre-fix, pass post-fix.
  • Silent payment-option fallback: DEFECT 2 (fixed). The GetOfferingDetails payment-option switch in compute/cache/cosmosdb/database/search/synapse silently treated an unrecognised PaymentOption as all-upfront (default: upfrontCost = totalCost), surfacing a plausible-but-wrong cost split. Per the no-silent-fallbacks policy for money-affecting fields, the default branch now returns an explicit error. Unreachable on the live path today (converters pin "upfront"), so a defence-in-depth fix with a compute regression test that fails pre-fix.

Verification: providers/azure go build ./... OK, go vet ./... clean, go test -race ./... 688 passed; repo-root go build ./... OK.

Also closes #1079

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling in reservation purchasing and pricing validation across Azure services by rejecting invalid configurations instead of silently applying defaults.
    • Fixed pricing behavior to return errors when reservation pricing is unavailable rather than fabricating values.
    • Enhanced HTTP client security with SSRF-hardened clients in applicable services.
    • Standardized error messages for configuration validation failures.
  • Tests

    • Added comprehensive regression tests for error scenarios including missing pricing data and unsupported payment options.
    • Extended test coverage for quantity validation and configuration failures.
  • Refactor

    • Consolidated reservation term parsing logic across services for consistency.
    • Streamlined pagination and pricing retrieval patterns.

cristim added 2 commits June 7, 2026 03:06
…agination

Both services/managedredis and services/savingsplans built bare
&http.Client{Timeout: 30s} instead of httpclient.New(), bypassing the
blockIMDSDialer that every other Azure service client uses to prevent SSRF
attacks that could exfiltrate managed-identity credentials (issue #1021 H1).

managedredis.getRedisPricing also hand-rolled a NextPageLink loop without the
seen-URL guard, max-pages cap, or per-page timeout that pricing.FetchAll
enforces -- a self-referential NextPageLink would loop forever (H2).

savingsplans.fetchOnDemandRate interpolated the $filter value directly into the
URL with only a partial QueryEscape, bypassing proper percent-encoding for
spaces and quotes; it also read only the first page and returned (0, nil) on
not-found, conflating "rate is zero" with "lookup found nothing" (H3).

Changes:
- Replace &http.Client{} with httpclient.New() in NewClient and
  NewClientWithHTTP for both managedredis and savingsplans (nil fallback also
  hardened).
- Introduce redisPriceItem named type in managedredis to satisfy the
  pricing.FetchAll[T] type constraint; replace the hand-rolled loop with
  pricing.FetchAll (seen-URL guard + max-pages cap + per-page timeout).
- Rewrite savingsplans.fetchOnDemandRate: build $filter with url.Values.Encode,
  route through pricing.FetchAll, return an error on not-found rather than
  (0, nil).
- Add regression tests: IMDS-block assertion for both constructors; not-found
  error for savingsplans; URL-encoding guard; paginated happy-path for
  managedredis.

Closes #1021
… clients

When the Retail Prices API returned no reservation line, compute, cache,
cosmosdb, database, and search each invented a reservation price from a
hardcoded discount multiplier (0.62, 0.45, 0.35/0.65, 0.65, 0.65) and
surfaced the result as a real OfferingDetails.TotalCost/SavingsPercentage.
This can justify a reservation purchase that is not actually economical
(issue #1020 H4, feedback_nullable_not_zero).

managedredis already returns an error in this case and is the model here.

Changes:
- Replace the fabrication branches with fmt.Errorf("no reservation pricing
  found for ...") in compute.getVMPricing, cache.getRedisPricing,
  cosmosdb.getCosmosPricing, database.getSQLPricing, search.getSearchPricing.
- Remove the now-dead estimateCosmosReservationPrice and
  estimateSearchReservationPrice helpers.
- Update mock pricing responses to include "3 Years" reservation entries so
  the existing 3YearTerm tests exercise real pricing rather than the former
  fabricated fallback.
- Add regression tests (NoReservationPricing) for all five services that
  fail pre-fix and pass post-fix.

Addresses the Azure portion of #1020 (GCP half is handled separately).
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/s Hours type/security Security finding labels Jun 7, 2026
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 27 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58d91dfe-0239-477e-8f4b-47ab99e660fc

📥 Commits

Reviewing files that changed from the base of the PR and between da76c08 and c873b35.

📒 Files selected for processing (14)
  • providers/azure/mocks/azure_mocks.go
  • providers/azure/recommendations_test.go
  • providers/azure/services/cache/client.go
  • providers/azure/services/cache/client_test.go
  • providers/azure/services/compute/client.go
  • providers/azure/services/compute/client_test.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/cosmosdb/client_test.go
  • providers/azure/services/database/client.go
  • providers/azure/services/database/client_test.go
  • providers/azure/services/managedredis/client.go
  • providers/azure/services/managedredis/client_test.go
  • providers/azure/services/search/client.go
  • providers/azure/services/search/client_test.go
📝 Walkthrough

Walkthrough

This PR implements comprehensive validation hardening and HTTP security improvements across all Azure service clients, introducing a shared ParseTermYears utility, standardizing error messages, adding strict input validation (Count > 0), consolidating term parsing, removing price-fabrication fallbacks, and hardening HTTP clients in managed Redis and savings plans services with deterministic idempotent purchase naming.

Changes

Azure Validation and HTTP Hardening

Layer / File(s) Summary
ParseTermYears Shared Utility
providers/azure/services/internal/reservations/purchase.go, providers/azure/services/internal/reservations/purchase_test.go
New canonical term-year parser that normalizes input and returns errors for unsupported formats; table-driven test covers valid/invalid term inputs.
Provider Error Message Standardization
providers/azure/provider.go, providers/azure/provider_test.go, providers/azure/recommendations.go, providers/azure/recommendations_test.go
Consistent lowercase error message "azure provider is not configured" across all configuration-guard points; Advisor pagination logging enhanced to include recommendation count.
Compute Client Hardening and Registration Refactoring
providers/azure/services/compute/client.go, providers/azure/services/compute/client_test.go, providers/azure/services/compute/exchange.go
Capacity provider registration refactored with explicit API versioning and detailed error handling; PurchaseCommitment validates Count > 0; GetOfferingDetails parses terms via ParseTermYears and errors on unsupported payment options; getVMPricing returns error instead of fabricating prices; comprehensive test coverage for registration errors, unsupported payment options, missing reservation pricing, and zero-count rejection.
Cache Client Hardening
providers/azure/services/cache/client.go, providers/azure/services/cache/client_test.go
Redis client adds Count > 0 validation, term parsing via ParseTermYears, unsupported payment option errors, and returns error when reservation pricing is missing; test fixture extended with 3-year reservation; new regression test validates error on missing reservation pricing.
CosmosDB, Database, and Search Client Hardening
providers/azure/services/cosmosdb/client.go, providers/azure/services/cosmosdb/client_test.go, providers/azure/services/database/client.go, providers/azure/services/database/client_test.go, providers/azure/services/search/client.go, providers/azure/services/search/client_test.go
All three clients updated with Count > 0 validation, ParseTermYears integration, unsupported payment option errors, and error return on missing reservation pricing (removing estimate helpers); test fixtures extended with 3-year reservation terms; new regression tests validate error behavior; error-path test inputs include Count: 1.
Synapse Client Hardening
providers/azure/services/synapse/client.go, providers/azure/services/synapse/client_test.go
Removes local parseReservationTermYears helper and consolidates on shared reservations.ParseTermYears for both PurchaseCommitment and GetOfferingDetails; payment option handling returns explicit error for unsupported options.
Managed Redis Client HTTP Hardening and Pricing Modernization
providers/azure/services/managedredis/client.go, providers/azure/services/managedredis/client_test.go
HTTP client hardened to use httpclient.New() instead of bare http.Client; term parsing consolidated to reservations.ParseTermYears; Count > 0 validation added; getRedisPricing refactored to use shared pricing.FetchAll helper with new redisPriceItem struct instead of manual pagination; test coverage extended with IMDS-blocking validation, missing-pricing error handling, and zero-count rejection.
Savings Plans Client HTTP Hardening, Pagination Modernization, and Idempotent Naming
providers/azure/services/savingsplans/client.go, providers/azure/services/savingsplans/client_test.go
HTTP client hardened to httpclient.New(); PurchaseCommitment now derives SavingsPlanOrderAlias name deterministically from IdempotencyToken via common.ReservationOrderID (enabling idempotent re-drives) with timestamp fallback; fetchOnDemandRate rewritten to use pricing.FetchAll helper with url.Values query encoding and error return on not-found; comprehensive test updates validate deterministic alias reuse, IMDS blocking, and fetchOnDemandRate behavior.
Documentation, Test Fixtures, and Formatting
providers/azure/internal/recommendations/converter.go, providers/azure/mocks/azure_mocks.go, providers/azure/recommendations_test.go, pkg/exchange/reshape_crossfamily_test.go
ExtractedFields Scope documentation clarified; Azure mock VM pricing extended with 3-year reservation term; recommendations test suite updated with strings.Contains integration and TestMergeServiceResults_OrderIsStable regression test; exchange test formatting standardized.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • LeanerCloud/CUDly#1079: Savings Plans purchase idempotency fix using deterministic alias naming from IdempotencyToken, directly addressed in this PR's savingsplans client refactoring.
  • LeanerCloud/CUDly#1021: SSRF hardening and pagination guard improvements for managedredis and savingsplans clients, directly implemented here via httpclient.New() and pricing.FetchAll integration.
  • LeanerCloud/CUDly#1055: Remaining M/L/N correctness findings consolidated into this PR including term parsing consolidation, Count validation, error-string standardization, and provider registration status handling.

Suggested labels

type/bug

Poem

🐰 Parsing terms in harmony, no more guessing games,
Count must be one or greater, no more secret shames,
SSRF shields the credentials, pagination guards the pace,
Idempotent saves plans find their destined place.
From compute down to synapse, each service stands more tall,
Hardened reservations now—we've fixed them all! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.08% 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 describes the main security and correctness fixes: hardening HTTP clients and removing fabricated reservation prices across Azure pricing clients.
Linked Issues check ✅ Passed All three linked issues (#1021, #1055, #1079) are addressed: HTTP clients hardened with SSRF defense, pagination fixes with FetchAll, fabricated-price fallbacks removed, term parsing consolidated via ParseTermYears, rec.Count validation added, status checking improved, error messages lowercased, and savings plan purchase made idempotent.
Out of Scope Changes check ✅ Passed All changes are within scope: security hardening (IMDS blocking), pagination safety (seen-URL guard, max-pages cap), pricing correctness (removing fabricated fallbacks), validation improvements (Count > 0, term parsing, status checking), and documentation/hygiene supporting these fixes. No unrelated changes detected.

✏️ 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/azure-pricing-httpclient

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

@coderabbitai

coderabbitai Bot commented Jun 7, 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 2 commits June 7, 2026 04:31
 #1055)

M4: lift ParseTermYears into services/internal/reservations so all service
clients share one erroring parser. Five clients (compute, cache, cosmosdb,
database, search) previously used a literal "3yr"||"3" check that silently
coerced unknown terms to 1yr instead of returning an error. managedredis
and synapse used private erroring parsers that are now removed in favour of
the shared one.

M6: add rec.Count <= 0 guard at the top of every PurchaseCommitment
implementation. An Advisor recommendation with a missing qty field defaults
to 0; without this guard a zero-quantity purchase reaches the Azure API and
produces a confusing 400. Synapse already had the guard; it is now added to
compute, cache, cosmosdb, database, search, and managedredis.

M3: fix checkAndRegisterCapacityProvider to check resp.StatusCode before
decoding the body. A 403/429/500 previously decoded to an empty
RegistrationState, triggering a spurious register POST. The register POST
response status is now also checked.

Tests: added ParseTermYears table test, ZeroCountRejected regression tests
for managedredis and compute, and a NonTwoxx regression test for the
capacity-provider status check.
…ns alias

M1/M2: add comment to ExtractedFields.Scope documenting that the field is
populated but not yet threaded into the purchase body. The recommendation
filter already constrains scope to Shared so the hardcoded value is always
correct; the comment preserves the field for future wiring.

M5: document the sync.Once sticky-failure contract on IsConfigured so
operators know a transient auth failure at startup requires a process restart
to recover.

N2: lowercase the leading word of error strings in provider.go per Go
revive/golint error-strings convention ("Azure is not configured" ->
"azure provider is not configured", "Azure credentials validation failed"
-> "azure credentials validation failed"). Update assertion strings in tests.

N3: inline the contains() one-line alias in recommendations.go; call
strings.Contains directly at the 4 call sites. The test for contains() is
updated to call strings.Contains directly.

L2: add a comment at convertToExchangeableReservation documenting that
reservations with an empty ReservationOrderID (from malformed ARM IDs) are
returned but callers MUST filter them before initiating an exchange.

L3: add TestMergeServiceResults_OrderIsStable to pin the canonical
compute->database->cache->cosmosdb->savingsplans->advisor merge order.

L4: include accumulated recommendation count in the Advisor pagination error
Warnf so operators can assess the completeness of partial results.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

FOLD-1045 additions (Medium/Low/Nit from report 09)

Two new commits (73f540a, 4c9c858) add the remaining findings from the FOLD-1045 bucket in docs/code-review/16-remaining-findings-plan.md. Closes #1055.

Functional changes

Finding File(s) Change
M4 services/internal/reservations/purchase.go Add ParseTermYears as the canonical erroring parser; all 7 service clients use it (5 previously used a literal 3yr/3 check, 2 had private duplicates now removed)
M6 all 7 PurchaseCommitment impls Add rec.Count <= 0 guard before any HTTP call; zero-count Advisor recs now fail fast
M3 services/compute/client.go checkAndRegisterCapacityProvider checks resp.StatusCode before decoding; register POST status also checked
M1/M2 internal/recommendations/converter.go Comment on ExtractedFields.Scope documenting it is populated but not yet threaded (filter already constrains to Shared)
M5 provider.go Document sync.Once sticky-failure contract on IsConfigured
N2 provider.go, provider_test.go Lowercase error string prefixes per revive error-strings
N3 recommendations.go Inline contains() alias; call strings.Contains directly
L2 services/compute/exchange.go Comment at convertToExchangeableReservation that callers must filter empty ReservationOrderID
L4 recommendations.go Include accumulated rec count in Advisor pagination error Warnf

Deferred / documented decisions

Verification

  • go build ./... from repo root: pass
  • go vet ./...: no issues
  • go test ./... (providers/azure): 685 passed, 0 failed (up from 680 before this pass; 5 new tests)

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim Thanks for the detailed FOLD-1045 summary — the coverage across M3/M4/M6, the lowercase error-string pass, contains() inlining, and the deferred/documented decisions all look well-organised. Triggering an incremental review of commits 73f540a and 4c9c858 now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-04T14:28:19.903Z
Learning: In providers/azure/recommendations.go (Go, Azure provider), the service constructors (compute.NewClient, cache.NewClient, etc.) build concrete Azure SDK types with no interface injection seam. Introducing a concurrency timing test that asserts elapsed ≈ max-single-service latency would require adding package-level function variables — a refactor that is out of scope for small performance PRs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), Azure service clients alias payment options in pairs: `{all-upfront, upfront}` and `{monthly, no-upfront}`. All 7 Azure service-client switches (compute, cache, cosmosdb, database, search, synapse, managedredis client.go) use these aliases. The canonical Azure payment option set in `ValidPaymentOptionsByProvider` is `{upfront, monthly}` (with synonyms `all-upfront → upfront`, `no-upfront → monthly`, `partial-upfront → upfront`).
✅ 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.

Extract fetchCapacityProviderState and triggerCapacityProviderRegistration
from checkAndRegisterCapacityProvider to bring complexity from 13 to under
10 (gocyclo limit). Also apply gofmt to recommendations.go (trailing
newline) and pkg/exchange/reshape_crossfamily_test.go (field alignment).
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

…lias name

PurchaseCommitment discarded PurchaseOptions and named the SavingsPlanOrderAlias
with time.Now().UnixNano(). Because the alias is created with an HTTP PUT on its
name (armbillingbenefits SavingsPlanOrderAliasClient.BeginCreate uses PUT on
.../savingsPlanOrderAliases/{name}), the name is the idempotency handle: a
non-deterministic name means a re-drive of a stranded execution (issue #636)
creates a second savings plan and double-charges the customer.

Derive the alias name from opts.IdempotencyToken via common.ReservationOrderID
so a re-drive PUTs the same alias (idempotent), falling back to the prior
timestamp-based name on the CLI path where no token is supplied. This mirrors
the reservation clients, which already route opts.IdempotencyToken through
reservations.DoIdempotentPurchaseTwoStep.

Regression tests assert two re-drives with the same token reuse the GUID alias
name (fail pre-fix) and the empty-token CLI path keeps the timestamp name.
The GetOfferingDetails payment-option switch in compute, cache, cosmosdb,
database, search and synapse silently treated any unrecognised PaymentOption as
all-upfront (default: upfrontCost = totalCost), so a bad value would produce a
plausible-but-wrong cost split surfaced as a real quote. Per the no-silent-
fallbacks policy for money-affecting fields, the default branch now returns an
explicit error instead of degrading to upfront.

The branch is unreachable today (every converter pins PaymentOption to
"upfront"), so this is a defence-in-depth fix with no behaviour change on the
live path; it prevents a future monthly/other-payment recommendation from being
silently mispriced.

Regression test (compute, the canonical client): an unrecognised payment option
with fully-present pricing now returns an error and a nil OfferingDetails;
verified to fail pre-fix (returned a valid details with no error).
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Money-path verification pass + 2 fixes

Ran a deep money-path correctness verification of the Azure provider (reservation recommendation -> purchase) across all services, probing the GCP money-path bug classes. Two real defects found and fixed in this PR; everything else verified correct (details in the PR description's verification section).

Commit 9d46d6ebe -- fix(azure/savingsplans): make purchase idempotent via deterministic alias name

Commit da76c0865 -- fix(azure): fail loud on unrecognised payment option in offering details

  • The GetOfferingDetails payment-option switch in compute/cache/cosmosdb/database/search/synapse silently treated any unrecognised PaymentOption as all-upfront. Per the no-silent-fallbacks policy for money-affecting fields, the default branch now returns an explicit error. Compute regression test fails pre-fix.

Verification: providers/azure go build/go vet clean, go test -race ./... 688 passed; repo-root go build OK.

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
providers/azure/services/cosmosdb/client.go (1)

534-559: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope the Cosmos retail-price lookup to the requested SKU.

getCosmosPricing() still ignores sku, and extractCosmosPricing() picks the last reservation/on-demand row for the term across the whole region. In regions with multiple Cosmos reservation SKUs, GetOfferingDetails can return a different SKU's quote instead of surfacing "no reservation pricing found".

Suggested direction
 func (c *CosmosDBClient) getCosmosPricing(ctx context.Context, sku, region string, termYears int) (*CosmosPricing, error) {
-	filter := fmt.Sprintf("serviceName eq 'Azure Cosmos DB' and armRegionName eq '%s'", region)
+	filter := fmt.Sprintf(
+		"serviceName eq 'Azure Cosmos DB' and armRegionName eq '%s' and (armSkuName eq '%s' or skuName eq '%s')",
+		region, sku, sku,
+	)
@@
-	onDemandPrice, reservationPrice, currency := extractCosmosPricing(priceData.Items, termYears)
+	onDemandPrice, reservationPrice, currency := extractCosmosPricing(priceData.Items, sku, termYears)
 }
 
-func extractCosmosPricing(items []CosmosRetailPriceItem, termYears int) (onDemand, reservation float64, currency string) {
+func extractCosmosPricing(items []CosmosRetailPriceItem, sku string, termYears int) (onDemand, reservation float64, currency string) {
 	currency = "USD"
 	termStr := fmt.Sprintf("%d Years", termYears)
 
 	for _, item := range items {
+		if item.ArmSKUName != "" && !strings.EqualFold(item.ArmSKUName, sku) &&
+			item.SKUName != "" && !strings.EqualFold(item.SKUName, sku) {
+			continue
+		}
+
 		if item.CurrencyCode != "" {
 			currency = item.CurrencyCode
 		}
🤖 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/azure/services/cosmosdb/client.go` around lines 534 - 559,
getCosmosPricing ignores the sku parameter so pricing is selected across all
SKUs in a region; update the Azure retail-price filter and downstream selection
so only entries for the requested SKU are considered. Specifically, include the
SKU identifier in the filter passed to fetchAzurePricing (so filter string
combines serviceName, armRegionName and the sku field used by the pricing API)
and change extractCosmosPricing (or call-site logic) to restrict candidate items
to the requested sku and termYears before computing
onDemandPrice/reservationPrice/currency so reservationPrice truly corresponds to
the requested SKU.
providers/azure/services/search/client.go (1)

480-548: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The Search pricing lookup still isn't keyed to the requested SKU or full result set.

getSearchPricing() ignores sku, and fetchAzurePricing() still reads only the first page despite the response carrying NextPageLink. With multiple Search SKUs in one region or paginated price results, GetOfferingDetails can either quote the wrong SKU or incorrectly raise the new "no reservation pricing found" error.

Suggested direction
+type SearchRetailPriceItem struct {
+	CurrencyCode    string  `json:"currencyCode"`
+	RetailPrice     float64 `json:"retailPrice"`
+	UnitPrice       float64 `json:"unitPrice"`
+	ArmRegionName   string  `json:"armRegionName"`
+	ProductName     string  `json:"productName"`
+	ServiceName     string  `json:"serviceName"`
+	ArmSKUName      string  `json:"armSkuName"`
+	MeterName       string  `json:"meterName"`
+	ReservationTerm string  `json:"reservationTerm"`
+	Type            string  `json:"type"`
+}
+
-func (c *SearchClient) fetchAzurePricing(ctx context.Context, filter string) (*AzureRetailPrice, error) {
-	...
-	resp, err := c.httpClient.Do(req)
-	...
-	var priceData AzureRetailPrice
-	if err := json.NewDecoder(resp.Body).Decode(&priceData); err != nil {
-		return nil, fmt.Errorf("failed to decode pricing response: %w", err)
-	}
-	return &priceData, nil
+func (c *SearchClient) fetchAzurePricing(ctx context.Context, filter string) ([]SearchRetailPriceItem, error) {
+	params := url.Values{}
+	params.Add("$filter", filter)
+	params.Add("api-version", "2023-01-01-preview")
+	initialURL := "https://prices.azure.com/api/retail/prices?" + params.Encode()
+	return pricing.FetchAll[SearchRetailPriceItem](ctx, c.httpClient, initialURL, pricing.DefaultPageTimeout, pricing.DefaultMaxPages)
 }
 
-func extractSearchPricing(items []struct { ... }, termYears int) ...
+func extractSearchPricing(items []SearchRetailPriceItem, sku string, termYears int) ...

and filter extractSearchPricing on ArmSKUName == sku before choosing the reservation/on-demand rows.

🤖 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/azure/services/search/client.go` around lines 480 - 548,
getSearchPricing currently ignores sku and fetchAzurePricing only decodes the
first page, so you can get wrong SKU results or miss reservation rows; update
fetchAzurePricing to follow pagination by checking AzureRetailPrice.NextPageLink
and repeatedly GET subsequent pages (accumulating Items into a single
AzureRetailPrice.Items slice) before returning, and either include sku in the
initial $filter or (preferably) make extractSearchPricing filter the returned
priceData.Items by ArmSKUName == sku before computing onDemand/reservation
prices; adjust getSearchPricing to pass sku through unchanged to
extractSearchPricing so the pricing selection is correctly keyed to the
requested SKU.
🧹 Nitpick comments (1)
providers/azure/services/synapse/client_test.go (1)

453-490: ⚡ Quick win

Please add a regression test for the new unsupported-payment branch.

GetOfferingDetails now errors on unrecognized payment options, but this suite still only covers the two happy paths plus transport failure. A one-case negative test here would pin the no-silent-fallback money-path change.

Suggested test
+func TestGetOfferingDetails_unsupportedPaymentOption(t *testing.T) {
+	mHTTP := &mockHTTPClient{}
+	mHTTP.On("Do", mock.Anything).Return(newHTTPResponse(http.StatusOK, sampleSynapsePricingJSON), nil)
+
+	c := &SynapseClient{subscriptionID: "sub-123", region: "eastus", httpClient: mHTTP}
+
+	rec := common.Recommendation{ResourceType: "DW100c", Term: "1yr", PaymentOption: "weekly-bananas"}
+	details, err := c.GetOfferingDetails(context.Background(), rec)
+	require.Error(t, err)
+	assert.Nil(t, details)
+	assert.Contains(t, err.Error(), "unsupported payment option")
+}
🤖 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/azure/services/synapse/client_test.go` around lines 453 - 490, Add
a negative regression test that asserts GetOfferingDetails returns an error for
unrecognized payment options: create a test (e.g.,
TestGetOfferingDetails_unsupportedPayment) that sets up mockHTTP to return the
same sampleSynapsePricingJSON, constructs a SynapseClient and a
common.Recommendation with PaymentOption set to an invalid value like
"unsupported", calls c.GetOfferingDetails(context.Background(), rec) and asserts
an error is returned (and optionally that the error message mentions unsupported
or payment). This ensures the new unsupported-payment branch in
GetOfferingDetails is exercised instead of silently falling back.
🤖 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/azure/recommendations_test.go`:
- Around line 425-454: The test TestMergeServiceResults_OrderIsStable can
falsely pass because both computeRec and advisorRec set Service to
common.ServiceCompute; modify the mkRec fixture usage (or mkRec itself) to
include a unique identifying field on the Recommendation (e.g., set ResourceType
or Account on the computeRec and a different value on advisorRec) so the two
compute-type entries are distinguishable, then update the assertions after
calling mergeServiceResults to check the unique marker (ResourceType or Account)
for result[0] and result[5] instead of relying only on Service equality; this
targets mkRec, computeRec, advisorRec and the mergeServiceResults result checks.

In `@providers/azure/services/cache/client.go`:
- Around line 560-566: The error is triggered because extractRedisPricing only
matches the plural "%d Years" string so a singular "1 Year" row leaves
reservationPrice at 0 and causes a false-positive error; update
extractRedisPricing (and the same logic around reservationPrice handling) to
accept both singular and plural forms—e.g., match "%d Year" and "%d Years" or
use a regex/normalized comparison for termYears == 1 vs >1—so the code correctly
finds reservation pricing for 1-year entries and does not return the
fabricated-error branch.

In `@providers/azure/services/compute/client.go`:
- Around line 693-699: The new error on zero reservationPrice incorrectly flags
rows where Azure uses the singular term "1 Year" because extractVMPricing
currently only matches "%d Years"; update extractVMPricing (and any parsing used
by the reservationPrice calculation) to accept both singular and plural forms
(e.g., "1 Year" and "%d Years"/"%d Year") or normalize ReservationTerm before
parsing so that ReservationTerm == "1 Year" yields termYears==1 and sets
reservationPrice correctly; verify the same change is applied to the other
parsing site referenced in the comment to avoid false no-pricing errors.

---

Outside diff comments:
In `@providers/azure/services/cosmosdb/client.go`:
- Around line 534-559: getCosmosPricing ignores the sku parameter so pricing is
selected across all SKUs in a region; update the Azure retail-price filter and
downstream selection so only entries for the requested SKU are considered.
Specifically, include the SKU identifier in the filter passed to
fetchAzurePricing (so filter string combines serviceName, armRegionName and the
sku field used by the pricing API) and change extractCosmosPricing (or call-site
logic) to restrict candidate items to the requested sku and termYears before
computing onDemandPrice/reservationPrice/currency so reservationPrice truly
corresponds to the requested SKU.

In `@providers/azure/services/search/client.go`:
- Around line 480-548: getSearchPricing currently ignores sku and
fetchAzurePricing only decodes the first page, so you can get wrong SKU results
or miss reservation rows; update fetchAzurePricing to follow pagination by
checking AzureRetailPrice.NextPageLink and repeatedly GET subsequent pages
(accumulating Items into a single AzureRetailPrice.Items slice) before
returning, and either include sku in the initial $filter or (preferably) make
extractSearchPricing filter the returned priceData.Items by ArmSKUName == sku
before computing onDemand/reservation prices; adjust getSearchPricing to pass
sku through unchanged to extractSearchPricing so the pricing selection is
correctly keyed to the requested SKU.

---

Nitpick comments:
In `@providers/azure/services/synapse/client_test.go`:
- Around line 453-490: Add a negative regression test that asserts
GetOfferingDetails returns an error for unrecognized payment options: create a
test (e.g., TestGetOfferingDetails_unsupportedPayment) that sets up mockHTTP to
return the same sampleSynapsePricingJSON, constructs a SynapseClient and a
common.Recommendation with PaymentOption set to an invalid value like
"unsupported", calls c.GetOfferingDetails(context.Background(), rec) and asserts
an error is returned (and optionally that the error message mentions unsupported
or payment). This ensures the new unsupported-payment branch in
GetOfferingDetails is exercised instead of silently falling back.
🪄 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: 1abf8e13-a463-4a14-8bba-29a3c05da976

📥 Commits

Reviewing files that changed from the base of the PR and between cbdc4be and da76c08.

📒 Files selected for processing (26)
  • pkg/exchange/reshape_crossfamily_test.go
  • providers/azure/internal/recommendations/converter.go
  • providers/azure/mocks/azure_mocks.go
  • providers/azure/provider.go
  • providers/azure/provider_test.go
  • providers/azure/recommendations.go
  • providers/azure/recommendations_test.go
  • providers/azure/services/cache/client.go
  • providers/azure/services/cache/client_test.go
  • providers/azure/services/compute/client.go
  • providers/azure/services/compute/client_test.go
  • providers/azure/services/compute/exchange.go
  • providers/azure/services/cosmosdb/client.go
  • providers/azure/services/cosmosdb/client_test.go
  • providers/azure/services/database/client.go
  • providers/azure/services/database/client_test.go
  • providers/azure/services/internal/reservations/purchase.go
  • providers/azure/services/internal/reservations/purchase_test.go
  • providers/azure/services/managedredis/client.go
  • providers/azure/services/managedredis/client_test.go
  • providers/azure/services/savingsplans/client.go
  • providers/azure/services/savingsplans/client_test.go
  • providers/azure/services/search/client.go
  • providers/azure/services/search/client_test.go
  • providers/azure/services/synapse/client.go
  • providers/azure/services/synapse/client_test.go

Comment thread providers/azure/recommendations_test.go
Comment thread providers/azure/services/cache/client.go
Comment thread providers/azure/services/compute/client.go
cristim added 2 commits June 7, 2026 11:32
The Azure Retail Prices API returns "1 Year" (singular) for 1-year
reservation terms and "N Years" (plural) for 2+ year terms. All six
pricing extractors (compute, cache, database, cosmosdb, search,
managedredis) used fmt.Sprintf("%d Years", termYears) unconditionally,
so a 1-year reservation row was silently skipped and reservationPrice
stayed 0. This caused a false "no reservation pricing found" error even
when the reservation pricing row was present in the API response.

Add azureTermString(termYears int) string helper to each affected
package, returning "1 Year" for termYears==1 and "%d Years" otherwise.
Synapse already handled the singular/plural distinction independently.

Update the existing "1 Years" test fixtures in mock data and
client_test.go files for all six packages to use the correct "1 Year"
form, and add TestExtractVMPricing_SingularOneYear,
TestExtractVMPricing_PluralThreeYears,
TestExtractRedisPricing_SingularOneYear,
TestExtractRedisPricing_PluralThreeYears regression tests that fail
on the pre-fix code and pass after.

Addresses CR finding on providers/azure/services/compute/client.go:699
and providers/azure/services/cache/client.go:566 from PR #1045 review.
TestMergeServiceResults_OrderIsStable set Service: ServiceCompute on
both computeRec and advisorRec. A compute/advisor position swap in
mergeServiceResults would still pass since both assertions checked
the same Service value.

Add a marker string via a new ResourceType parameter to mkRec and
assert on ResourceType instead of Service for result[0] and result[5]
so compute and advisor entries are distinguishable and a position swap
is caught immediately.

Addresses CR finding on providers/azure/recommendations_test.go:454
from PR #1045 review.
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Addressed CR pass findings:

  • Finding 1 (compute/client.go:699, Major): extractVMPricing rejected 1-year rows as "no reservation pricing" because it matched only "%d Years" while Azure's API returns "1 Year" (singular). Fixed in 5e938d9 by adding azureTermString(termYears) helper to all 6 pricing extractors (compute, cache, database, cosmosdb, search, managedredis) and updating test fixtures from "1 Years" to "1 Year". Regression tests TestExtractVMPricing_SingularOneYear and TestExtractVMPricing_PluralThreeYears added.

  • Finding 2 (cache/client.go:566, Major): Same singular/plural mismatch in extractRedisPricing. Fixed in the same commit (5e938d9) alongside the other 4 affected extractors. Regression tests TestExtractRedisPricing_SingularOneYear and TestExtractRedisPricing_PluralThreeYears added.

  • Finding 3 (recommendations_test.go:454, Minor): TestMergeServiceResults_OrderIsStable could falsely pass since computeRec and advisorRec both had Service: ServiceCompute. Fixed in c873b35 by adding a marker string parameter to mkRec, setting unique ResourceType values per fixture, and asserting on ResourceType rather than Service for positions 0 and 5.

All 692 tests pass (go test -race ./... in providers/azure). Repo-root go build ./... clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026 •

Copy link
Copy Markdown
Contributor

@cristim All three findings look correctly addressed:

  • Finding 1 & 2 (Major): The azureTermString(termYears) helper elegantly handles the Azure API's singular/plural asymmetry ("1 Year" vs "3 Years") in one place rather than inline format strings in each extractor — good that it was applied uniformly across all 6 extractors.
  • Finding 3 (Minor): Using ResourceType as the discriminator in TestMergeServiceResults_OrderIsStable is the right fix; asserting position by a field that is unique per fixture entry makes the test meaningful.

Triggering an incremental review of commits 5e938d9 and c873b35 now.


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1045

Timestamp: 2026-06-07T18:34:23.733Z
Learning: In LeanerCloud/CUDly Azure pricing extractors (compute, cache, database, cosmosdb, search, managedredis), Azure's Retail Prices API returns "1 Year" (singular) for 1-year reservation terms and "3 Years" (plural) for 3-year terms. All 6 pricing extractors use `azureTermString(termYears int) string` helper (added in PR `#1045` commit 5e938d9) to produce the correct singular/plural form for term matching. Prior to this fix, "%d Years" format produced "1 Years" which never matched Azure API responses, causing all 1-year reservation pricing lookups to silently fail. Regression tests: `TestExtractVMPricing_SingularOneYear`, `TestExtractVMPricing_PluralThreeYears`, `TestExtractRedisPricing_SingularOneYear`, `TestExtractRedisPricing_PluralThreeYears`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.040Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` no longer hardcodes `Term = "1yr"` (fixed in PR `#1047` commit 8f9a787, H-3 audit finding). It propagates `params.Term` with a `"1yr"` default, validates via `termPlan`, and returns nil on unknown term. This is stricter than memorystore/cloudstorage/cloudsql (which default and continue): computeengine returns nil on unknown term because it is on the purchase path. Regression tests: `TestConvertGCPRecommendation_PropagatesParamsTerm`, `TestConvertGCPRecommendation_RejectsUnknownTerm`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:28:28.112Z
Learning: In LeanerCloud/CUDly `providers/gcp/recommendations.go`, `collectRegion` fans out to all four GCP service clients (computeengine, cloudsql, memorystore, cloudstorage) concurrently (fixed in PR `#1047` commit 95a390f, H-2 finding). `regionResult` gains `cache` and `storage` slices; merge is updated accordingly. The decision was to wire (not trim) memorystore and cloudstorage because both have complete `GetRecommendations` implementations and their no-op `PurchaseCommitment` paths are orthogonal to surfacing advisory recommendations. Regression tests: `TestRegionResult_HasCacheAndStorageFields` and `TestShouldIncludeService_Cache_Storage`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:28:28.112Z
Learning: In LeanerCloud/CUDly GCP service clients (computeengine, cloudsql, memorystore, cloudstorage), `GetRecommendations` filters out non-ACTIVE recommendations using the typed `recommenderpb.RecommendationStateInfo_ACTIVE` enum (CLAIMED/SUCCEEDED/FAILED/DISMISSED are skipped). State is accessed nil-safely via the proto getter. Fixed in PR `#1047` commit 95a390f (H-1 finding). Regression tests: `TestGetRecommendations_FiltersNonActiveStates` (5-rec mock, only ACTIVE survives) and `TestGetRecommendations_ActiveRecIncluded`. Existing mock tests in all four services had StateInfo added to satisfy the new filter.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-04T14:28:19.903Z
Learning: In providers/azure/recommendations.go (Go, Azure provider), the service constructors (compute.NewClient, cache.NewClient, etc.) build concrete Azure SDK types with no interface injection seam. Introducing a concurrency timing test that asserts elapsed ≈ max-single-service latency would require adding package-level function variables — a refactor that is out of scope for small performance PRs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, the `memMBPerVCPU = 4096` const was removed in PR `#1047` commit 8f9a787. MEMORY MB is now extracted directly from the Recommender payload via `memoryMBFromOperationGroups` (sibling of `vcpuCountFromOperationGroups`). `buildInsertRequest` and `GroupCommitments` call `memoryMBFromDetails(rec)` which returns an error (no silent fallback) when `Details.MemoryGB` is absent. This ensures high-memory families (e.g. N2-highmem) get the correct MB from the payload rather than the GENERAL_PURPOSE approximation. Regression test: `TestBuildInsertRequest_RefusesMissingMemory`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), `internal/commitmentopts/normalizePayment` is unexported and AWS-spelling-focused (parses "All Upfront"/"ALL_UPFRONT" → canonical AWS tokens). `internal/api/validation.go:purchasePaymentWhitelist` is the purchase-execute boundary. `pkg/common/reservation_name.go:normalizeReservationPayment` produces short-form name segments. These are all distinct concerns from the cross-provider `NormalizePaymentOption` helper in `internal/config/validation.go`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T18:13:37.421Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `termPlan` returns `(string, error)` using `computepb.Commitment_TWELVE_MONTH.String()` and `computepb.Commitment_THIRTY_SIX_MONTH.String()` as the canonical plan strings (fixed in PR `#1047` commit 8f9a787). It accepts 1yr/1/12mo and 3yr/3/36mo; it errors on empty or unrecognized input (no silent 12-month default). Regression tests: `TestTermPlan_UsesSdkEnumConstants`, `TestTermPlan_RejectsUnknownTerm`, `TestTermPlan_AcceptsAllDocumentedForms`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.
✅ 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant