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
13 changes: 11 additions & 2 deletions providers/aws/recommendations/parser_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,24 @@ func (c *Client) parseElastiCacheDetails(_ context.Context, rec *common.Recommen
// directly. A nil pointer (CE omitted the field) also maps to default, because
// CE only populates the field when it is non-default.
//
// Matching is case- and whitespace-insensitive: Cost Explorer returns the
// value title-cased ("Shared", "Dedicated", "Host") even though older docs
// imply lowercase, so a case-sensitive compare silently rejected every real
// EC2 RI recommendation and the caller dropped it (no AWS opportunities on the
// dashboard). Normalising the input keeps the parser robust to AWS varying the
// casing without re-introducing a silent fallback.
//
// Unknown tenancy values (e.g. "host" for Dedicated Hosts, which have no
// corresponding RI product) return an error so the caller fails loud rather
// than silently querying for and buying a default-tenancy RI on behalf of a
// workload that requires a different tenancy class (M5 fix).
func resolveEC2Tenancy(tenancy *string) (string, error) {
if tenancy == nil || *tenancy == "shared" {
if tenancy == nil {
return string(ec2types.TenancyDefault), nil
}
switch *tenancy {
switch strings.ToLower(strings.TrimSpace(*tenancy)) {
case "shared":
return string(ec2types.TenancyDefault), nil
case "dedicated":
return string(ec2types.TenancyDedicated), nil
default:
Expand Down
82 changes: 82 additions & 0 deletions providers/aws/recommendations/parser_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,88 @@ func TestParseEC2Details(t *testing.T) {
assert.Equal(t, "default", ec2Details.Tenancy)
},
},
{
// Regression for "AWS opportunities missing": Cost Explorer
// returns Tenancy title-cased as "Shared" (not lowercase
// "shared"). The pre-fix case-sensitive compare rejected it,
// parseRecommendations swallowed the error and continue'd, and
// every EC2 RI recommendation was dropped -> no AWS recs on the
// dashboard. This is the exact casing observed in production logs.
name: "EC2 CE title-cased Shared tenancy maps to default",
details: &types.ReservationPurchaseRecommendationDetail{
InstanceDetails: &types.InstanceDetails{
EC2InstanceDetails: &types.EC2InstanceDetails{
InstanceType: aws.String("m5.large"),
Platform: aws.String("Linux/UNIX"),
Region: aws.String("US East (N. Virginia)"),
Tenancy: aws.String("Shared"),
},
},
},
expectError: false,
validate: func(t *testing.T, rec *common.Recommendation) {
ec2Details, ok := rec.Details.(*common.ComputeDetails)
require.True(t, ok)
assert.Equal(t, "default", ec2Details.Tenancy)
},
},
{
// Same fix for the dedicated case: CE title-cases "Dedicated".
name: "EC2 CE title-cased Dedicated tenancy maps to dedicated",
details: &types.ReservationPurchaseRecommendationDetail{
InstanceDetails: &types.InstanceDetails{
EC2InstanceDetails: &types.EC2InstanceDetails{
InstanceType: aws.String("r5.xlarge"),
Platform: aws.String("Windows"),
Region: aws.String("eu-central-1"),
Tenancy: aws.String("Dedicated"),
},
},
},
expectError: false,
validate: func(t *testing.T, rec *common.Recommendation) {
ec2Details, ok := rec.Details.(*common.ComputeDetails)
require.True(t, ok)
assert.Equal(t, "dedicated", ec2Details.Tenancy)
},
},
{
// Leading/trailing whitespace must not defeat the mapping either.
name: "EC2 tenancy with surrounding whitespace maps to default",
details: &types.ReservationPurchaseRecommendationDetail{
InstanceDetails: &types.InstanceDetails{
EC2InstanceDetails: &types.EC2InstanceDetails{
InstanceType: aws.String("m5.large"),
Platform: aws.String("Linux/UNIX"),
Region: aws.String("us-east-1"),
Tenancy: aws.String(" Shared "),
},
},
},
expectError: false,
validate: func(t *testing.T, rec *common.Recommendation) {
ec2Details, ok := rec.Details.(*common.ComputeDetails)
require.True(t, ok)
assert.Equal(t, "default", ec2Details.Tenancy)
},
},
{
// M5 must still hold under the case-insensitive compare: title-cased
// "Host" (Dedicated Hosts, no RI product) must still error rather
// than collapse to default.
name: "EC2 title-cased Host tenancy still errors",
details: &types.ReservationPurchaseRecommendationDetail{
InstanceDetails: &types.InstanceDetails{
EC2InstanceDetails: &types.EC2InstanceDetails{
InstanceType: aws.String("m5.large"),
Platform: aws.String("Linux/UNIX"),
Region: aws.String("us-east-1"),
Tenancy: aws.String("Host"),
},
},
},
expectError: true,
},
{
name: "Missing EC2 instance details",
details: &types.ReservationPurchaseRecommendationDetail{
Expand Down
Loading