Skip to content

feat(ladder): add ladder_execution_enabled kill-switch gating AWS write side - #1396

Merged
cristim merged 1 commit into
mainfrom
feat/ladder-write-wiring
Jul 16, 2026
Merged

feat(ladder): add ladder_execution_enabled kill-switch gating AWS write side#1396
cristim merged 1 commit into
mainfrom
feat/ladder-write-wiring

Conversation

@cristim

@cristim cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Adds a ladder_execution_enabled global kill-switch (default FALSE, fail-closed) that gates the AWS ladder write side independently of the existing laddering_enabled flag.

Dual-flag gate

  • laddering_enabled (migration 000079): controls whether the ladder scheduler runs at all and produces plans. Existing flag; unchanged.
  • ladder_execution_enabled (migration 000083): controls whether purchases and exchanges actually fire against AWS. New flag; defaults to FALSE so any deployment that already has laddering_enabled=true continues to produce plans but never calls AWS purchase APIs until an operator explicitly opts in.

Both must be true for the write side to be wired with real AWS clients. Fail-closed design: missing/false means no spend.

Disabled shims

When ladder_execution_enabled is false, WireWriteSide returns a disabledPurchaser and disabledExchangeRunner in place of the real AWS clients. Both shims return ErrLadderExecutionDisabled on every call so the caller sees an explicit, named error rather than a silent no-op or a fabricated success.

Idempotency preserved

WireWriteSide reuses the production ec2svc/savingsplansvc purchaser constructors with their ClientToken/EC2-tag idempotency logic fully intact. The kill-switch only controls whether those purchasers are wired in; it does not alter how they behave when they are.

Spend-prevention test

TestKillSwitchPreventsSpend in ladder_write_test.go proves that both PurchaseLayer and ReshapeBuffer no-op (return ErrLadderExecutionDisabled) when the kill-switch is off, using a mock factory that would panic if a real AWS call were made.

Migration

Migration 000083 adds ladder_execution_enabled BOOLEAN NOT NULL DEFAULT FALSE to global_config. Idempotent (ADD COLUMN IF NOT EXISTS). The down migration drops it cleanly.

Part of #1336 (ladder phase 3 pipeline).

Summary by CodeRabbit

  • New Features
    • Added a configuration switch to control whether ladder execution can perform real AWS purchases and reshaping.
    • Ladder operations now fail safely with a clear error when execution is disabled.
    • Enabled ladder execution can now connect to AWS services for reserved-instance purchases, exchanges, and reshaping.
    • The new setting defaults to disabled and is stored with global configuration.

…itch

Add the ladder_execution_enabled kill-switch (default FALSE) and wire the
AWS ladder write side behind it. Both laddering_enabled AND
ladder_execution_enabled must be true for any purchase or reshape to reach
AWS; otherwise the wired ladder returns ErrLadderExecutionDisabled without
an outbound call. The ladder_run handler stays plan-only.

Migration 000083 adds the boolean column (numbered 000083, not 000082, to
avoid colliding with the in-flight 000082_rename_cancelled_to_canceled on
PR #1277). The column is threaded through GlobalConfig via COALESCE so older
rows read as false; pgxmock tests cover the new column 22.

providers/aws/ladder: WireWriteSide wires real EC2 + Savings Plans (umbrella
mode) clients plus the exchangeRunner; WireWriteSideDisabled wires shims that
return the exported ErrLadderExecutionDisabled sentinel (errors.Is-able,
distinct from the internal errWriteNotWired programming-error).

internal/server: exchangeRunnerAdapter satisfies the unexported exchangeRunner
seam via Go structural typing, forwarding LadderRunID and DryRun into
RunAutoExchangeParams (gap G10 / issue #1348 origin scoping). wireLadderWriteSide
is a no-op for non-AWSLadder test fakes. executionEnabled is threaded
structurally through handleLadderRun -> runLadderConfigs ->
processOneLadderConfig -> buildAndWireCapability -> wireLadderWriteSide;
buildAndWireCapability keeps processOneLadderConfig at or below cyclomatic 10.

Tests: a real *AWSLadder wired with executionEnabled=false refuses both
PurchaseLayer and ReshapeBuffer with ErrLadderExecutionDisabled and no AWS
call (offline, no creds); ladder-package kill-switch tests assert the same at
the provider boundary; handler plan-only invariant t.Fatal guards retained.
@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 16, 2026
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 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 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ec7ff2f-5fdc-4b73-bfc7-fd048e69af8c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a persisted ladder_execution_enabled flag, propagates it through ladder runs, and conditionally wires AWS purchase and exchange operations. Disabled execution returns a dedicated error without invoking AWS write APIs.

Changes

Ladder execution kill-switch

Layer / File(s) Summary
Persisted execution configuration
internal/database/postgres/migrations/*, internal/config/types.go, internal/config/store_postgres.go, internal/config/store_postgres_pgxmock_test.go
Adds the ladder_execution_enabled column and GlobalConfig field, loads and persists the value, and updates PostgreSQL mock coverage.
Ladder run propagation and wiring
internal/server/handler_ladder.go, internal/server/ladder_write.go, internal/server/*_test.go
Passes the execution flag through ladder processing, constructs wired capabilities, and adapts ladder exchange runs to AWS-backed exchange operations.
AWS write-side gate
providers/aws/ladder/factory.go, providers/aws/ladder/ladder.go, providers/aws/ladder/purchase_test.go
Adds enabled and disabled write-side wiring, introduces ErrLadderExecutionDisabled, and tests disabled purchase and reshape behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant GlobalConfig
  participant handleLadderRun
  participant Application
  participant AWSLadder
  participant AWS
  GlobalConfig->>handleLadderRun: provide LadderExecutionEnabled
  handleLadderRun->>Application: buildAndWireCapability(executionEnabled)
  Application->>AWSLadder: WireWriteSide or WireWriteSideDisabled
  AWSLadder->>AWS: invoke purchase or exchange APIs when enabled
Loading
🚥 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 matches the main change: adding a ladder_execution_enabled kill-switch for AWS write-side gating.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ladder-write-wiring

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/config/store_postgres_pgxmock_test.go (1)

63-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the enabled value and its bind position.

All fixtures use false, and the UPSERT accepts only AnyArgs. Seed ladder_execution_enabled=true, assert the loaded/merged field is true, and match bind argument 22 explicitly; otherwise swapping it with LadderingEnabled can still pass.

Also applies to: 117-130, 188-188, 217-226, 272-272

🤖 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 `@internal/config/store_postgres_pgxmock_test.go` around lines 63 - 75, The
PostgreSQL mock fixtures and assertions must exercise
ladder_execution_enabled=true and verify its exact bind position. Update the
affected test cases around the pgxmock row setup, loaded/merged expectations,
and UPSERT arguments to use true for this field and explicitly match bind
argument 22, distinguishing it from LadderingEnabled.
🤖 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 `@internal/server/ladder_write_test.go`:
- Around line 34-51: Update
TestWireLadderWriteSide_ExecutionDisabled_RealAWSLadder_RefusesWithoutAWSCall to
clear ambient AWS credentials and disable EC2 instance metadata before calling
awsladder.NewFromAWSConfig, using t.Setenv for the relevant AWS environment
variables. Remove t.Parallel because t.Setenv cannot be used safely with
parallel tests, while preserving the existing offline construction and
disabled-write assertions.

---

Nitpick comments:
In `@internal/config/store_postgres_pgxmock_test.go`:
- Around line 63-75: The PostgreSQL mock fixtures and assertions must exercise
ladder_execution_enabled=true and verify its exact bind position. Update the
affected test cases around the pgxmock row setup, loaded/merged expectations,
and UPSERT arguments to use true for this field and explicitly match bind
argument 22, distinguishing it from LadderingEnabled.
🪄 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: 05dbcc8c-6a8d-4283-9de9-ee87adf2aaff

📥 Commits

Reviewing files that changed from the base of the PR and between 1391680 and f511312.

📒 Files selected for processing (12)
  • internal/config/store_postgres.go
  • internal/config/store_postgres_pgxmock_test.go
  • internal/config/types.go
  • internal/database/postgres/migrations/000083_ladder_execution_enabled.down.sql
  • internal/database/postgres/migrations/000083_ladder_execution_enabled.up.sql
  • internal/server/handler_ladder.go
  • internal/server/handler_ladder_test.go
  • internal/server/ladder_write.go
  • internal/server/ladder_write_test.go
  • providers/aws/ladder/factory.go
  • providers/aws/ladder/ladder.go
  • providers/aws/ladder/purchase_test.go

Comment on lines +34 to +51
func TestWireLadderWriteSide_ExecutionDisabled_RealAWSLadder_RefusesWithoutAWSCall(t *testing.T) {
t.Parallel()
ctx := context.Background()
const (
region = "us-east-1"
accountID = "123456789012"
)

// Real AWSLadder from the production factory (read side wired to real clients;
// construction is offline: the AWS SDK builds clients lazily, no network call).
capRaw, err := awsladder.NewFromAWSConfig(ctx, region, accountID)
require.NoError(t, err)

// Wire the write side with the kill-switch OFF. app fields are unused on the
// disabled path (WireWriteSideDisabled ignores the exchange adapter), so a
// zero-value Application is sufficient.
app := &Application{}
wired, err := app.wireLadderWriteSide(ctx, false /* executionEnabled */, region, accountID, capRaw)

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n '^go [0-9]+\.[0-9]+' go.mod
rg -n --glob='*.go' '\.Setenv\(' .

Repository: LeanerCloud/CUDly

Length of output: 21153


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Map the relevant files first.
ast-grep outline internal/server/ladder_write_test.go --view expanded
printf '\n---\n'
ast-grep outline internal/server/*.go --view expanded 2>/dev/null | sed -n '1,220p'
printf '\n---\n'
ast-grep outline providers/aws/*.go --view expanded 2>/dev/null | sed -n '1,260p'
printf '\n---\n'
ast-grep outline internal/server/ladder.go --view expanded 2>/dev/null || true

# Show the specific test and constructor-related code with line numbers.
printf '\n===== ladder_write_test.go =====\n'
cat -n internal/server/ladder_write_test.go | sed -n '1,220p'

printf '\n===== search for NewFromAWSConfig / wireLadderWriteSide =====\n'
rg -n "NewFromAWSConfig|wireLadderWriteSide|WireWriteSideDisabled|ExecutionDisabled" internal providers -g '*.go'

Repository: LeanerCloud/CUDly

Length of output: 40228


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Focused inspection of the test file and AWS ladder implementation.
cat -n internal/server/ladder_write_test.go | sed -n '1,220p'

printf '\n===== matching symbols =====\n'
rg -n "NewFromAWSConfig|wireLadderWriteSide|WireWriteSideDisabled|ExecutionDisabled|AWSLadder" internal providers -g '*.go'

Repository: LeanerCloud/CUDly

Length of output: 18108


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '1,220p' internal/server/ladder_write_test.go
printf '\n===== constructors =====\n'
rg -n "func NewFromAWSConfig|func \(.*\) wireLadderWriteSide|WireWriteSideDisabled|NewFromAWSConfig" internal providers -g '*.go'

Repository: LeanerCloud/CUDly

Length of output: 6991


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Find the AWS ladder implementation file(s).
git ls-files | rg '^.*awsladder.*\.go$|^.*ladder.*\.go$|^providers/aws/.*\.go$|^internal/server/.*\.go$'

Repository: LeanerCloud/CUDly

Length of output: 4416


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '===== providers/aws/ladder/factory.go =====\n'
cat -n providers/aws/ladder/factory.go | sed -n '1,240p'

printf '\n===== internal/server/ladder_write.go =====\n'
cat -n internal/server/ladder_write.go | sed -n '1,220p'

printf '\n===== relevant env handling in providers/aws/provider.go =====\n'
cat -n providers/aws/provider.go | sed -n '150,240p'

Repository: LeanerCloud/CUDly

Length of output: 17877


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '===== parallel tests that also use Setenv =====\n'
python3 - <<'PY'
from pathlib import Path
import re
for path in Path('.').rglob('*.go'):
    text = path.read_text(errors='ignore')
    if 't.Parallel()' in text and 't.Setenv(' in text:
        print(path)
PY

printf '\n===== ladder-related tests using Setenv =====\n'
rg -n "t\.Setenv\(|t\.Parallel\(\)" internal/server internal/api providers/aws/ladder -g '*_test.go'

Repository: LeanerCloud/CUDly

Length of output: 10411


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cat -n internal/server/handler_test.go | sed -n '88,126p'
printf '\n---\n'
cat -n providers/aws/provider_test.go | sed -n '226,246p'
printf '\n---\n'
cat -n internal/server/ladder_write.go | sed -n '105,142p'

Repository: LeanerCloud/CUDly

Length of output: 4643


Isolate this AWS regression test from ambient credentials (internal/server/ladder_write_test.go:34-51).
This uses a real AWSLadder; if the disabled-write path regresses, the test can inherit live AWS credentials and make an unintended call. Clear the AWS env/IMDS before construction, and drop t.Parallel if you switch to t.Setenv.

🤖 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 `@internal/server/ladder_write_test.go` around lines 34 - 51, Update
TestWireLadderWriteSide_ExecutionDisabled_RealAWSLadder_RefusesWithoutAWSCall to
clear ambient AWS credentials and disable EC2 instance metadata before calling
awsladder.NewFromAWSConfig, using t.Setenv for the relevant AWS environment
variables. Remove t.Parallel because t.Setenv cannot be used safely with
parallel tests, while preserving the existing offline construction and
disabled-write assertions.

@cristim
cristim merged commit 73166c2 into main Jul 16, 2026
20 checks passed
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Merged to main after adversarial money-path review (verdict SHIP) + all CI green (rebased onto current main, migration 000083 collision-free, gates build/vet/gocyclo/lint-new/server/integration all exit 0). Adds the ladder_execution_enabled kill-switch (default FALSE, fail-closed): dual-flag gate (LadderingEnabled controls whether a run happens; LadderExecutionEnabled controls whether purchases/exchanges fire), disabled shims return ErrLadderExecutionDisabled so no spend is possible when off, and WireWriteSide reuses the production ec2/savingsplans purchasers with ClientToken/EC2-tag idempotency untouched. Kill-switch spend-prevention test proves both PurchaseLayer and ReshapeBuffer no-op when disabled.

@cristim
cristim deleted the feat/ladder-write-wiring branch July 16, 2026 19:34
cristim added a commit that referenced this pull request Jul 16, 2026
Main now tops at migration 000083 (ladder_execution_enabled, PR #1396).
golang-migrate applies only versions strictly greater than the current
schema version, so a 000082 migration would be silently skipped on any
DB already advanced to 000083 (prod / staging): the cancelled->canceled
rename would never run.

Renumber to 000084, the next free slot above main's max (PR #808 is
taking 000085, so use 000084 exactly). Renames the up/down SQL and the
integration test file, bumps the pinned version constants
(renameCanceledVersion 82->84, renameCanceledPrevVersion 81->83, the
version immediately below now that 000083 sits directly beneath), and
updates every 000082 reference in comments, OpenAPI, the frontend
history/riexchange notes, and the .golangci.yml misspell exemption path.

Collision-checked: no 000084 exists on origin/main. Verified on a fresh
testcontainer PG that the full chain migrates cleanly through 000084
(Database migrations completed successfully (version: 84)) and the
expand/rollback integration tests pass.
cristim added a commit that referenced this pull request Jul 16, 2026
Main now tops at migration 000083 (ladder_execution_enabled, PR #1396).
golang-migrate applies only versions strictly greater than the current
schema version, so a 000082 migration would be silently skipped on any
DB already advanced to 000083 (prod / staging): the cancelled->canceled
rename would never run.

Renumber to 000084, the next free slot above main's max (PR #808 is
taking 000085, so use 000084 exactly). Renames the up/down SQL and the
integration test file, bumps the pinned version constants
(renameCanceledVersion 82->84, renameCanceledPrevVersion 81->83, the
version immediately below now that 000083 sits directly beneath), and
updates every 000082 reference in comments, OpenAPI, the frontend
history/riexchange notes, and the .golangci.yml misspell exemption path.

Collision-checked: no 000084 exists on origin/main. Verified on a fresh
testcontainer PG that the full chain migrates cleanly through 000084
(Database migrations completed successfully (version: 84)) and the
expand/rollback integration tests pass.
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