Skip to content

fix(cli): --override-count and --max-instances mutate Count without scaling costs, misstating the spend #1611

Description

@cristim

Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.

ApplyCountOverride and ApplyInstanceLimit assign .Count directly and leave the cost fields describing the old quantity. The confirmation prompt, the CSV report and the audit record all then state the wrong money, on the exact screen the operator uses to approve the spend.

Where

  • cmd/helpers.go:491-502 - ApplyCountOverride
  • cmd/helpers.go:505-526 - ApplyInstanceLimit
  • The correct idiom, in the same file: cmd/helpers.go:173-178 (applyCoverage, routed through common.ScaleRecommendationCosts, with a comment explaining why desynchronising count and cost is a bug) and cmd/helpers.go:396-403 (applyTargetCoverageRI)
  • Consumers of the stale fields: ConfirmPurchase's savings figure (cmd/multi_service.go:157 -> sumPassedRecs over EstimatedSavings), the UpfrontPayment / EstimatedSavings CSV columns (cmd/multi_service_csv.go:272-274), the TOTAL row (cmd/multi_service_csv.go:307-321), and the audit record's EstimatedCost (pkg/common/audit.go:60-61)

What

After either helper runs, CommitmentCost, OnDemandCost, EstimatedSavings and RecurringMonthlyCost still describe the pre-override or pre-truncation quantity. Every other sizing path in the same file routes through common.ScaleRecommendationCosts precisely to keep the two in sync, and applyCoverage carries a comment saying why.

Failure scenario (override)

A recommendation with Count = 1, CommitmentCost = $4,000. The operator runs:

cudly ... --override-count 10 --purchase

ApplyCountOverride sets Count = 10 and leaves CommitmentCost = $4,000. The confirmation prompt and the CSV both report about $4,000 upfront while AWS is charged about $40,000. That is a 10x understatement on the approval screen.

Failure scenario (limit)

A recommendation with Count = 20, CommitmentCost = $100,000, run with --max-instances 5. Count becomes 5, the cost stays $100,000, so the CSV and the audit record overstate the purchase by 4x. The error runs in both directions depending on which helper fired, so an operator cannot even learn a consistent correction factor.

Fix direction

Route both helpers through common.ScaleRecommendationCosts(rec, newCount/oldCount) the way applyCoverage does, guarding oldCount == 0.

The regression test should assert the cost fields, not just Count. A test that only checks Count after the override passes today and would keep passing with the bug present.

Related

  • --max-instances is separately applied per (service, region) rather than as the documented global cap, filed as its own issue from this review. The two interact: the per-region application means ApplyInstanceLimit runs many times per run, so the cost desync fires once per region as well.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions