Skip to content

fix(azure): retail-price currency is a USD seed overwritten by whichever item came last #102

Open

Description

@cristim

Summary

All seven Azure retail-price extractors seed currency = "USD" when the API reports none, then let every item with a non-empty CurrencyCode overwrite it, while onDemand and reservation are assigned only from items matching the term or Type == "Consumption". The returned currency is therefore whatever the last item carried, not the currency of the two prices actually returned, and a response mixing currencies produces a savings percentage computed across denominations and labelled with one of them. No client pins currencyCode in the request (params.Add sets only $filter and api-version in all seven), so the code relies on the Retail Prices API's undocumented USD default to keep the item set single-currency. The same seed-and-overwrite shape is copied into the GCP cloudstorage, cloudsql and memorystore clients. OfferingDetails.Currency, the field this label ends up in, currently has no consumer, so the spend-cap half of the original finding is not established; the mislabelled savings percentage on the recommendation path is. LeanerCloud/cloud-commitments-cli#1934 covers the price last-item-wins in the same loop; this is the currency half its fix does not address.

Location

  • providers/azure/services/cache/client.go:600-612 at 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (savings percentage from the pair at :558, currency stamped at :564)
  • providers/azure/services/compute/client.go:766-778
  • providers/azure/services/cosmosdb/client.go:598-610
  • providers/azure/services/database/client.go:618-630
  • providers/azure/services/managedredis/client.go:520-529 (parsePriceItems)
  • providers/azure/services/search/client.go:511-523
  • providers/azure/services/synapse/client.go:457-475
  • GCP copies: providers/gcp/services/cloudstorage/client.go:388, :400-402, and the same in cloudsql and memorystore

Failure scenario

A Retail Prices response whose Consumption item is USD and whose later reservation item is EUR. The function returns onDemand in USD, reservation in EUR, labels the pair EUR, and getRedisPricing computes savingsPercentage by mixing the two, reporting a fabricated saving on the recommendation with no signal.

Evidence

currency = "USD"
termStr := azureTermString(termYears)

for _, item := range items {
    if item.CurrencyCode != "" {
        currency = item.CurrencyCode
    }
    if item.ReservationTerm == termStr {
        reservation = item.RetailPrice
    } else if item.Type == "Consumption" {
        onDemand = item.UnitPrice
    }
}

Suggested fix

Pin currencyCode=USD in the request params, take currency only from the items the two prices were read from, and return an error when those items carry no currency or disagree, instead of seeding USD and labelling a mixed pair.


Found by the 2026-09-02 codebase audit, findings A15-009, A08b-011, each reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md.

Activity

  1. cristim commented on Oct 16, 2026

    @cristim
    MemberAuthor

    Checked against main 740dfbd: the Azure extractors now take currency from the selected item via pricing.SelectReservation, and GCP Cloud SQL and Memorystore use billingcurrency.Unify. The only remaining USD seed is providers/gcp/services/cloudstorage/client.go:283, which open PR #424 changes. This can close once #424 merges.

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