Repository navigation
fix(azure/savingsplans): fail closed on a live plan without an effective date - #498
Conversation
|
@coderabbitai review |
|
Warning Review limit reached
This review includes 5 billable files and costs up to $1.25.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 13 minutes for your next included review. View limit details
Comment |
|
Independent adversarial review: CLEAN at
|
| Azure state | Maps to | Missing start |
|---|---|---|
| Succeeded | Active | error |
| Creating, Created, PendingBilling, ConfirmedBilling | PaymentPending | error |
| Failed, Cancelled, Expired | terminal | allowed |
| nil properties or nil state | Active (#74) | error |
| unknown value | passed verbatim, live | error |
The terminal set matches recfilter's reject set (pkg/recfilter/dedupe.go:107-114). That set is a superset with Retired and PendingReturn, which Azure never emits, so no state slips between the two checks.
Trade-off for the owner to note (non-blocking). The SDK documents effectiveDateTime as "DateTime of the savings plan starts providing benefit from" (models.go:503-504). It is therefore plausibly absent for the pending states (Creating, Created, PendingBilling, ConfirmedBilling). With this PR, a subscription holding a just-ordered, not-yet-effective plan gets an error from the whole GetExistingCommitments call until the plan activates, which blocks savings plan purchases for it in the meantime.
That is deliberate fail-closed behaviour: it prevents the duplicate purchase this issue is about, and the error names the plan ID and state. Narrowing it to Succeeded only would reopen the fail-open path for pending plans. A one-line code comment saying pending states are included on purpose would help future readers. Not falling back to benefitStartTime or purchaseDateTime is correct (models.go:461-462), because guessing a start date is the fail-open risk.
Other attacks:
- Owned paths.
convertSavingsPlanhas one caller,appendOwnedPlans(client.go:193). That caller covers subscription-scoped plans and billing-account Single-scope plans (fix(azure): attribute single-scope savings plans billed to a billing account #468), so nothing bypasses the check. - fix(azure/savingsplans): take the inventory resource type from the SKU, not the ARM name #494.
savingsPlanResourceTypestill runs first (client.go:257), so SKU errors take precedence and are not hidden. - Fixtures. The four edited fixtures only gained
EffectiveDateTimeon owned live plans. No assertion was loosened. - Mutation probes (scratch copy, reverted):
- Disabling the check fails the live and pending sub-cases.
- Adding Active to the terminal set fails the live and no-state sub-cases.
Local verification (macOS, exact SHA f014142)
SDK-decoded fixtures; no Azure call.
- Fail-before. With
origin/main'sclient.goand the PR's tests,TestGetExistingCommitments_EffectiveDateTimeCompletenessFAILS. I ran this locally. - Pass-after.
(cd providers/azure && GOWORK=off go test -race -short -count=1 ./...)exits 0 with 13 packages ok.make buildandmake vetpass.
Labels mirror #481. CodeRabbit hit "Review limit reached". If that holds at merge: CR waived: quota, adversarial review + local verification + green CI.
…ive date convertSavingsPlan left StartDate zero when effectiveDateTime was missing, so recfilter.isRecentActiveCommitment treated a live plan as old and never suppressed a duplicate purchase. A plan that is not provably terminal (Failed, Canceled, Expired) and has no effectiveDateTime now returns an error naming the plan ID; GetExistingCommitments surfaces it through the incomplete-inventory wrapper. A nil Properties or ProvisioningState defaults to Active and therefore errors too. No fallback to PurchaseDateTime or BenefitStartTime, to avoid fabricating a date. Existing fixtures gain an effectiveDateTime to stay valid live rows. Closes #481 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f014142 to
7d1bd94
Compare
|
Updated onto |
|
Merged by squash at reviewed head
CR waived: quota, adversarial review + local verification + green CI. A retrospective CodeRabbit review will run when quota returns. Closes #481. Note for the owner: plans in pending states (Creating/Created/PendingBilling/ConfirmedBilling) without effectiveDateTime now fail the subscription's savings plan inventory until they activate (fail-closed by design; see the verdict). |
…d#498 LeanerCloud#498 makes convertSavingsPlan reject a live plan without effectiveDateTime. The page-2 plan billed to another subscription is now in the covering list, so give it one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… duplicate check (#491) * fix(azure): count savings plans billed to another subscription in the duplicate check GetExistingCommitments keeps the #40/#207 rule that Account is the payer, so a plan billed to subscription B never appears in A's inventory, even when its applied scope covers A. DuplicateChecker only saw that inventory, so a recently bought plan billed to B and applied to A did not suppress A's matching recommendation, and the same commitment could be bought again while the benefit-recommendations API lagged. recfilter.DuplicateChecker now checks recommendations against GetCoveringCommitments when a client implements it, and falls back to GetExistingCommitments otherwise. The Azure savings plans client implements it as its inventory plus plans billed to another subscription that may apply to this one. Only a Single scope that resolves to a different subscription is left out. Shared, management-group, unknown and unresolvable scopes count as covering, because their containment cannot be established here. A covering plan without an ID fails the call. Account stays the payer, so ownership inventory is unchanged. Closes #466 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(azure): adapt covering savings plans to convertSavingsPlan's error return #494 made convertSavingsPlan return an error and require a SKU. Propagate the error for plans billed to another subscription too, and give the covering-path fixtures the Compute_Savings_Plan SKU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(azure): avoid shadowed err in appendOwnedPlans after the main merge Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(azure): date the covering-path pagination fixture for #498 #498 makes convertSavingsPlan reject a live plan without effectiveDateTime. The page-2 plan billed to another subscription is now in the covering list, so give it one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cristian Magherusan-Stanciu <cristim@users.noreply.github.com>
Summary
convertSavingsPlan(providers/azure/services/savingsplans/client.go) setStartDateonly wheneffectiveDateTimewas present. A live savings plan returned without it kept a zeroStartDate,recfilter's recency check then treated it as old, and the duplicate-purchase guard let a repeat purchase through. Azure reservations already reject this case (internal/reservations/strict.go); savings plans did not.effectiveDateTimenow failsGetExistingCommitments(through the existing "incomplete savings plan inventory ... row N" wrapping), naming the plan ID and its state.armbillingbenefitsvaluesazureSavingsPlanStatemaps to failed, canceled and expired).recfilterrejects those before readingStartDate; its own reject set is a superset (it adds retired and pending-return, which Azure never emits), so no state slips between the two. Unknown states are treated as live.purchaseDateTimeorbenefitStartTime(both exist on the SDK model): the repo does not fabricate a start date.Closes #481
Tests
TestGetExistingCommitments_EffectiveDateTimeCompleteness, SDK-decoded fixtures:recfilter.DuplicateChecker:AdjustRecommendationsForExistingreturns the error instead of passing the recommendation through.Four existing fixtures (
client_test.go,scope_test.go,client_pager_cap_test.go) gained aneffectiveDateTime: they had no state or start and passed only through the old fail-open path.client.go, the live, pending and no-state cases fail in both variants.Verification
make build,make vet,(cd providers/azure && GOWORK=off go test -race -short ./services/savingsplans/...), the pinned golangci-lint v2.10.1 on the final code (0 issues), gofmt, and the commit/pre-push hooks pass. Fixture-based evidence only; no live Azure call.Local review: one adversarial pass by a non-author reviewer (the one that reviewed #494), no actionable findings.
Overlap
#491 also calls
convertSavingsPlan(appendCoveringForeignPlan): after a rebase its foreign-plan fixtures need aneffectiveDateTime, and a live foreign plan without one will fail the covering list closed, consistent with this fix. #473 only touches a comment in this file.🤖 Generated with Claude Code