Skip to content

Commit 700ff43

Browse files
cristimclaude
andauthored
fix(cli): bound --min-savings-pct to 0-100 (#2231)
--min-savings-pct was only checked for being finite. A negative value behaved as no filter and a value above 100 dropped every recommendation, since savings percentages are measured against on-demand and cannot exceed 100. Reject both at validation, document the range, and rename the flag-parsing validation test to cover out-of-range as well as non-finite values. Closes #2161 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e0a1db6 commit 700ff43

4 files changed

Lines changed: 20 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
1313

1414
### Fixed
1515

16+
- `--min-savings-pct` now rejects values below 0 or above 100 at validation.
17+
Previously a negative value was accepted as no filter and a value above 100
18+
silently dropped every recommendation (#2161).
1619
- `configure-gcp`: skipping (or giving an unknown answer at) the "Create Service
1720
Account" step no longer continues with the default
1821
`cudly-service-account@<project>` email that was never created. The wizard now

‎cmd/validators.go‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ func requireFinite(name string, v float64) error {
145145
return nil
146146
}
147147

148-
// validateCoverageAndSavingsPct checks --coverage (finite, 0-100) and that
149-
// --min-savings-pct is finite. target-coverage and min-pool-size call
148+
// validateCoverageAndSavingsPct checks that --coverage and --min-savings-pct
149+
// are finite and within 0-100. target-coverage and min-pool-size call
150150
// requireFinite in their own validators.
151151
func validateCoverageAndSavingsPct() error {
152152
if err := requireFinite("coverage", toolCfg.Coverage); err != nil {
@@ -155,7 +155,13 @@ func validateCoverageAndSavingsPct() error {
155155
if toolCfg.Coverage < 0 || toolCfg.Coverage > 100 {
156156
return fmt.Errorf("coverage percentage must be between 0 and 100, got: %.2f", toolCfg.Coverage)
157157
}
158-
return requireFinite("min-savings-pct", toolCfg.MinSavingsPct)
158+
if err := requireFinite("min-savings-pct", toolCfg.MinSavingsPct); err != nil {
159+
return err
160+
}
161+
if toolCfg.MinSavingsPct < 0 || toolCfg.MinSavingsPct > 100 {
162+
return fmt.Errorf("min-savings-pct must be between 0 and 100, got: %.2f", toolCfg.MinSavingsPct)
163+
}
164+
return nil
159165
}
160166

161167
// validateMinPoolSize checks that --min-pool-size is a non-negative whole number.

‎cmd/validators_test.go‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -815,10 +815,11 @@ func TestCheckDuplicates_HonorsIdempotencyWindow(t *testing.T) {
815815
}
816816
}
817817

818-
// TestValidateNumericFlagsRejectNonFinite drives the real flag set through
818+
// TestValidateNumericFlagsRejectInvalid drives the real flag set through
819819
// rootCmd.ParseFlags (cobra/pflag parse "NaN" and "Inf" as floats) and then
820-
// the validators. Nothing here calls runTool, so no AWS client is built.
821-
func TestValidateNumericFlagsRejectNonFinite(t *testing.T) {
820+
// the validators, covering non-finite and out-of-range values. Nothing here
821+
// calls runTool, so no AWS client is built.
822+
func TestValidateNumericFlagsRejectInvalid(t *testing.T) {
822823
saved := toolCfg
823824
reset := func() {
824825
toolCfg = Config{}
@@ -848,6 +849,9 @@ func TestValidateNumericFlagsRejectNonFinite(t *testing.T) {
848849
{name: "min-pool-size NaN", args: []string{"--min-pool-size", "NaN"}, wantErr: "min-pool-size must be a finite number, got: NaN"},
849850
{name: "min-savings-pct NaN", args: []string{"--min-savings-pct", "NaN"}, wantErr: "min-savings-pct must be a finite number, got: NaN"},
850851
{name: "min-savings-pct Inf", args: []string{"--min-savings-pct", "Inf"}, wantErr: "min-savings-pct must be a finite number, got: +Inf"},
852+
{name: "min-savings-pct negative", args: []string{"--min-savings-pct", "-1"}, wantErr: "min-savings-pct must be between 0 and 100, got: -1.00"},
853+
{name: "min-savings-pct above 100", args: []string{"--min-savings-pct", "100.5"}, wantErr: "min-savings-pct must be between 0 and 100, got: 100.50"},
854+
{name: "min-savings-pct bounds stay valid", args: []string{"--min-savings-pct", "100"}},
851855
{name: "overflow rejected by the flag parser", args: []string{"--target-coverage", "1e999"}, parseErr: true},
852856
{name: "explicit target 0 stays valid", args: []string{"--target-coverage", "0"}},
853857
{name: "valid values pass", args: []string{"--target-coverage", "95", "--coverage", "50", "--min-pool-size", "2", "--min-savings-pct", "5"}},

‎docs/cli/filtering.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ cudly --services rds --min-count 3
149149
--min-savings-pct <float> Minimum savings percentage (0 = no filter)
150150
```
151151

152-
Drop recommendations whose estimated savings percentage falls below this threshold.
152+
Drop recommendations whose estimated savings percentage falls below this threshold. The value must be between 0 and 100.
153153

154154
**Important naming distinction:** `--min-savings-pct` is a **percentage** value (e.g. `10` means "at least 10% savings"). This is different from the GUI and API `min_savings` parameter, which filters by **dollar amount**. A value of `30` in `--min-savings-pct` means "30% savings", but `min_savings=30` in the API means "$30 in savings". Mixing up the two by copying a CLI value into the GUI filter (or vice versa) produces silent, incorrect filtering.
155155

0 commit comments

Comments
 (0)