fix(azure): carry the source reservation's applied scope through an RI exchange - #209
Merged
Merged
Conversation
…I exchange buildCalculateExchangeRequest defaulted every purchased reservation to AppliedScopeType Shared when the target left it unset, and no caller set it. Exchanging a reservation Single-scoped to one subscription therefore bought a Shared replacement, moving the discount to every subscription in the billing scope without telling the caller. ExchangeableReservation now carries AppliedScopeType and AppliedScopes from the tenant listing, and CalculateExchange purchases every target with the sources' scope. A source with no reported scope, an unknown scope type, Single without scopes, or sources whose scopes differ is refused instead of being defaulted. ExchangeTarget.AppliedScopeType is removed: it could not express a Single scope's subscription list and its nil value was the Shared fallback. Refs #57
Contributor
|
Warning Review limit reached
This review includes 4 billable files and costs up to $1.00.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 7 minutes for your next included review. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Your 50 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (4)
Comment |
…fusals Shared exchanges no longer send "appliedScopes":[] (Azure says not to specify it for Shared). Scope refusals wrap the exported ErrUnsupportedAppliedScope so callers can map them to a client error. Single requires exactly one scope, and scope lists are compared only for Single sources. Refs #57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
An Azure RI exchange no longer buys the replacement reservation with
AppliedScopeType = Sharedby default. The source reservation's applied scope is carried from the tenant listing throughCalculateExchangeinto everyreservationsToPurchaseentry.ExchangeableReservationgainsAppliedScopeType(armreservations.AppliedScopeType) andAppliedScopes, populated byListExchangeableReservationsfromProperties.AppliedScopeType/Properties.AppliedScopes.CalculateExchangerefuses, before calling Azure:applied_scope_type is required; pass the reservation as returned by ListExchangeableReservations)ManagementGroup, which this SDK/API version cannot express on a purchase)Singlewithout exactly one applied scope (Azure'sMissingAppliedScopesForSingle/InvalidSingleAppliedScopesCount)compute.ErrUnsupportedAppliedScope(%w), so callers can match it witherrors.IsbuildCalculateExchangeRequestsendssources[0]'s scope type on every target, and its single scope only when the type is Single (appliedScopes stays nil, so it is omitted from the JSON, for Shared). The hard-codedAppliedScopeTypeSharedfallback is gone.ExchangeTarget.AppliedScopeTypeis removed. It could not express the subscription list aSinglescope needs, and its nil value was the Shared fallback. Nothing in this repo or the platform sets it (checkedcloud-commitments-platformat HEAD:internal/api/handler_ri_exchange.go:661buildsExchangeTargetwithout it).Why
Exchanging a reservation Single-scoped to
prodfor chargeback returned a Shared reservation, so other subscriptions took the discount andprodpaid on-demand, and nothing in the request or response said so (#57).Azure contract (api-version 2022-03-01, the version armreservations v1.1.0 calls:
calculateexchange_client.go:98)PurchaseRequest:properties.appliedScopeType(Single | Shared) andproperties.appliedScopes, "List of the subscriptions that the benefit will be applied. Do not specify if AppliedScopeType is Shared." No default is documented. https://learn.microsoft.com/en-us/rest/api/reserved-vm-instances/calculate-exchange/post?view=rest-reserved-vm-instances-2022-03-01ReservationsProperties:appliedScopeType,appliedScopes, plusappliedScopeProperties(management group / tenant), which the v1.1.0 SDK does not model. https://learn.microsoft.com/en-us/rest/api/reserved-vm-instances/reservation/list-all?view=rest-reserved-vm-instances-2022-03-01armreservations@v1.1.0):PurchaseRequestProperties.AppliedScopeType/AppliedScopesatmodels.go:652-657,Properties.AppliedScopeType/AppliedScopesatmodels.go:548-553, enumAppliedScopeTypeShared/AppliedScopeTypeSingleatconstants.go:21-22.What the platform must change to benefit
toAzureExchangeSources(internal/api/handler_ri_exchange.go:639) builds sources from the request body with onlyReservationIDandQuantity. After bumping this module, every Azure exchange will be refused withErrUnsupportedAppliedScope(sources[0]: unsupported applied scope: applied_scope_type is required) until the handler passes the matching entry from theownedlisting it already fetches forrequireAzureSourceOwnership, with the request's quantity, and mapErrUnsupportedAppliedScopeto a 4xx (todaymapAzureExchangeErrorturns anything that is not an*azcore.ResponseError4xx into a generic 500). That refusal is intentional: the alternative is the silent Shared purchase. HenceRefs #57, notCloses: the end-to-end fix needs that platform change.How verified
GOTOOLCHAIN=go1.26.6 GOWORK=off GOFLAGS='-p=2 -count=1' AWS_EC2_METADATA_DISABLED=true, inproviders/azure:go build ./...,go vet ./...,go test ./...: all pass.go test -race ./services/compute/: 251 passed.golangci-lint run ./services/compute/...: no issues.gofmt -l: clean.providers/azure/services/compute.Failing before: with the new tests and the new
ExchangeableReservationfields butexchange_operations.gofromorigin/main:Passing after:
ok github.com/LeanerCloud/cloud-commitments-go/providers/azure/services/compute.Mutation: disabling the same-scope check (
if false && !s.sameAppliedScope(...)) failsValidationSources/shared_mixed_with_singleandValidationSources/single_on_different_subscriptions. Reverted.This is fixture-based evidence: no real Azure exchange was priced or executed.
Reviewer should challenge
appliedScopeshas them dropped: nothing is sent for Shared, per the contract.ExchangeTarget.AppliedScopeTypefield breaks the API for any consumer outside LeanerCloud.Update: review fixes. appliedScopes is no longer sent as
[]for Shared (asserted nil and absent from the marshalled JSON), refusals wrapErrUnsupportedAppliedScope, Single requires exactly one scope, and scope lists are compared only for Single.