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
9 changes: 9 additions & 0 deletions providers/aws/recommendations/parser_sp.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -192,6 +193,11 @@ func (c *Client) parseSavingsPlansRecommendations(
// the same class rather than silently substituting 0. Callers must propagate
// this error and drop the recommendation so a corrupt money figure never
// reaches the scheduler or frontend.
// - non-nil pointer that parses to NaN or ±Inf: returns (0, error). Go's
// strconv.ParseFloat accepts "NaN", "Inf", "+Inf", "-Inf" (and Infinity
// variants) as valid with a nil error, so without this guard a corrupt
// non-finite money value would flow straight through to the scheduler and
// frontend. Reject non-finite values the same way as unparseable ones.
func parseOptionalFloat(field string, s *string) (float64, error) {
if s == nil {
return 0, nil
Expand All @@ -200,6 +206,9 @@ func parseOptionalFloat(field string, s *string) (float64, error) {
if err != nil {
return 0, fmt.Errorf("failed to parse %s %q: %w", field, *s, err)
}
if math.IsNaN(val) || math.IsInf(val, 0) {
return 0, fmt.Errorf("%s %q is not a finite number", field, *s)
}
return val, nil
}

Expand Down
44 changes: 42 additions & 2 deletions providers/aws/recommendations/parser_sp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,55 @@ func TestParseSavingsPlanDetail_MoneyFieldUnparseable(t *testing.T) {
UpfrontCost: aws.String("not-a-float"),
},
},
// NaN / ±Inf are accepted by strconv.ParseFloat with a nil error, so
// without an explicit non-finite guard these would flow through as a
// corrupt money value. They must be rejected like any unparseable field.
{
name: "NaN HourlyCommitmentToPurchase",
detail: &types.SavingsPlansPurchaseRecommendationDetail{
HourlyCommitmentToPurchase: aws.String("NaN"),
},
},
{
name: "Inf EstimatedMonthlySavingsAmount",
detail: &types.SavingsPlansPurchaseRecommendationDetail{
HourlyCommitmentToPurchase: aws.String("1.0"),
EstimatedMonthlySavingsAmount: aws.String("+Inf"),
},
},
{
name: "negative Inf UpfrontCost",
detail: &types.SavingsPlansPurchaseRecommendationDetail{
HourlyCommitmentToPurchase: aws.String("1.0"),
UpfrontCost: aws.String("-Inf"),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec, err := client.parseSavingsPlanDetail(tt.detail, &params, types.SupportedSavingsPlansTypeComputeSp)
require.Error(t, err,
"present-but-unparseable money field must return an error, not a silently-fabricated $0")
"present-but-unparseable or non-finite money field must return an error, not a silently-fabricated value")
assert.Nil(t, rec,
"rec must be nil when a money field is unparseable")
"rec must be nil when a money field is unparseable or non-finite")
})
}
}

// TestParseOptionalFloat_RejectsNonFinite is the unit-level guard for the
// NaN/±Inf money-parsing fix: strconv.ParseFloat accepts these strings with a
// nil error, so parseOptionalFloat must reject them explicitly.
func TestParseOptionalFloat_RejectsNonFinite(t *testing.T) {
for _, s := range []string{"NaN", "Inf", "+Inf", "-Inf", "Infinity", "-Infinity"} {
t.Run(s, func(t *testing.T) {
v, err := parseOptionalFloat("TestField", aws.String(s))
require.Error(t, err, "non-finite value %q must be rejected", s)
assert.Zero(t, v)
})
}
// Sanity: a normal finite value still parses.
v, err := parseOptionalFloat("TestField", aws.String("12.5"))
require.NoError(t, err)
assert.InDelta(t, 12.5, v, 0.0001)
}
Loading