Skip to content

test(purchase): pin the Azure savings-plans alias to the re-drive token through the real client - #1305

Merged
IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-838
Oct 25, 2026
Merged

IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-838

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

What #838 asked, and what the pinned module actually does now

#838's matrix was written against cloud-commitments-go ad67030387e4. Its open gap was that the Azure savings-plans client names its order alias from time.Now().UnixNano(), so the platform's DeriveIdempotencyToken(lineage, index) never reaches Azure.

I re-checked that against the module go.mod pins now (providers/azure v0.0.0-20261017134002-3def3f898617). That row is out of date. services/savingsplans/client.go:376 now does:

aliasName, err := common.OrderIDFromToken(opts.IdempotencyToken, fmt.Sprintf("cudly-%d", time.Now().UnixNano()))

So with a token (the platform's purchase path), the alias is the deterministic GUID derived from the token. The timestamp name is used only with an empty token (the CLI path), and a malformed token fails before any PUT. The module tests this with a mock in TestPurchaseCommitment_DeterministicAliasNameFromToken.

The gap in the platform: nothing here pinned it. TestRedriveReusesDerivedTokensForEveryProvider stops at a mock service client and does not cover azure savings-plans at all. A go.mod bump that brought the timestamp alias back would pass every platform test.

Change (tests only, no production change)

New file: internal/purchase/azure_savings_plans_alias_proof_test.go, with TestAzureSavingsPlansAliasIsDerivedFromTheRedriveToken. It runs processPurchaseRecommendations (the platform's real per-rec fan-out and token derivation) against the real pinned savingsplans.Client. Only the Azure order-alias API (SetOrderAliasClient) is faked. For both dispatchable slugs (savings-plans and savingsplans) it asserts:

  • each recommendation PUTs OrderIDFromToken(DeriveIdempotencyToken(lineage, index));
  • a re-drive of the same lineage PUTs the identical alias names;
  • recommendations and lineages do not collide, so a genuinely new purchase is not swallowed by an earlier alias;
  • no alias is the cudly-<nanos> timestamp fallback;
  • the provider is asked for common.ServiceSavingsPlansAll, the only service type Azure routes to this client;
  • recRedriveRefusalReason still refuses Azure savings-plans re-drive (see below).

Why the platform keeps refusing Azure savings-plans re-drive

A deterministic alias is necessary but not sufficient. Whether Azure answers a second PUT savingsPlanOrderAliases/{name} by returning the first order is server behavior. That could mean neither a second plan nor an error, and no fake can prove it. Neither repo has a test or a cited provider doc for it. So this PR keeps option (a) from the issue: refuse automatic re-drive and Retry for Azure savings plans. That covers allRecsSafeToRedrive, the #1668 Retry gate and the #1718 armedRedriveRefusal backstop, all of which go through recRedriveRefusalReason. The new test fails if someone relaxes that as a side effect.

Remaining exposure, unchanged: a first attempt whose response is lost. The row fails, and a human who then starts a new purchase gets a new lineage, so a new alias and a second plan. That is a legitimate new buy from the platform's point of view.

Proposed cloud-commitments-go change (not made here; go.mod untouched)

This would let the owner choose option (b) safely. In providers/azure/services/savingsplans/client.go PurchaseCommitment, when opts.IdempotencyToken != "", first call GET savingsPlanOrderAliases/{aliasName}. That is armbillingbenefits.SavingsPlanOrderAliasClient.Get, which exists in the pinned armbillingbenefits v1.0.0; add Get to the SavingsPlanOrderAliasAPI interface and realOrderAliasClient.

  • 200: adopt the existing order and return Success with its ID, with no PUT. This is the same shape as Azure reservations' DoIdempotentPurchaseTwoStep lookup.
  • 404: proceed to BeginCreate.
  • Any other error: fail closed, with no PUT.

Add module tests for each branch, including "existing alias, so BeginCreate is never called". After that lands and go.mod is bumped, the platform can drop the azure/savingsplans case in recRedriveRefusalReason. It would update the existing guard tests (TestRedriveGuardRefusalSetIsExactlyTheDispatchableSpellings, TestRedriveRefusedWhereNoProviderGuardExists) and the last assertion of the new test.

Matrix re-verified at the current pins

The pins are providers/aws aeb8a6f7ff51, providers/azure and providers/gcp 3def3f898617, and pkg 541e05e5fd29. I read the module source in the Go module cache.

Provider / service Token use at the pin Module test Platform re-drive
AWS Savings Plans CreateSavingsPlanInput.ClientToken (savingsplans/client.go:250) TestClient_PurchaseCommitment_SetsClientTokenForIdempotency allowed
AWS RDS, ElastiCache, MemoryDB, OpenSearch IdempotentReservationID + idempotencyGuard / recoverAlreadyExists (e.g. rds/client.go:150,173) rds *Idempotent* (5 tests), plus purchase_empty_token_test.go per service allowed
AWS EC2 tag lookup findRIByIdempotencyToken before buying (ec2/client.go:145,226) ec2/client_test.go refused
AWS Redshift corrected: not a tag lookup. Reserved nodes cannot be tagged, so guardAgainstRepurchase matches an owned node on offering ID, node count and start time (redshift/client.go:113-139) redshift/client_test.go *Idempotent* refused
Azure compute, cache, cosmosdb, database, managedredis, search, synapse reservations.DoIdempotentPurchaseTwoStep (internal/reservations/purchase.go:679) TestFindReservationOrderByIdempotencyToken_* etc. allowed
Azure Savings Plans corrected: alias = OrderIDFromToken(token) (savingsplans/client.go:376); server-side collapse of a repeat PUT unproven TestPurchaseCommitment_DeterministicAliasNameFromToken (mock); now also pinned here refused
GCP Compute Engine RequestId from OrderIDFromToken(token) + deterministic name (computeengine/client.go:829,873-878) TestComputeEngineClient_PurchaseCommitment_IdempotentReDrive allowed
GCP Cloud SQL, Cloud Storage, Memorystore PurchaseCommitment returns an error (no purchase API) n/a n/a (cannot buy)
any other provider n/a n/a refused

Owner decisions left open (why this is Refs, not Closes)

  1. Azure savings plans: keep (a), refusing permanently, or adopt the go change above and then relax. The platform is safe either way; this PR only pins the current state.
  2. EC2/Redshift: whether to relax the refusal. Redshift's guard is a spec match, not a token tag, and its comment requires that there be no concurrent same-spec purchase in the account and region. I would keep refusing Redshift. EC2's tag lookup is closer to safe, but the tag is written by a separate CreateTags after the buy, so a crash between the buy and the tag still leaves an untagged RI. That argues for keeping the refusal there too.

Deploy

None needed. This PR is tests only, and the platform behavior is unchanged. The Azure/GCP deploy pause since 2026-10-20 does not affect it.

Coordination with in-flight work

This PR adds one new test file and touches no production lines. Our fix/issue-243 branch already rewrites the stale "UnixNano" comments in internal/purchase/manager.go (the RedriveRefusalReason/recRedriveRefusalReason/safeFail doc comments), so I deliberately left them alone here to avoid a conflict. It does not overlap #1111, #1245 or fix/issue-532/-54/-361.

Refs #838

Verification

All of the following is unit, mock or fixture evidence. Nothing calls Azure or makes a purchase. The Azure order-alias API is a fake recorder.

Env: AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, Go 1.26.9, Linux.

  • go build ./..., go vet ./..., go vet -tags integration ./...: OK
  • go test ./internal/purchase/ -count=1: ok (32.6s)
  • go test ./internal/mocks/ -count=1: ok
  • golangci-lint run --allow-parallel-runners ./internal/purchase/ (v2.10.1): 0 issues
  • gofmt -l internal/purchase/: clean. GOWORK=off go mod tidy -diff: clean (go.mod/go.sum unchanged; armbillingbenefits was already a direct require).
  • gocyclo: no non-test Go files changed.

Fail-before and pass-after, by mutation. The test pins existing behavior, so "before" means a regression of each link in the chain:

  • Module regression (a temp copy of the pinned azure module with line 376 changed back to the cudly-<UnixNano> alias, wired in through GOWORK=off go test -modfile=<tmp>/go.mod with a replace; the repo's go.mod is untouched): FAIL. Expected c505bf94-f62f-1682-6300-54342dcab999, got cudly-1792744476476032937; the re-drive alias differs; "alias … is the timestamp fallback". Both subtests fail.
  • Platform token regression (execution.go derives the token from exec.ExecutionID instead of idempotencyLineageKey(exec)): FAIL, "each purchase PUTs the alias derived from DeriveIdempotencyToken(lineage, index)".
  • Guard relaxed (recRedriveRefusalReason azure savings-plans case disabled): FAIL, "Azure savings-plans re-drive must stay refused until a repeat alias PUT is proven to dedupe".
  • Unmutated: go test ./internal/purchase/ -count=1 -run TestAzureSavingsPlansAliasIsDerivedFromTheRedriveToken -v: PASS (both subtests).

Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ b56afb2b = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-204 76f85a4
fix/issue-438 78fe011
fix/issue-838 120b07f
fix/issue-143 aef1256
fix/issue-349 cfbb551
fix/issue-80 63405f9
fix/issue-199 36b9f8f
fix/issue-41 96fc237
fix/issue-299 6af14ed
fix/issue-324 3ab6729

fix/issue-324 and fix/issue-204 both edit .github/workflows/ci.yml next to the azure-kv-access-policy job. The tree kept #324's new "Run Function URL reader self-tests" step and #204's updated job comment; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers. The full suite ran on ed0c7174, which differs from b56afb2b only by one .gitallowed line on fix/issue-324:

  • Passed: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package that has them, one package at a time (-tags integration -count=1, 60-minute local timeout): internal/api, internal/purchase, internal/scheduler and internal/server all pass.
  • Secrets: the first run caught a bare placeholder account ID in fix/issue-324's new tftest, now allowlisted with an anchored .gitallowed line. Re-run at b56afb2b: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope

  • The go-side pre-PUT Get (above) belongs in cloud-commitments-go.
  • The CLI path (empty token, so the timestamp alias is still used) is cloud-commitments-cli's concern.
  • Any real-Azure check of repeat alias PUT semantics needs an owner-run sandbox test. It was not attempted.

Maintainer: please mirror #838's triage labels onto this PR: priority/p2, severity/medium, type/chore, triaged, urgency/this-quarter, impact/few, effort/m.

🤖 Generated with Claude Code

…en through the real client

The LeanerCloud#838 matrix (written against cloud-commitments-go ad67030387e4) says the
Azure savings-plans client names its order alias from time.Now().UnixNano(),
so the platform's derived idempotency token never reaches Azure. The module
pinned in go.mod now (providers/azure 3def3f898617) derives the alias from the
token via common.OrderIDFromToken, but nothing on the platform side pinned
that: TestRedriveReusesDerivedTokensForEveryProvider stops at a mock service
client and does not cover azure savings-plans.

Run the platform purchase fan-out against the real pinned savings-plans client
with only the order-alias API faked, and assert that each recommendation PUTs
OrderIDFromToken(DeriveIdempotencyToken(lineage, index)), that a re-drive PUTs
the identical names, that recommendations and lineages do not collide, and
that the platform still refuses Azure savings-plans re-drive (whether Azure
collapses a repeat alias PUT is server behavior no fake can prove).

Tests only; no production change.

Refs LeanerCloud#838

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm type/chore Maintenance / non-user-visible triaged Item has been triaged urgency/this-quarter Within the quarter impact/few Limited audience effort/m Days labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

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.

Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 0aa5ccd153b2fb76652fced8c40b7084086acb9a

Blocking:

  • none

Non-blocking:

  • internal/purchase/manager.go:559-563: the comment in recRedriveRefusalReason ("Azure savings-plans uses a timestamp-based alias name and has no server-side idempotency key") and its user-facing refusal text are now inaccurate at the pinned module. The alias is token-derived, and what is unproven is only Azure's handling of a repeat PUT. The body defers this to fix/issue-243 to avoid a conflict, which is fine, but that branch should change the refusal message as well as the doc comments.
  • The test fixes the dispatch to common.ServiceSavingsPlansAll via prov.On("GetServiceClient", ..., common.ServiceSavingsPlansAll, ...). That is useful, but it means a future change to the Azure dispatch mapping shows up as an unexpected-mock-call failure here and not as a clear assertion. That is acceptable for a pinning test.

Attacked:

  • Claims: I read providers/azure@v0.0.0-20261017134002-3def3f898617/services/savingsplans/client.go and confirmed that at :376 the alias is common.OrderIDFromToken(opts.IdempotencyToken, "cudly-<nanos>"), and that only the injectable order-alias client is called (no other Azure API before the PUT). So the test uses the real pinned client with no network. "Refs Provider duplicate-purchase proof matrix: Azure Savings Plans has no server-side dedupe #838", not Closes, is correct, because the (a)/(b) and EC2/Redshift decisions in the issue stay with the owner.
  • Fail-before (test-only PR, so by mutation):
    • Module regression: a copy of the pinned azure module with line 376 changed back to the cudly-<UnixNano> alias, wired in through GOWORK=off go test -modfile=<tmp> with a replace. Both subtests FAIL, with "alias ... is the timestamp fallback".
    • Platform token regression (execution.go:683 derives from exec.ExecutionID): both subtests FAIL.
    • Guard relaxed (manager.go:562 if false): both subtests FAIL with "must stay refused".
    • The repo's go.mod, go.sum and sources were restored, and git status was clean afterwards.
  • Ran:
    • go build ./... and go vet (plain, -tags integration, e2e): clean.
    • go test -race -short -count=1 ./internal/purchase/ ./internal/mocks/: ok. This includes the unfailable-mock-assertion guard.
    • go test -race -tags integration -count=1 ./internal/purchase/: ok (55s).
    • golangci-lint v2.10.1, with and without --build-tags integration: 0 issues.

Body-stale:

  • none found. I checked the go.mod pins, client.go:376 and the recRedriveRefusalReason routing as described.

Reviewer: rB8. 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.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 0aa5ccd153b2fb76652fced8c40b7084086acb9a

Blocking:

  • none

Non-blocking:

  • internal/purchase/manager.go:559: the comment on the Azure savings-plans refusal still says the client "uses a timestamp-based alias name". That is false at the current pin, and this PR's own test shows it. The body defers the fix to the fix/issue-243 branch to avoid a conflict, which is fine, but it should not be lost.
  • The re-drive leg builds a fresh PurchaseExecution with the same ExecutionID and IdempotencyKey rather than reloading the same row. That is enough to pin DeriveIdempotencyToken(lineage, i) through the real client, but it doesn't cover how a real re-drive reloads lineage. TestRedriveReusesDerivedTokensForEveryProvider already covers that half.

Attacked:

  • Read the pinned providers/azure@3def3f898617 savingsplans.Client.PurchaseCommitment. With the alias client injected, the only external call is BeginCreate/PollUntilDone, so NewClient(nil, ...) in the test cannot reach Azure. The alias is OrderIDFromToken(opts.IdempotencyToken, cudly-<nanos>), as the body says.
  • Fail-before (mutation): with execution.go:683 changed to derive the token from exec.ExecutionID instead of idempotencyLineageKey(exec), both subtests fail on the expected-alias assertion. Restored afterwards. The guard assertion (recRedriveRefusalReason non-empty for both slugs) would directly catch a relaxed refusal.
  • Ran: go build ./..., go vet (with and without -tags integration) on purchase, golangci-lint v2.10.1 (both tag sets, 0 issues), go test -race -short -count=1 ./internal/purchase/ (pass), the new test alone with -race -v (both subtests pass), and GOWORK=off go mod tidy -diff (clean). No production or integration-tagged files changed, so no integration run is needed. PR checks are green.
  • Refs, not Closes, is correct: Provider duplicate-purchase proof matrix: Azure Savings Plans has no server-side dedupe #838's ask, (a) versus (b) for Azure savings plans and EC2/Redshift relaxation, is still an owner decision. This PR only pins the current state.

Body-stale:

  • none

Reviewer: rB12. 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.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 0aa5ccd153b2fb76652fced8c40b7084086acb9a merged onto main (a59740da), together with #1249, #1258, #1265, #1305 in that order (combined tree c5a06eed, Linux, Go 1.26.10): go build ./..., go vet with and without -tags integration, the full go test -race -short -count=1 ./..., and go test -race -tags integration -count=1 on every changed package, against Docker Postgres. All pass. CI Success and pre-commit are green on this head, with 2 approving independent review(s) on it. Nothing was run against any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 25, 2026 21:20
@IAmJSD
IAmJSD merged commit eb165b4 into LeanerCloud:main Oct 25, 2026
35 checks passed
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on 0aa5ccd1 (two required: money, auth or security path), green CI, and the combined-tree verification above. Squash-merged at exactly 0aa5ccd153b2fb76652fced8c40b7084086acb9a.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 25, 2026
…alias proof

Main's LeanerCloud#1305 added a test that calls processPurchaseRecommendations with
the old five-argument signature. This branch adds the awsPartition
argument, so after the merge the purchase test package no longer
compiled. The account in that test is Azure, so it passes "" like the
branch's other non-AWS call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit that referenced this pull request Oct 26, 2026
…purchases (#1248)

* chore(deps): bump cloud-commitments-go providers/aws to 92722e6 for the aws-cn Savings Plans refusal

Pins providers/aws to the cloud-commitments-go#566 merge commit, which makes
Savings Plans offering lookups, validation and purchases fail with
savingsplans.ErrUnsupportedPartition before any AWS call when a region they
see is cn-* (aws-cn bills in CNY and the purchase path is USD-only). MVS
moves pkg to ff6225d, the version that commit requires.

Refs #1194

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(accounts): return an unsupported-partition notice for aws-cn accounts

aws-cn accounts were onboarded with no hint that CUDly cannot buy Savings
Plans for them. Account responses (create, update, get, list and the
self-account) now carry a derived, non-persisted "notices" list; an AWS
account whose role ARN is in aws-cn (or a self-account when CUDly itself runs
in aws-cn) gets an "unsupported_partition" notice. Onboarding is still
allowed so existing aws-cn accounts are not stranded.

The ARN partition parsing moves to config.AWSPartitionFromARN so the account
model and the STS host-identity path share it.

Refs #1194

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(purchase): refuse aws-cn Savings Plans purchases before any AWS call

A Savings Plans rec for an account whose role ARN is in aws-cn now fails
with an error wrapping savingsplans.ErrUnsupportedPartition before a
provider is built, on both the plan fan-out and the single-account
(direct-execute) paths. Other recs in the same execution still run, so a
mixed run ends partially_completed as before. The library's region-based
guard stays as the backstop for accounts with no role ARN.

Closes #1194

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): put the host account ID on an allowlisted fixture line

git-secrets' AWS account-ID detector flagged the inline sourceIdentity
literal in the partition notice test, which failed the pre-commit hook
and the allowlist script's scan of all tracked files. Split the literal
one field per line so the placeholder sits on an `AccountID: "..."`
line that .gitallowed already permits in test files. Same value, same
assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(purchase): pass the account partition in the Azure savings-plans alias proof

Main's #1305 added a test that calls processPurchaseRecommendations with
the old five-argument signature. This branch adds the awsPartition
argument, so after the merge the purchase test package no longer
compiled. The account in that test is Azure, so it passes "" like the
branch's other non-AWS call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant