Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ type GlobalConfig struct {
// Set to true to allow per-account configs to activate individually.
LadderingEnabled bool `json:"laddering_enabled" db:"laddering_enabled"`

// LadderExecutionEnabled gates the write side of the ladder capability
// (migration 000083). BOTH LadderingEnabled AND LadderExecutionEnabled
// must be true for PurchaseLayer / ReshapeBuffer to be wired with real
// AWS SDK clients. Default false: existing deployments that enable
// laddering produce plans but never call AWS purchase APIs until an
// operator explicitly opts in. Fail-loud: wireLadderWriteSide returns
// a typed ErrLadderExecutionDisabled when this is false.
// LadderExecutionEnabled gates ladder purchases and reshapes (migration
// 000083). BOTH LadderingEnabled AND LadderExecutionEnabled must be true
// for a sweep that buys or reshapes to act. Planning (ladder_run) never
// reads it: its capability always has the disabled write side, which
// returns a typed ErrLadderExecutionDisabled. Default false: deployments
// that enable laddering produce plans but never call AWS purchase APIs
// until an operator explicitly opts in.
LadderExecutionEnabled bool `json:"ladder_execution_enabled" db:"ladder_execution_enabled"`

// AutoPurchaseEnabled is the global kill switch for AutoPurchase plans
Expand Down
10 changes: 4 additions & 6 deletions internal/server/handler_ladder.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (app *Application) handleLadderRun(ctx context.Context) (*LadderRunResult,
}

now := time.Now().UTC()
result := app.runLadderConfigs(ctx, allConfigs, ownAccountID, region, term, paymentOpt, now, globalCfg.LadderExecutionEnabled)
result := app.runLadderConfigs(ctx, allConfigs, ownAccountID, region, term, paymentOpt, now)

log.Printf("ladder_run done: planned=%d skipped_cadence=%d skipped_disabled=%d skipped_multi_account=%d errored=%d",
result.Planned, result.SkippedCadence, result.SkippedDisabled, result.SkippedMultiAccount, result.Errored)
Expand Down Expand Up @@ -198,11 +198,10 @@ func (app *Application) runLadderConfigs(
term pkgladder.Term,
paymentOpt pkgladder.PaymentOption,
now time.Time,
executionEnabled bool,
) *LadderRunResult {
result := &LadderRunResult{}
for i := range configs {
result.record(app.processOneLadderConfig(ctx, &configs[i], ownAccountID, region, term, paymentOpt, now, executionEnabled))
result.record(app.processOneLadderConfig(ctx, &configs[i], ownAccountID, region, term, paymentOpt, now))
}
return result
}
Expand All @@ -218,7 +217,6 @@ func (app *Application) processOneLadderConfig(
term pkgladder.Term,
paymentOpt pkgladder.PaymentOption,
now time.Time,
executionEnabled bool,
) ladderConfigOutcome {
if !dbCfg.Enabled {
log.Printf("ladder_run: config %s: enabled=false, skipping", dbCfg.ID)
Expand Down Expand Up @@ -256,8 +254,8 @@ func (app *Application) processOneLadderConfig(
return outcomeSkippedCadence
}

// Build and wire the LadderCapability for this account.
capability, err := app.buildAndWireCapability(ctx, region, cloudAcct.ExternalID, executionEnabled)
// Build the planning capability for this account: its write side is always disabled.
capability, err := app.planningCapability(ctx, region, cloudAcct.ExternalID)
if err != nil {
log.Printf("ladder_run: config %s: %v", dbCfg.ID, err)
return outcomeErrored
Expand Down
2 changes: 1 addition & 1 deletion internal/server/handler_ladder_inflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func TestRunLadderConfigs_InFlightFetchError_CountsErrored(t *testing.T) {
},
}

result := app.runLadderConfigs(ctx, []config.LadderConfigDB{dbCfg}, inFlightTestAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now, false)
result := app.runLadderConfigs(ctx, []config.LadderConfigDB{dbCfg}, inFlightTestAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now)

assert.Equal(t, LadderRunResult{Errored: 1}, *result, "an in-flight fetch error must count the config Errored and nothing else")
assertNothingPersistedOrSubmitted(t, store, fakeCap)
Expand Down
6 changes: 3 additions & 3 deletions internal/server/handler_ladder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func TestHandleLadderRun_MultiAccountSkip_CountedAndIsolated(t *testing.T) {

// Put the foreign config first to prove isolation is order-independent.
configs := []config.LadderConfigDB{cfgForeign, cfgHealthy}
result := app.runLadderConfigs(ctx, configs, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now, false)
result := app.runLadderConfigs(ctx, configs, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now)

require.NotNil(t, result)
// (a) The foreign config must be counted as SkippedMultiAccount.
Expand Down Expand Up @@ -981,7 +981,7 @@ func TestProcessOneLadderConfig_CadenceDBError_Errored(t *testing.T) {
},
}

result := app.runLadderConfigs(ctx, []config.LadderConfigDB{dbCfg}, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now, false)
result := app.runLadderConfigs(ctx, []config.LadderConfigDB{dbCfg}, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now)

assert.Equal(t, 1, result.Errored, "a cadence lookup error must count the config Errored")
assert.Equal(t, 0, result.Planned)
Expand Down Expand Up @@ -1105,7 +1105,7 @@ func TestHandleLadderRun_MultiConfigIsolation(t *testing.T) {
// Order the broken config first to prove a leading failure does not abort
// the healthy config that follows.
configs := []config.LadderConfigDB{cfgBroken, cfgHealthy}
result := app.runLadderConfigs(ctx, configs, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now, false)
result := app.runLadderConfigs(ctx, configs, ownAccount, "us-east-1", pkgladder.Term1Year, pkgladder.PaymentNoUpfront, now)

require.NotNil(t, result)
assert.Equal(t, 1, result.Planned, "the healthy config must still be planned despite the broken one")
Expand Down
2 changes: 1 addition & 1 deletion internal/server/ladder_compute_cap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func runComputeCapConfig(t *testing.T, cap *float64) (*LadderRunResult, *persist
dbCfg := validTestDBConfig("cfg-compute-cap")
dbCfg.MaxHourlyCommitPerRun = cap
result := app.runLadderConfigs(testutil.TestContext(t), []config.LadderConfigDB{dbCfg}, computeCapAccount, "us-east-1",
pkgladder.Term1Year, pkgladder.PaymentNoUpfront, ratesTestNow, false)
pkgladder.Term1Year, pkgladder.PaymentNoUpfront, ratesTestNow)
return result, store, reads
}

Expand Down
14 changes: 7 additions & 7 deletions internal/server/ladder_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,20 @@ func (a *exchangeRunnerAdapter) run(ctx context.Context, cfg exchange.RIExchange
})
}

// buildAndWireCapability constructs a LadderCapability via the factory and wires its
// write side. Extracted from processOneLadderConfig to keep that function's cyclomatic
// complexity below the project threshold (10). The returned error carries no config
// ID: the sole caller already prefixes its log line with the config ID, so repeating
// it here would duplicate it in the output.
func (app *Application) buildAndWireCapability(ctx context.Context, region, accountID string, executionEnabled bool) (pkgladder.LadderCapability, error) {
// planningCapability builds the capability ladder_run plans with. Planning never
// buys, so its write side is always the disabled one, whatever
// ladder_execution_enabled says; it takes no flag, so the planner cannot ask for
// real clients (#70). The returned error carries no config ID: the caller
// already prefixes its log line with it.
func (app *Application) planningCapability(ctx context.Context, region, accountID string) (pkgladder.LadderCapability, error) {
if app.LadderCapabilityFactory == nil {
return nil, errors.New("LadderCapabilityFactory is nil (not wired)")
}
capability, err := app.LadderCapabilityFactory(ctx, region, accountID)
if err != nil {
return nil, fmt.Errorf("failed to build ladder capability: %w", err)
}
return app.wireLadderWriteSide(ctx, executionEnabled, region, accountID, capability)
return app.wireLadderWriteSide(ctx, false, region, accountID, capability)
}

// wireLadderWriteSide wires the write side of a LadderCapability if and only if
Expand Down
40 changes: 40 additions & 0 deletions internal/server/ladder_write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,43 @@ func TestWireLadderWriteSide_ExecutionDisabled_RealAWSLadder_RefusesWithoutAWSCa
assert.ErrorIs(t, reshapeErr, awsladder.ErrLadderExecutionDisabled,
"kill-switch OFF must refuse ReshapeBuffer with ErrLadderExecutionDisabled, not an AWS error")
}

// TestPlanningCapability_RealAWSLadder_RefusesPurchases pins that ladder_run plans
// with a capability whose write side is always the disabled one. Planning must
// never hold a purchase-capable capability, whatever ladder_execution_enabled
// says; only the reshape sweep may wire real clients (#70 S3).
func TestPlanningCapability_RealAWSLadder_RefusesPurchases(t *testing.T) {
// No credentials can be found, so a regression that wired real clients
// fails on a credentials error instead of buying with the caller's account.
// t.Setenv rules out t.Parallel.
for k, v := range map[string]string{
"AWS_CONFIG_FILE": "/dev/null", "AWS_SHARED_CREDENTIALS_FILE": "/dev/null",
"AWS_PROFILE": "", "AWS_ACCESS_KEY_ID": "", "AWS_SECRET_ACCESS_KEY": "",
"AWS_SESSION_TOKEN": "", "AWS_EC2_METADATA_DISABLED": "true",
"AWS_WEB_IDENTITY_TOKEN_FILE": "", "AWS_ROLE_ARN": "",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "", "AWS_CONTAINER_CREDENTIALS_FULL_URI": "",
"AWS_CONTAINER_AUTHORIZATION_TOKEN": "",
} {
t.Setenv(k, v)
}
ctx := context.Background()
const (
region = "us-east-1"
accountID = "123456789012"
)
app := &Application{LadderCapabilityFactory: func(ctx context.Context, region, accountID string) (pkgladder.LadderCapability, error) {
return awsladder.NewFromAWSConfig(ctx, region, accountID)
}}
capability, err := app.planningCapability(ctx, region, accountID)
require.NoError(t, err)

rec := pkgcommon.Recommendation{
ResourceType: "m5.large",
Count: 1,
Term: "1yr",
PaymentOption: "no-upfront",
Details: &pkgcommon.ComputeDetails{InstanceType: "m5.large", Platform: "linux", Tenancy: "default", Scope: "regional"},
}
_, purchaseErr := capability.PurchaseLayer(ctx, pkgladder.LayerConvertibleRI, rec, pkgcommon.PurchaseOptions{Source: pkgcommon.PurchaseSourceWeb, IdempotencyToken: "ladder-plan-tok"})
assert.ErrorIs(t, purchaseErr, awsladder.ErrLadderExecutionDisabled, "the planning capability must refuse every purchase")
}
Loading