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). |
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