Summary
An Azure RI exchange executed through POST /api/ri-exchange/azure-instances/exchange silently forces every acquired reservation to AppliedScopeType = Shared, discarding whatever scope the source reservation had. That is a silent reallocation of real money between cost centres, mentioned in neither the request nor the response.
Where
providers/azure/services/compute/exchange_operations.go defaults to Shared when tgt.AppliedScopeType is nil.
toAzureExchangeTargets (internal/api/handler_ri_exchange.go) never sets it.
AzureExchangeTargetBody has no such field, and openapi.yaml documents none - so no caller can control it.
ExchangeableReservation does not carry the source's applied scope either, so it cannot currently be preserved even in principle.
Concrete scenario
A reservation is deliberately Single-scoped to the prod subscription for chargeback. It is SKU-swapped through this endpoint and comes back Shared. Dev/test workloads now absorb the discount and prod pays on-demand. Nothing in the API surface told the caller this would happen.
Fix direction (any one, in preference order)
- Carry the source's
AppliedScopeType (and applied scopes) through the exchange - requires adding the field to ExchangeableReservation so the tenant listing surfaces it.
- Expose
applied_scope_type on AzureExchangeTargetBody and require it, so the caller states intent explicitly.
- At minimum, document the non-preservation in
openapi.yaml and in the handler's response, so the behaviour is at least not silent.
Found during adversarial review of PR LeanerCloud/cloud-commitments-cli#1515 (Azure RI exchange execute). Out of scope for that PR, which fixed the Regions-constraint source gap.
Summary
An Azure RI exchange executed through
POST /api/ri-exchange/azure-instances/exchangesilently forces every acquired reservation toAppliedScopeType = Shared, discarding whatever scope the source reservation had. That is a silent reallocation of real money between cost centres, mentioned in neither the request nor the response.Where
providers/azure/services/compute/exchange_operations.godefaults toSharedwhentgt.AppliedScopeTypeis nil.toAzureExchangeTargets(internal/api/handler_ri_exchange.go) never sets it.AzureExchangeTargetBodyhas no such field, andopenapi.yamldocuments none - so no caller can control it.ExchangeableReservationdoes not carry the source's applied scope either, so it cannot currently be preserved even in principle.Concrete scenario
A reservation is deliberately Single-scoped to the
prodsubscription for chargeback. It is SKU-swapped through this endpoint and comes backShared. Dev/test workloads now absorb the discount andprodpays on-demand. Nothing in the API surface told the caller this would happen.Fix direction (any one, in preference order)
AppliedScopeType(and applied scopes) through the exchange - requires adding the field toExchangeableReservationso the tenant listing surfaces it.applied_scope_typeonAzureExchangeTargetBodyand require it, so the caller states intent explicitly.openapi.yamland in the handler's response, so the behaviour is at least not silent.Found during adversarial review of PR LeanerCloud/cloud-commitments-cli#1515 (Azure RI exchange execute). Out of scope for that PR, which fixed the Regions-constraint source gap.