Skip to content
52 changes: 34 additions & 18 deletions providers/gcp/services/cloudsql/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,13 @@ func skuMatchesTier(sku *cloudbilling.Sku, tier, region string) bool {
return true
}

// extractResourceTypeFromContent extracts the last path segment of the first
// non-empty Operation.Resource across all operation groups. Used by all four
// GCP service converters to set rec.ResourceType from Recommender payloads.
func extractResourceTypeFromContent(content *recommenderpb.RecommendationContent) string {
if content == nil || content.OperationGroups == nil {
// extractGCPResourceType returns the last path segment of the first non-empty
// resource field found across all operation groups, or "" if none is present.
func extractGCPResourceType(rec *recommenderpb.Recommendation) string {
if rec.Content == nil || rec.Content.OperationGroups == nil {
return ""
}
for _, opGroup := range content.OperationGroups {
for _, opGroup := range rec.Content.OperationGroups {
for _, op := range opGroup.Operations {
if op.Resource == "" {
continue
Expand All @@ -476,13 +475,13 @@ func extractResourceTypeFromContent(content *recommenderpb.RecommendationContent
return ""
}

// extractEstimatedSavings returns the negative of the PrimaryImpact cost
// projection (GCP encodes savings as a negative cost delta).
func extractEstimatedSavings(gcpRec *recommenderpb.Recommendation) float64 {
if gcpRec.PrimaryImpact == nil {
// extractGCPSavings returns the estimated monthly savings (positive value)
// from the primary cost impact of a GCP recommendation, or 0 if absent.
func extractGCPSavings(rec *recommenderpb.Recommendation) float64 {
if rec.PrimaryImpact == nil {
return 0
}
costProj := gcpRec.PrimaryImpact.GetCostProjection()
costProj := rec.PrimaryImpact.GetCostProjection()
if costProj == nil || costProj.Cost == nil {
return 0
}
Expand Down Expand Up @@ -510,6 +509,15 @@ func (c *CloudSQLClient) fillSQLPricing(ctx context.Context, rec *common.Recomme
}
}

// termYearsFromLabel converts a term string such as "1yr" or "3yr" to an
// integer number of years (defaults to 1 for any unrecognized value).
func termYearsFromLabel(term string) int {
if term == "3yr" || term == "3" {
return 3
}
return 1
}

// convertGCPRecommendation converts a GCP Recommender recommendation to common format.
// It also calls getSQLPricing to fill CommitmentCost/OnDemandCost/SavingsPercentage/
// BreakEvenMonths so the scorer can filter and rank GCP recommendations correctly
Expand All @@ -536,17 +544,25 @@ func (c *CloudSQLClient) convertGCPRecommendation(ctx context.Context, gcpRec *r
PaymentOption: paymentOption,
}

rec.ResourceType = extractResourceTypeFromContent(gcpRec.Content)
rec.EstimatedSavings = extractEstimatedSavings(gcpRec)
rec.ResourceType = extractGCPResourceType(gcpRec)
rec.EstimatedSavings = extractGCPSavings(gcpRec)

// Thread pricing into the converter so the scorer can rank/filter GCP recs
// correctly (issue #1022 C2).
// correctly (issue #1022 C2). fillSQLPricing performs the single billing
// lookup and populates CommitmentCost; we reuse that value below to derive
// RecurringMonthlyCost rather than issuing a second SKU call.
if rec.ResourceType != "" {
termYears := 1
if rec.Term == "3yr" || rec.Term == "3" {
termYears = 3
}
termYears := termYearsFromLabel(rec.Term)
c.fillSQLPricing(ctx, rec, termYears)

// Cloud SQL CUDs are monthly-payment commitments, so the per-month
// charge is CommitmentCost / termMonths. When the billing lookup
// failed, CommitmentCost stays 0 and RecurringMonthlyCost remains nil
// so the frontend renders "—" rather than a stale value.
if rec.CommitmentCost > 0 {
monthly := rec.CommitmentCost / float64(termYears*12)
rec.RecurringMonthlyCost = &monthly
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return rec
Expand Down
116 changes: 116 additions & 0 deletions providers/gcp/services/cloudsql/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,3 +841,119 @@ func TestGetSQLPricing_CommitmentPriceIsTermTotal(t *testing.T) {
assert.Less(t, pricing.SavingsPercentage, float64(50),
"SavingsPercentage must be realistic (not ~100%)")
}

func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost(t *testing.T) {
ctx := context.Background()
client, _ := NewClient(ctx, "test-project", "us-central1")

// Inject a billing mock with a known on-demand price and a separate
// commitment SKU. getSQLPricing derives CommitmentPrice from the
// "commitment" SKU (CommitmentPrice = commitmentHourly * 8760), so
// RecurringMonthlyCost must equal CommitmentPrice / 12 (one year = 12 months).
const onDemandHourly = 0.12 // USD/h per vCPU -- representative db-n1-standard-1 value
const commitmentHourly = 0.102 // USD/h -- 1yr CUD rate from the catalog
mockBilling := &MockBillingService{
skus: &cloudbilling.ListSkusResponse{
Skus: []*cloudbilling.Sku{
{
Description: "db-n1-standard-1 Cloud SQL",
ServiceRegions: []string{"us-central1"},
PricingInfo: []*cloudbilling.PricingInfo{
{
PricingExpression: &cloudbilling.PricingExpression{
TieredRates: []*cloudbilling.TierRate{
{
UnitPrice: &cloudbilling.Money{
Units: 0,
Nanos: int64(onDemandHourly * 1e9),
CurrencyCode: "USD",
},
},
},
},
},
},
},
{
// Commitment SKU required by getSQLPricing: without a
// "commitment" SKU it errors and RecurringMonthlyCost stays nil.
Description: "db-n1-standard-1 Cloud SQL commitment 1yr",
ServiceRegions: []string{"us-central1"},
PricingInfo: []*cloudbilling.PricingInfo{
{
PricingExpression: &cloudbilling.PricingExpression{
TieredRates: []*cloudbilling.TierRate{
{
UnitPrice: &cloudbilling.Money{
Units: 0,
Nanos: int64(commitmentHourly * 1e9),
CurrencyCode: "USD",
},
},
},
},
},
},
},
},
},
}
client.SetBillingService(mockBilling)

gcpRec := &recommenderpb.Recommendation{
Name: "test-rec",
PrimaryImpact: &recommenderpb.Impact{
Category: recommenderpb.Impact_COST,
Projection: &recommenderpb.Impact_CostProjection{
CostProjection: &recommenderpb.CostProjection{
Cost: &money.Money{Units: -100, CurrencyCode: "USD"},
},
},
},
Content: &recommenderpb.RecommendationContent{
OperationGroups: []*recommenderpb.OperationGroup{
{
Operations: []*recommenderpb.Operation{
{Resource: "projects/test/instances/db-n1-standard-1"},
},
},
},
},
}

rec := client.convertGCPRecommendation(ctx, gcpRec, common.RecommendationParams{})
require.NotNil(t, rec)

// RecurringMonthlyCost must be a non-nil pointer to a positive value for
// a monthly Cloud SQL CUD recommendation.
require.NotNil(t, rec.RecurringMonthlyCost, "RecurringMonthlyCost must be non-nil when billing lookup succeeds")
assert.Greater(t, *rec.RecurringMonthlyCost, 0.0, "RecurringMonthlyCost must be positive for a monthly Cloud SQL CUD")

// Verify the value matches CommitmentPrice / 12 exactly. CommitmentPrice is
// the commitment SKU's hourly rate scaled to the 1yr term total.
const hoursIn1yr = 8760.0
expectedMonthly := commitmentHourly * hoursIn1yr / 12
assert.InDelta(t, expectedMonthly, *rec.RecurringMonthlyCost, 1e-6)
}

func TestCloudSQLClient_ConvertGCPRecommendation_RecurringMonthlyCost_BillingFailure(t *testing.T) {
ctx := context.Background()
client, _ := NewClient(ctx, "test-project", "us-central1")

// Inject a billing mock that always errors; RecurringMonthlyCost must
// remain nil (frontend renders "—") rather than a zero or stale value.
client.SetBillingService(&MockBillingService{err: errors.New("billing unavailable")})

gcpRec := &recommenderpb.Recommendation{
Name: "test-rec",
Content: &recommenderpb.RecommendationContent{
OperationGroups: []*recommenderpb.OperationGroup{
{Operations: []*recommenderpb.Operation{{Resource: "projects/test/instances/db-n1-standard-1"}}},
},
},
}

rec := client.convertGCPRecommendation(ctx, gcpRec, common.RecommendationParams{})
require.NotNil(t, rec)
assert.Nil(t, rec.RecurringMonthlyCost, "RecurringMonthlyCost must be nil when billing lookup fails")
}
52 changes: 34 additions & 18 deletions providers/gcp/services/cloudstorage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,13 @@ func skuMatchesStorageClass(sku *cloudbilling.Sku, storageClass, region string)
return true
}

// extractResourceTypeFromContent extracts the last path segment of the first
// non-empty Operation.Resource across all operation groups. Used by all four
// GCP service converters to set rec.ResourceType from Recommender payloads.
func extractResourceTypeFromContent(content *recommenderpb.RecommendationContent) string {
if content == nil || content.OperationGroups == nil {
// extractGCPResourceType returns the last path segment of the first non-empty
// resource field found across all operation groups, or "" if none is present.
func extractGCPResourceType(rec *recommenderpb.Recommendation) string {
if rec.Content == nil || rec.Content.OperationGroups == nil {
return ""
}
for _, opGroup := range content.OperationGroups {
for _, opGroup := range rec.Content.OperationGroups {
for _, op := range opGroup.Operations {
if op.Resource == "" {
continue
Expand All @@ -468,13 +467,13 @@ func extractResourceTypeFromContent(content *recommenderpb.RecommendationContent
return ""
}

// extractEstimatedSavings returns the negative of the PrimaryImpact cost
// projection (GCP encodes savings as a negative cost delta).
func extractEstimatedSavings(gcpRec *recommenderpb.Recommendation) float64 {
if gcpRec.PrimaryImpact == nil {
// extractGCPSavings returns the estimated monthly savings (positive value)
// from the primary cost impact of a GCP recommendation, or 0 if absent.
func extractGCPSavings(rec *recommenderpb.Recommendation) float64 {
if rec.PrimaryImpact == nil {
return 0
}
costProj := gcpRec.PrimaryImpact.GetCostProjection()
costProj := rec.PrimaryImpact.GetCostProjection()
if costProj == nil || costProj.Cost == nil {
return 0
}
Expand Down Expand Up @@ -502,6 +501,15 @@ func (c *CloudStorageClient) fillStoragePricing(ctx context.Context, rec *common
}
}

// termYearsFromLabel converts a term string such as "1yr" or "3yr" to an
// integer number of years (defaults to 1 for any unrecognized value).
func termYearsFromLabel(term string) int {
if term == "3yr" || term == "3" {
return 3
}
return 1
}

// convertGCPRecommendation converts a GCP Recommender recommendation to common format.
// It also calls getStoragePricing to fill CommitmentCost/OnDemandCost/SavingsPercentage/
// BreakEvenMonths so the scorer can filter and rank GCP recommendations correctly
Expand All @@ -528,17 +536,25 @@ func (c *CloudStorageClient) convertGCPRecommendation(ctx context.Context, gcpRe
PaymentOption: paymentOption,
}

rec.ResourceType = extractResourceTypeFromContent(gcpRec.Content)
rec.EstimatedSavings = extractEstimatedSavings(gcpRec)
rec.ResourceType = extractGCPResourceType(gcpRec)
rec.EstimatedSavings = extractGCPSavings(gcpRec)

// Thread pricing into the converter so the scorer can rank/filter GCP recs
// correctly (issue #1022 C2).
// correctly (issue #1022 C2). fillStoragePricing performs the single billing
// lookup and populates CommitmentCost; we reuse that value below to derive
// RecurringMonthlyCost rather than issuing a second SKU call.
if rec.ResourceType != "" {
termYears := 1
if rec.Term == "3yr" || rec.Term == "3" {
termYears = 3
}
termYears := termYearsFromLabel(rec.Term)
c.fillStoragePricing(ctx, rec, termYears)

// Cloud Storage committed-use discounts are monthly-payment commitments,
// so the per-month charge is CommitmentCost / termMonths. When the
// billing lookup failed, CommitmentCost stays 0 and RecurringMonthlyCost
// remains nil so the frontend renders "—" rather than a stale value.
if rec.CommitmentCost > 0 {
monthly := rec.CommitmentCost / float64(termYears*12)
rec.RecurringMonthlyCost = &monthly
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return rec
Expand Down
Loading
Loading