Skip to content

fix(azure): RI exchange silently forces AppliedScopeType to Shared, reallocating the discount #57

Open

Description

@cristim

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)

  1. Carry the source's AppliedScopeType (and applied scopes) through the exchange - requires adding the field to ExchangeableReservation so the tenant listing surfaces it.
  2. Expose applied_scope_type on AzureExchangeTargetBody and require it, so the caller states intent explicitly.
  3. 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.

Activity

  1. self-assigned this
    on Oct 16, 2026
  2. cristim commented on Oct 16, 2026

    @cristim
    MemberAuthor

    Claiming: working on this.

  3. cristim commented on Oct 16, 2026

    @cristim
    MemberAuthor

    Releasing my claim: checked against main 740dfbd, this is already fixed here by #209 (merged 2026-10-05 with Refs #57). ExchangeableReservation now carries the source AppliedScopeType/AppliedScopes (providers/azure/services/compute/exchange.go:60-64), validateAppliedScope refuses an empty or unsupported scope (exchange.go:287), and the calculateExchange request copies the source scope onto each target (exchange_operations.go:440-470). No Shared default remains. The remaining caller work (passing the listed source with its scope, mapping the refusal to a 4xx, openapi) is tracked in LeanerCloud/cloud-commitments-platform#521. Marking needs-info for a close decision.

  4. removed their assignment
    on Oct 16, 2026
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