Skip to content

feat(aws): AWSLadder capability write side - #1349

Merged
cristim merged 3 commits into
feat/ladder-aws-readfrom
feat/ladder-aws-write
Jul 3, 2026
Merged

feat(aws): AWSLadder capability write side#1349
cristim merged 3 commits into
feat/ladder-aws-readfrom
feat/ladder-aws-write

Conversation

@cristim

@cristim cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Completes the AWS LadderCapability: PurchaseLayer and ReshapeBuffer replace the PR 5 not-wired stubs. Write-side dependencies (riPurchaser, spPurchaser, exchangeRunner) are wired via WithWriteSide; unwired instances keep failing loud, so read-only wiring stays free of purchase/exchange infrastructure.
  • PurchaseLayer dispatches by layer: LayerConvertibleRI -> EC2 client (idempotency-tag dedupe guard), LayerEC2InstanceSP/LayerComputeSP -> Savings Plans client with the matching plan type. Boundary validation before any AWS call: supported-layer check, mandatory non-empty opts.IdempotencyToken (re-driven executions can never double-buy), and per-target rec validation (*ComputeDetails + Count/InstanceType for RIs; *SavingsPlanDetails + plan-type match + positive finite HourlyCommitment for SPs; Term/PaymentOption for both). Client errors are wrapped with layer context via %w, preserving errors.Is(err, common.ErrCommitmentPurchaseNotSupported).
  • ReshapeBuffer is a thin delegation to exchange.RunAutoExchange through a config-only exchangeRunner seam (the concrete runner owns store/quote-execute client/offering lookup/inventory, mirroring internal/server.executeRIExchangeReshape). Money caps: nil = no cap maps to an explicit unlimitedCapUSD constant (never 0 — a zero cap would silently block every exchange and is rejected loudly; +Inf is unusable because big.Rat.SetFloat64(+Inf) returns nil). Outcome maps to ReshapeSummary; partial failures return the populated summary TOGETHER with a non-nil error.
  • DryRun is rejected fail-loud rather than mapped onto the exchange flow's manual mode: manual mode persists pending ExchangeRecords with live approval tokens (actionable money instruments) and still triggers the store-wide pending cancellation — neither is a simulation. A true simulation mode in pkg/exchange is tracked in feat(exchange): add true simulation mode to RunAutoExchange; make pending-cancellation coordination explicit #1348; until then the engine previews reshapes via ActionReshape rationales without calling ReshapeBuffer.

Coordination warning for the pipeline phase

exchange.RunAutoExchange begins by unconditionally canceling ALL pending exchange records store-wide (CancelAllPendingExchanges) — a side effect shared with the standalone ri_exchange_reshape scheduled task. The pipeline phase that invokes ReshapeBuffer must coordinate with (or disable) the standalone scheduler; the two must never run concurrently against the same store. Documented prominently in the ReshapeBuffer godoc.

CodeRabbit nitpick fixes carried from #1347

  • Savings Plan plan-type constants are now derived from the AWS SDK enum (string(sptypes.SavingsPlanTypeEc2Instance) / ...Compute), replacing the scattered "EC2Instance"/"Compute" literals across dispatch, filtering, and validation.
  • The two-method coverageSource interface is split into single-method riCoverageSource and onDemandSeriesSource (interface segregation); method names unchanged, so one concrete adapter can still satisfy both.

Stacked on #1347. Part of #1335 (phase 2 of #1333) - completes the AWS LadderCapability.

Test plan

  • 107 hermetic tests (fakes for all injected interfaces; no AWS calls): purchase dispatch per layer with call-count assertions, unknown-layer and missing-idempotency-token rejection, per-target rec validation tables, client-error wrapping and ErrCommitmentPurchaseNotSupported passthrough, cap mapping incl. nil-vs-zero distinction, DryRun fail-loud with zero runner invocations, summary mapping incl. partial failures, runner-contract nil-result guard, plus the full PR 5 read-side suite.
  • All gates exit 0 from providers/aws/: go build ./..., go vet ./ladder/..., gofmt -l ladder/ (empty), go test ./ladder/... -count=1, golangci-lint run --config ../../.golangci.yml ./ladder/..., gocyclo -over 10 ladder/ (empty).

Summary by CodeRabbit

  • New Features

    • Added ladder “write side” support with explicit runtime wiring and a WithWriteSide step.
    • Introduced PurchaseLayer for executing RI and Savings Plan purchases with recommendation validation.
    • Added ReshapeBuffer to run automated buffer reshaping, including caps/threshold validation and audit summaries.
  • Bug Fixes

    • Improved usage baseline and layer state sourcing for more accurate recommendations.
    • Standardized Savings Plan plan-type handling and improved write-side error messaging.
  • Tests

    • Expanded coverage with new purchase and reshape test suites, plus updated wiring/validation assertions.

PurchaseLayer dispatches to the savingsplans/ec2 purchase clients with
mandatory idempotency tokens and per-target boundary validation.
ReshapeBuffer is a thin RunAutoExchange delegation with fail-loud dry-run
rejection pending #1348 and a documented store-wide pending-cancellation
warning. Also: SDK-derived plan-type constants and coverage interface split.

Part of #1335.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/feat New capability labels Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60edaff1-5c4c-418b-89bf-9b7f888ab988

📥 Commits

Reviewing files that changed from the base of the PR and between b459358 and b42fee4.

📒 Files selected for processing (6)
  • providers/aws/ladder/baseline.go
  • providers/aws/ladder/ladder.go
  • providers/aws/ladder/purchase.go
  • providers/aws/ladder/purchase_test.go
  • providers/aws/ladder/reshape.go
  • providers/aws/ladder/reshape_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • providers/aws/ladder/baseline.go
  • providers/aws/ladder/purchase_test.go
  • providers/aws/ladder/reshape_test.go
  • providers/aws/ladder/reshape.go
  • providers/aws/ladder/ladder.go

📝 Walkthrough

Walkthrough

This PR separates AWSLadder read-side coverage sources from on-demand series sources, introduces named Savings Plan plan-type constants, adds explicit write-side wiring, and implements PurchaseLayer and ReshapeBuffer with corresponding tests.

Changes

AWS Ladder Read/Write Side Refactor and Implementation

Layer / File(s) Summary
Interface segregation and plan-type constants
providers/aws/ladder/interfaces.go
Splits coverageSource into riCoverageSource and onDemandSeriesSource; adds SP plan-type constants from AWS SDK enums and new write-side interfaces riPurchaser, spPurchaser, exchangeRunner.
AWSLadder struct, constructor, and WithWriteSide wiring
providers/aws/ladder/ladder.go, providers/aws/ladder/ladder_test.go
Reworks New to accept separate RI coverage and on-demand series sources, updates struct fields, adds WithWriteSide for write dependencies, removes prior stub methods, and updates tests to the new constructor signature and error wording.
Read-side call sites for RI coverage and SP plan types
providers/aws/ladder/baseline.go, providers/aws/ladder/commitments.go, providers/aws/ladder/layer_states.go
Updates GetUsageBaseline and GetLayerStates to use the new onDemand/riCoverage fields and switches SP plan-type comparisons to the new named constants.
PurchaseLayer implementation and tests
providers/aws/ladder/purchase.go, providers/aws/ladder/purchase_test.go
Implements PurchaseLayer dispatch to RI/SP purchase handlers with validation of recommendation details, term/payment, and idempotency token; adds comprehensive dispatch, validation, and error-wrapping tests.
ReshapeBuffer implementation and tests
providers/aws/ladder/reshape.go, providers/aws/ladder/reshape_test.go
Implements ReshapeBuffer mapping config to exchange.RIExchangeConfig, running the exchange, and summarizing results; adds tests for cap mapping, validation, dry-run rejection, and outcome summarization.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant PurchaseLayer
  participant riPurchase
  participant spPurchase
  participant riPurchaser
  participant spPurchaser
  Caller->>PurchaseLayer: layer, rec, opts
  PurchaseLayer->>PurchaseLayer: validate wiring, layer type, idempotency token
  alt convertible RI
    PurchaseLayer->>riPurchase: purchaseRI(rec, opts)
    riPurchase->>riPurchaser: PurchaseCommitment
  else EC2Instance/Compute SP
    PurchaseLayer->>spPurchase: purchaseSP(rec, opts, planType)
    spPurchase->>spPurchaser: PurchaseCommitment
  end
  riPurchaser-->>Caller: PurchaseResult
  spPurchaser-->>Caller: PurchaseResult
Loading
sequenceDiagram
  participant Caller
  participant ReshapeBuffer
  participant buildRIExchangeConfig
  participant exchangeRunner
  participant summarizeExchangeResult
  Caller->>ReshapeBuffer: scope, cfg
  ReshapeBuffer->>ReshapeBuffer: validate wiring, scope, DryRun
  ReshapeBuffer->>buildRIExchangeConfig: cfg
  buildRIExchangeConfig-->>ReshapeBuffer: RIExchangeConfig
  ReshapeBuffer->>exchangeRunner: RunAutoExchange(RIExchangeConfig)
  exchangeRunner-->>ReshapeBuffer: AutoExchangeResult
  ReshapeBuffer->>summarizeExchangeResult: AutoExchangeResult
  summarizeExchangeResult-->>Caller: ReshapeSummary, error
Loading

Suggested labels: enhancement

🚥 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 clearly summarizes the main change: adding the AWSLadder write side capability and wiring.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ladder-aws-write

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

@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 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 (4)
providers/aws/ladder/purchase_test.go (1)

119-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the "write side not wired" guard.

PurchaseLayer returns errWriteNotWired when a.riPurchase/a.spPurchase are nil (purchase.go Line 38-40), but no test in this file exercises PurchaseLayer on a ladder that hasn't had WithWriteSide called. TestWithWriteSide_NilArgsRejected only covers WithWriteSide itself rejecting nil args, not PurchaseLayer's own nil-dependency guard.

✅ Suggested test addition
func TestPurchaseLayer_WriteSideNotWired_ErrorsWithoutCalling(t *testing.T) {
	a := newTestLadder(t, &fakeRILister{}, &fakeSPLister{}, &fakeCoverageSource{}, &fakeUtilizationSource{})
	_, err := a.PurchaseLayer(context.Background(), ladder.LayerConvertibleRI, validRIRec(), validPurchaseOpts())
	require.Error(t, err)
	assert.ErrorIs(t, err, errWriteNotWired)
}
🤖 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/ladder/purchase_test.go` around lines 119 - 301,
`PurchaseLayer` is missing coverage for its own write-side wiring guard: add a
test that calls `PurchaseLayer` on a ladder created without `WithWriteSide` so
the nil `riPurchase`/`spPurchase` path is exercised. Use the existing
`PurchaseLayer` and `newTestLadder` helpers, pass a valid recommendation/opts,
and assert the call fails with `errWriteNotWired` without invoking any
purchaser.
providers/aws/ladder/ladder.go (1)

59-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale dependency count in doc comment.

All four read data-source dependencies are injected via narrow interfaces so that unit tests are hermetic (no real AWS calls needed). But New's own docstring in this same file states New constructs an AWSLadder. The five required read-side interfaces must be non-nil; spCov and spUtil may be nil (wired later). Since coverageSource was split into riCoverageSource and onDemandSeriesSource, the required count grew from 4 to 5; this struct-level comment wasn't updated to match.

📝 Proposed fix
-// All four read data-source dependencies are injected via narrow interfaces so
+// All five read data-source dependencies are injected via narrow interfaces so
 // that unit tests are hermetic (no real AWS calls needed). The caller wires the
 // concrete adapters (ec2svc.Client, savingsplans.Client, etc.) at startup.
🤖 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/ladder/ladder.go` around lines 59 - 71, Update the AWSLadder
documentation comment to reflect the current required read-side wiring: the
split of coverageSource into riCoverageSource and onDemandSeriesSource means the
constructor now needs five non-nil read dependencies, not four. Adjust the
comment near AWSLadder/New to mention the five required read-side interfaces and
keep the note that spCov and spUtil may still be nil until WithWriteSide wiring.
providers/aws/ladder/baseline.go (1)

58-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale "coverage source" reference in error message.

The fetch now goes through a.onDemand.GetOnDemandSeries (renamed from the old coverageSource), but the adjacent length-check error still says GetUsageBaseline: series length %d is below minimum %d days; extend the lookback window or check the coverage source. Since there is no longer a "coverage source" involved in this path (it's the on-demand series source), this message will mislead an on-call engineer debugging a short series.

📝 Proposed fix
 	if len(series) < minBaselineSeriesDays {
 		return ladder.UsageBaseline{}, fmt.Errorf(
-			"GetUsageBaseline: series length %d is below minimum %d days; extend the lookback window or check the coverage source",
+			"GetUsageBaseline: series length %d is below minimum %d days; extend the lookback window or check the on-demand series source",
 			len(series), minBaselineSeriesDays,
 		)
 	}
🤖 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/ladder/baseline.go` around lines 58 - 70, The length-check
error message in GetUsageBaseline still refers to the old “coverage source,”
which is misleading now that the data comes from a.onDemand.GetOnDemandSeries.
Update the fmt.Errorf text in the UsageBaseline path to mention the on-demand
series source instead of coverage source, while keeping the existing
minBaselineSeriesDays and len(series) context so engineers can correctly
identify the failing input.
providers/aws/ladder/reshape.go (1)

188-192: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Misleading denominator in partial-failure message.

summary.Analyzed includes Pending and Skipped items, which are not "exchange attempts." If a run has failures alongside skipped/pending items, the message ("N of Analyzed exchange attempt(s) failed") overstates the attempt count, which could confuse an on-call engineer reading the error during a money-path incident. Consider denominating by len(result.Completed)+len(result.Failed) (actual attempts) instead.

✏️ Proposed fix
 	if len(result.Failed) > 0 {
+		attempts := len(result.Completed) + len(result.Failed)
 		return summary, fmt.Errorf(
-			"ReshapeBuffer: %d of %d exchange attempt(s) failed (first: %s: %s); see summary details for the full list",
-			len(result.Failed), summary.Analyzed, result.Failed[0].SourceRIID, result.Failed[0].Error)
+			"ReshapeBuffer: %d of %d exchange attempt(s) failed (first: %s: %s); see summary details for the full list",
+			len(result.Failed), attempts, result.Failed[0].SourceRIID, result.Failed[0].Error)
 	}
🤖 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/ladder/reshape.go` around lines 188 - 192, The partial-failure
error in ReshapeBuffer uses summary.Analyzed as the total attempt count, which
includes Pending and Skipped items. Update the failure message in reshape.go
within the ReshapeBuffer flow to denominator by the actual attempted exchanges,
using len(result.Completed)+len(result.Failed), while keeping the existing
first-failure details from result.Failed[0].SourceRIID and
result.Failed[0].Error.
🤖 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/ladder/baseline.go`:
- Around line 58-70: The length-check error message in GetUsageBaseline still
refers to the old “coverage source,” which is misleading now that the data comes
from a.onDemand.GetOnDemandSeries. Update the fmt.Errorf text in the
UsageBaseline path to mention the on-demand series source instead of coverage
source, while keeping the existing minBaselineSeriesDays and len(series) context
so engineers can correctly identify the failing input.

In `@providers/aws/ladder/ladder.go`:
- Around line 59-71: Update the AWSLadder documentation comment to reflect the
current required read-side wiring: the split of coverageSource into
riCoverageSource and onDemandSeriesSource means the constructor now needs five
non-nil read dependencies, not four. Adjust the comment near AWSLadder/New to
mention the five required read-side interfaces and keep the note that spCov and
spUtil may still be nil until WithWriteSide wiring.

In `@providers/aws/ladder/purchase_test.go`:
- Around line 119-301: `PurchaseLayer` is missing coverage for its own
write-side wiring guard: add a test that calls `PurchaseLayer` on a ladder
created without `WithWriteSide` so the nil `riPurchase`/`spPurchase` path is
exercised. Use the existing `PurchaseLayer` and `newTestLadder` helpers, pass a
valid recommendation/opts, and assert the call fails with `errWriteNotWired`
without invoking any purchaser.

In `@providers/aws/ladder/reshape.go`:
- Around line 188-192: The partial-failure error in ReshapeBuffer uses
summary.Analyzed as the total attempt count, which includes Pending and Skipped
items. Update the failure message in reshape.go within the ReshapeBuffer flow to
denominator by the actual attempted exchanges, using
len(result.Completed)+len(result.Failed), while keeping the existing
first-failure details from result.Failed[0].SourceRIID and
result.Failed[0].Error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02345d7c-8d21-4fea-a610-ed702075a2a3

📥 Commits

Reviewing files that changed from the base of the PR and between 018da7a and b459358.

📒 Files selected for processing (10)
  • providers/aws/ladder/baseline.go
  • providers/aws/ladder/commitments.go
  • providers/aws/ladder/interfaces.go
  • providers/aws/ladder/ladder.go
  • providers/aws/ladder/ladder_test.go
  • providers/aws/ladder/layer_states.go
  • providers/aws/ladder/purchase.go
  • providers/aws/ladder/purchase_test.go
  • providers/aws/ladder/reshape.go
  • providers/aws/ladder/reshape_test.go

cristim added 2 commits July 3, 2026 22:03
Require non-empty Platform/Tenancy/Scope on EC2 RI purchase recs (the ec2
client silently defaults empty Tenancy to "default" and Scope to "Regional",
which could buy a default-tenancy RI from a dedicated-tenancy rec). Correct
the ReshapeBuffer godoc: daily-cap stops surface as Failed plus an error,
not Skipped (upstream reclassification tracked in #1348). Add tests for the
new rejections, +Inf HourlyCommitment, and the inverse plan-type mismatch.
Add direct coverage of the write-side-not-wired guard (errors.Is on both
write methods of a New()-built ladder). Fix the stale read-dependency count
in the AWSLadder doc comment (four -> five after the coverage interface
split) and the stale "coverage source" reference in the short-series error.
Denominate the partial-failure message by actual attempts (completed +
failed) instead of Analyzed, which includes never-attempted items.
@cristim

cristim commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 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 0216127 into feat/ladder-aws-read Jul 3, 2026
4 checks passed
@cristim
cristim deleted the feat/ladder-aws-write branch July 27, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant