Reviewed commit: be11bdcb5 (origin/main), from the 2026-07-28 full-repo review.
Severity: CRITICAL. Azure EA customers buy a fraction of the recommended capacity while the dashboard reports the recommendation as fully actioned.
Where
providers/azure/internal/recommendations/converter.go:146 and :151-152, via resolveLegacyResourceType at :228-230
- Consumed by every service purchase body, e.g.
providers/azure/services/compute/client.go:421-426
What
extractLegacy sets ResourceType from props.NormalizedSize (preferred first in the ladder) but sets Count from props.RecommendedQuantity.
Azure's armconsumption model carries two distinct quantity fields: RecommendedQuantity (units of the ACTUAL SKU, i.e. SKUProperties[SKUName]) and RecommendedQuantityNormalized (units of NormalizedSize). See armconsumption@v1.1.0/models.go:715-721. The code mixes the normalized size with the un-normalized count.
The Modern path (:243-247) prefers SKUName first and is therefore self-consistent. Only the Legacy/EA path is mismatched.
Failure scenario
EA subscription. Azure recommends 4 x Standard_D8s_v3 with NormalizedSize="Standard_D2s_v3", RecommendedQuantity=4, RecommendedQuantityNormalized=16.
The converter yields ResourceType="Standard_D2s_v3", Count=4. buildReservationBody sends sku.name="Standard_D2s_v3", quantity=4. The customer buys one quarter of the recommended capacity and keeps paying on-demand for the rest, while the dashboard reports the recommendation as fully actioned.
The error scales with the family's instance-flexibility ratio, up to 16x for a large-to-smallest normalization.
Scope (added after investigation, PR #1771)
Seven services consume this pairing through the one shared recommendations.Extract, not just the compute example above: compute, cache, cosmosdb, database, managedredis, search, synapse.
Count also feeds provider-agnostic logic in cmd/helpers.go, so the wrong count reached sizing math and duplicate suppression as well as the purchase body:
applySizing / ApplyCoverage scale by newCount / rec.Count
ApplyTargetCoverage anchors on rec.Count
--max-instances clamps on it
- the duplicate checker compares
existingCount >= rec.Count and subtracts
Verified it reaches a purchase, not only a display. The pair becomes sku.name and quantity on the calculatePrice and purchase request bodies, and Azure accepts it because both values are individually valid. The only quantity guard on the path is rec.Count <= 0.
Direction: under-buy — fails safe on commitment risk, unsafe on reported savings. Nobody is locked into capacity they cannot use, but EstimatedSavings is Azure's figure for the full recommended quantity and rides along unchanged, so the savings number shown to whoever approves the purchase is wrong. A decision-quality defect rather than a spend defect.
Checked negative: Modern/MCA's primary path is NOT affected
resolveModernResourceType prefers the top-level SKUName, which RecommendedQuantity counts, so the rung real MCA payloads take is self-consistent. Confirmed by running the converter, not assumed. Do not "fix" that rung.
(PR #1771 does pair Modern's fallback rung, which had the same mismatch when SKUName is absent, while leaving the SKUName rung byte-identical.)
Fix direction
In extractLegacy, either pair NormalizedSize with RecommendedQuantityNormalized, or make resolveLegacyResourceType prefer SKUProperties[SKUName] (matching the Modern ladder) so that RecommendedQuantity is in the right units. Assert the pairing in a converter test.
Note
Neither providers/azure nor providers/gcp is covered by CI (#1478), so nothing in this package has been vetted by go vet, golangci-lint, or the unit/integration suites.
Reviewed commit:
be11bdcb5(origin/main), from the 2026-07-28 full-repo review.Severity: CRITICAL. Azure EA customers buy a fraction of the recommended capacity while the dashboard reports the recommendation as fully actioned.
Where
providers/azure/internal/recommendations/converter.go:146and:151-152, viaresolveLegacyResourceTypeat:228-230providers/azure/services/compute/client.go:421-426What
extractLegacysetsResourceTypefromprops.NormalizedSize(preferred first in the ladder) but setsCountfromprops.RecommendedQuantity.Azure's
armconsumptionmodel carries two distinct quantity fields:RecommendedQuantity(units of the ACTUAL SKU, i.e.SKUProperties[SKUName]) andRecommendedQuantityNormalized(units ofNormalizedSize). Seearmconsumption@v1.1.0/models.go:715-721. The code mixes the normalized size with the un-normalized count.The Modern path (
:243-247) prefersSKUNamefirst and is therefore self-consistent. Only the Legacy/EA path is mismatched.Failure scenario
EA subscription. Azure recommends 4 x
Standard_D8s_v3withNormalizedSize="Standard_D2s_v3",RecommendedQuantity=4,RecommendedQuantityNormalized=16.The converter yields
ResourceType="Standard_D2s_v3",Count=4.buildReservationBodysendssku.name="Standard_D2s_v3",quantity=4. The customer buys one quarter of the recommended capacity and keeps paying on-demand for the rest, while the dashboard reports the recommendation as fully actioned.The error scales with the family's instance-flexibility ratio, up to 16x for a large-to-smallest normalization.
Scope (added after investigation, PR #1771)
Seven services consume this pairing through the one shared
recommendations.Extract, not just the compute example above:compute,cache,cosmosdb,database,managedredis,search,synapse.Countalso feeds provider-agnostic logic incmd/helpers.go, so the wrong count reached sizing math and duplicate suppression as well as the purchase body:applySizing/ApplyCoveragescale bynewCount / rec.CountApplyTargetCoverageanchors onrec.Count--max-instancesclamps on itexistingCount >= rec.Countand subtractsVerified it reaches a purchase, not only a display. The pair becomes
sku.nameandquantityon thecalculatePriceandpurchaserequest bodies, and Azure accepts it because both values are individually valid. The only quantity guard on the path isrec.Count <= 0.Direction: under-buy — fails safe on commitment risk, unsafe on reported savings. Nobody is locked into capacity they cannot use, but
EstimatedSavingsis Azure's figure for the full recommended quantity and rides along unchanged, so the savings number shown to whoever approves the purchase is wrong. A decision-quality defect rather than a spend defect.Checked negative: Modern/MCA's primary path is NOT affected
resolveModernResourceTypeprefers the top-levelSKUName, whichRecommendedQuantitycounts, so the rung real MCA payloads take is self-consistent. Confirmed by running the converter, not assumed. Do not "fix" that rung.(PR #1771 does pair Modern's fallback rung, which had the same mismatch when
SKUNameis absent, while leaving theSKUNamerung byte-identical.)Fix direction
In
extractLegacy, either pairNormalizedSizewithRecommendedQuantityNormalized, or makeresolveLegacyResourceTypepreferSKUProperties[SKUName](matching the Modern ladder) so thatRecommendedQuantityis in the right units. Assert the pairing in a converter test.Note
Neither
providers/azurenorproviders/gcpis covered by CI (#1478), so nothing in this package has been vetted bygo vet, golangci-lint, or the unit/integration suites.