Repository navigation
fix(azure/compute): skip scope checks on terminal VM reservations - #439
Conversation
StrictCommitment ran the applied-scope attribution check on every VM reservation, so a single cancelled, expired, failed, split or merged Shared (or scope-less) reservation made GetExistingCommitments fail and the duplicate guard block purchases for the subscription indefinitely. Gate the applied-scope check, like the live-field completeness check, on the converted commitment state being non-terminal. The date-only expiry-today check still runs on every row, since a Succeeded reservation expiring today converts to Expired but may still be live. Closes LeanerCloud#315 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Commitment lifecycle and scope validation providers/azure/services/internal/reservations/strict.go, providers/azure/services/internal/reservations/strict_test.go |
StrictCommitment delegates scope attribution and expiry checks to attributeReservation. Tests cover Shared reservations across billing scopes and lifecycle states. |
Inventory behavior coverage providers/azure/services/compute/inventory_scope_sdk_test.go |
SDK-backed tests cover terminal reservations, live Shared attribution errors, expiry-today errors, split parent and child rows, and recommendation filtering. |
Priority:
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix · Severity of issue fixed: Medium
Suggested reviewers: cristim
Merge Risk: 
Minimal · up to 7604d
-
The change is mergeable with normal checks. An expiry-today test has a rare UTC-midnight timing flake, but no material behavior issue is established.
Security architecture risk: 
Low · up to b7ced
-
The change stops inactive VM reservations from blocking purchases while preserving live-reservation validation and ambiguous-expiry checks. Remaining uncertainty concerns Azure’s reporting of terminal states and replacement reservations, rather than a demonstrated security bypass.
Retained concerns
No architecture-level concerns identified.Security review details
-
Security Blast Radius
- inferred — The demonstrated exposure is the Azure VM inventory decision for the queried subscription and its downstream recommendation filtering. Shared reservations can originate from applied reservation orders spanning a billing scope, but the changed validation does not itself grant Azure permissions or execute a purchase.
Trust Boundaries and Controls
- observed — Provider-returned lifecycle state determines eligibility for the exemption. Single-scope rows are subscription-matched upstream, while live Shared, missing-scope and unknown-scope rows remain rejected by StrictCommitment. Pending states are not exempted.
Resilience and Maintainability Implications
- observed — The added tests assert that terminal rows do not block recommendations, live Single coverage remains effective beside terminal rows, live Shared rows still fail, and a listed live Shared child still fails beside a Split parent. These are fixture-based assertions, not evidence of live Azure consistency or concurrent purchase safety.
-
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Out of Scope Changes check
-
Explanation
The changes stay within issue
#315. The shared attribution helper supports the Azure compute inventory path, preserves fail-closed behavior for live unattributable reservations, and skips terminal scope failures. The added internal and SDK-backed tests verify the issue behavior. No unrelated implementation is shown.
-
Full details: Linked Issues check
-
Explanation
Issue
#315requires scope and lifecycle attribution checks to run only for non-terminal commitments. The change correctly defers unknown, missing, and Shared scope errors for terminal states, and the tests cover those cases. However,attributeReservationcallsvalidateReservationExpirybefore the terminal check. A Succeeded reservation with an expiry date equal to the current UTC date converts toExpiredbut still returns an error. The new tests explicitly require this behavior, so the lifecycle requirement in#315remains unmet.Resolution
Gate
validateReservationExpiryon!isTerminalCommitmentState(commitment.State), or otherwise apply the#315requirement that terminal commitments cannot fail scope or lifecycle attribution checks. Update the current-date expiry test to cover the required terminal outcome.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
cristim
left a comment
There was a problem hiding this comment.
Independent review verdict: CLEAN at d57bfa1
Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief. The PR body was treated as a claim. Coordinator: claude-cc-reviewer.
Blocking findings: none. Every terminal state the conversion can produce is excluded from coverage. The only possibly-live reservation that converts to a terminal state (expiring today) is still caught by the expiry check, which runs on every row.
Non-blocking notes:
- Split/Merged parents (Retired) skip the scope check on the assumption that Azure lists their children separately and the children are scope-checked. This matches #317's gate, but it is an API assumption that could not be checked offline.
- One mutation probe was equivalent to the original, since Active is never terminal. That is not a test gap.
What was attacked and how (local macOS; unit tests with SDK HTTP fixtures through the public GetExistingCommitments and the real recfilter.DuplicateChecker; no live Azure):
- Terminal set vs conversion:
inventory.go:283-308maps provisioning states to Expired, Canceled, Failed (Failed/BillingFailed) and Retired (Split/Merged).isTerminalCommitmentState(strict.go:55-62) lists exactly those four.- Pending states (Creating, PendingBilling, ...) map to PaymentPending, and nil or unknown states map to
"". Both still get the scope and live-field checks. - Succeeded with expiry not after now maps to Expired.
validateExpiryLifecycleruns first on every row (strict.go:36), so a same-UTC-date expiry still fails closed.
- Pending states (Creating, PendingBilling, ...) map to PaymentPending, and nil or unknown states map to
- recfilter:
isRecentActiveCommitment(pkg/recfilter/dedupe.go:99-106) returns false for Retired, PendingReturn, Expired, Canceled and Failed before any account or key use. So terminal rows attributed to the queried subscription never count as coverage, and can't cause a false "already covered" decision. - Callers:
StrictCommitmenthas one non-test caller (compute/client.go:282). The split helpers have no other users. - Ordering: expiry check, then terminal return, then scope, then live fields.
- Tests: with
GOTOOLCHAIN=go1.26.9and AWS config set to/dev/null:go test -race -count=1 ./...inproviders/azurepasses, with no SKIP seen.go test -race ./recfilter/...inpkgpasses.go vetis clean.
- Fail-before: with
strict.goreverted to main, the three new tests fail (TerminalSharedVMDoesNotFail,TerminalSharedMixedWithLive,SharedExpiryTodayStillFails). - Mutation probes: each of these is killed:
- dropping Retired from the terminal set;
- dropping Expired from the terminal set;
- moving the expiry check under the gate;
- disabling the live-field check.
Gate:
- Labels: mirrored from #315.
- CodeRabbit: skipped because the PR is a draft. A thorough review plus exact-revision local verification covers the SHA.
- CI: the fork runs need maintainer approval: 38896046545 (pre-commit) and 38896046576 (CI - Build & Test) are
action_required. Merge waits on them going green and on the draft being marked ready.
…l fails closed Document the Retired scope-check exemption's API-shape assumption and add a test that the exemption cannot hide a live child's unattributable coverage. Addresses review note on LeanerCloud#439. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both non-blocking notes are addressed at b7cedd8, one commit on top of the reviewed d57bfa1. No rebase.
Checks for 🤖 Generated with Claude Code |
|
Delta review: CLEAN at Delta
Evidence (macOS; SDK HTTP fixtures; no Azure call):
Earlier findings re-checked: the Split/Merged API-shape assumption is unchanged and is now documented in code. CI is green on Needed before merge. @IAmJSD, please rebase onto |
|
Brought up to date with |
|
The head moved to
CI is re-running on |
|
Merge driver status: blocked on the author. The branch now conflicts with |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…expiry-today rows The merge of main brought LeanerCloud#423's billingScopeId attribution together with this PR's terminal-row exemption. Pin how they compose in StrictCommitment: a Shared reservation billed to another subscription is skipped even when it is terminal or expires today, one billed to this subscription still hits the expiry-today check, and a terminal one whose purchaser cannot be identified no longer fails closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main to resolve the conflict in |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB1. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rB30. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main (now |
|
Local verification at |
Closes #315
Root cause
reservations.StrictCommitment(providers/azure/services/internal/reservations/strict.go, the shared reader that #328 moved compute'svalidateVMReservationInventoryinto) ranvalidateReservationScopeon every VM reservation before the terminal-state exemption. #317 (Refs #190) only gated the live-field completeness check on non-terminal state, so on current main (740dfbd) a single cancelled/expired/failed/split/merged reservation withappliedScopeTypeShared (or missing) still madecompute.GetExistingCommitmentsreturn an error, and the duplicate guard then blocked purchases for the subscription indefinitely.Fix
validateReservationScopeis split into two checks:validateExpiryLifecycle(date-only expiry falls on today's UTC date): still runs on every row. A Succeeded reservation expiring today converts toExpired(midnight UTC is not after now), but it may still be live, so exempting terminal rows from this check would hide possibly-live coverage.validateAppliedScope(Shared / missing / unknown scope type): now runs only when the convertedcommitment.Stateis non-terminal, the same gate fix(azure/compute): fail closed on incomplete live VM reservations #317 uses forvalidateLiveReservationFields.Terminal rows are still returned in the inventory (attributed to the queried subscription, as before #274's checks).
pkg/recfilterignores Canceled/Expired/Failed/Retired, so they never count as coverage. Live Shared/missing-scope rows still fail closed, unchanged.Verification (unit tests with SDK HTTP fixtures through public
GetExistingCommitmentsand the realrecfilter.DuplicateChecker; not live Azure)All test runs use
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null.New tests in
providers/azure/services/compute/inventory_scope_sdk_test.go:TestGetExistingCommitmentsSDKTerminalSharedVMDoesNotFail: Shared VM reservations in Cancelled, Expired, Failed, Split, Merged, Succeeded-with-past-expiry, plus Cancelled with null scope type. Expects no error, the terminal state, and the guard passing the recommendation through.TestGetExistingCommitmentsSDKTerminalSharedMixedWithLive: cancelled Shared + live Single still filters the recommendation; cancelled Shared + live Shared (Succeeded, PendingBilling) still errors with "cannot attribute".TestGetExistingCommitmentsSDKSharedExpiryTodayStillFails: Succeeded Shared reservation expiring today still errors with "date-only expiryDate".Fail-before (main 740dfbd, test file only):
(
SharedExpiryTodayerrored before too, with "cannot attribute" rather than "date-only expiryDate", since the scope check ran first.)Pass-after: all three tests and subtests pass.
Mutation probes, each reverted:
TestGetExistingCommitmentsSDKExpiryTodayFailsClosedand...SharedExpiryTodayStillFailsfail...TerminalSharedVMDoesNotFailand...TerminalSharedMixedWithLivefail...SharedVMFailsClosed,...ScopeBoundary(missing/unknown),...MixedWithLivelive-Shared subtests andTestStrictCommitment_ErrorTextUsesLabelsfailCommands run (macOS):
Real-cloud gap: not exercised against a live Azure subscription with a terminal Shared reservation; the fixtures follow the armreservations JSON shape the existing #317 tests use.
Out of scope
StrictCommitmentyet, so this change only affects compute today.Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,urgency/this-sprint,impact/few,effort/xs,type/bug,triaged.🤖 Generated with Claude Code
Summary by CodeRabbit