Skip to content

refactor(provider): build credentialed Azure/GCP providers through the registry - #1310

Draft
IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-41
Draft

IAmJSD wants to merge 1 commit into
LeanerCloud:mainfrom
IAmJSD:fix/issue-41

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

First slice of the ARCH-12 cleanup. It is meant to change no behavior.

Root cause

The credentialed Azure and GCP paths in internal/purchase/execution.go (resolveAzureProvider, resolveGCPProvider) and internal/scheduler/scheduler.go (collectAzureForAccount, collectGCPForAccount) skipped the cloud-commitments-go provider registry. They called the concrete constructors directly: azureprovider.NewAzureProvider(&provider.ProviderConfig{Profile: sub}) + SetCredential(cred), and gcpprovider.NewProviderWithCredentials(ctx, project, ts). The Azure path also pinned the subscription through the deprecated ProviderConfig.Profile field.

The pinned pkg/provider registry can already take credentials. ProviderConfig has typed AzureSubscriptionID/GCPProjectID fields and AzureTokenCredential/GCPTokenSource slots, and the registered azure/gcp factories (NewAzureProvider/NewProvider) install them. So the fix needs no change in cloud-commitments-go.

Fix

  • New internal/providerfactory package:
    • NewAzure(subscriptionID, cred) and NewGCP(projectID, ts) build the provider with Registry.GetProviderWithConfig. They use only the typed fields and credential slots, never Profile.
    • The package blank-imports the Azure/GCP provider packages, so both factories are always registered.
  • The four call sites above now use these helpers. purchase/execution.go no longer imports the concrete Azure/GCP provider packages. The scheduler still imports them for IsPermissionError / AsPartialSubscriptionFailure.
  • After this change no non-test code under cmd/ or internal/ sets ProviderConfig.Profile. That clears the platform-side blocker to removing the field in cloud-commitments-go.

Behavior notes (please review)

  • Azure: the result is identical to the legacy construction: same subscription ID, same credential, same region. The test compares the whole struct against the real registered factory. A nil credential reaches the factory as a nil slot, which keeps the same ambient fallback as SetCredential(nil).
  • GCP: also identical, with one difference. The legacy constructor stored the caller's ctx while the registry factory stores context.Background(), but the GCP provider never reads that field.
  • One deliberate difference: NewGCP rejects an empty project ID (and a nil token source) before calling the registry. The registry's GCP factory resolves an empty project ID with a live getDefaultProject lookup. The direct constructor never made that lookup, and it could pin the provider to a project other than the account's. The scheduler refuses an unpinned Azure account for the same reason.
    • Before: an account with credentials but an empty gcp_project_id got a provider for projects/ that failed on first use.
    • Now: the account fails at provider construction, with create provider: providerfactory: GCP project ID is required. On the purchase path the message is credentials: create GCP provider for account ….
    • It fails in both cases, and no purchase can happen in either.

Refs #41

Verification

Unit and fixture evidence only. The tests use fake credentials and token sources. No test calls a real cloud API, and constructing the providers makes no network calls.

export PATH=$HOME/.local/go/bin:$HOME/go/bin:$HOME/.local/bin:$PATH
export AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true
go build ./... && go vet ./... && go vet -tags integration ./...                       # OK
go test -count=1 ./internal/providerfactory/                                            # ok
go test -count=1 ./internal/purchase/                                                   # ok (32.6s)
go test -count=1 ./internal/scheduler/                                                  # ok
go test -count=1 ./internal/mocks/                                                      # ok
golangci-lint (v2.10.1) run --allow-parallel-runners ./internal/providerfactory/ ./internal/purchase/ ./internal/scheduler/   # 0 issues
gocyclo -over 10 -ignore "_test\.go" internal/providerfactory/ internal/purchase/execution.go internal/scheduler/scheduler.go   # none over 10

Tests added (internal/providerfactory/):

  • TestNewAzure_PassesTypedConfigToRegistry and TestNewGCP_PassesTypedConfigToRegistry use a private registry with recording fake factories. They check that the factory is called once with Name, the typed ID field and the same credential instance (assert.Same). They also check that Profile and ProviderOverride are unset.
  • TestNewAzure_NilCredentialLeavesSlotNil, TestNewGCP_RejectsMissingInputsBeforeRegistry (the factory is never called) and TestBuild_PropagatesFactoryErrorsAndNilProviders.
  • TestNewAzure_MatchesLegacyConstruction and TestNewGCP_MatchesLegacyConstruction build the provider both ways with the real registered factories and compare the whole structs. The Azure test first calls InvalidateAccountsCache() on the new provider, to match the cache-generation bump that legacy SetCredential makes. Negative cases (a different credential, subscription or project) show the comparison isn't vacuous.
  • TestNoRegistryBypassInProductionCode is an AST guard. It fails if any non-test .go file under cmd/ or internal/ refers to the concrete Azure/GCP constructors or puts a Profile key in a provider.ProviderConfig literal.

Fail-before / pass-after:

  • Guard test: run against main's execution.go and scheduler.go, it fails with 6 violations:

    • execution.go:549 NewAzureProvider + Profile
    • execution.go:583 NewProviderWithCredentials
    • scheduler.go:912 NewAzureProvider + Profile
    • scheduler.go:980 NewProviderWithCredentials

    It passes on this branch.

  • Mutation probes on the helper: each of these makes at least one test fail, and the equivalence tests catch all of them except the Profile swap:

    • dropping the Azure credential
    • dropping the GCP token source
    • changing the GCP project ID
    • setting Profile instead of AzureSubscriptionID

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 (left for follow-ups under #41)

  • internal/api/handler_ri_exchange.go still imports the concrete AWS/Azure packages (providers/aws, providers/aws/recommendations, providers/aws/services/ec2, providers/azure/services/compute). Moving exchange operations onto a capability interface, or into one composition file, is a bigger change. Several in-flight branches also touch that file, so it's deferred.
  • The deprecated ProviderConfig.Profile field itself lives in cloud-commitments-go (pkg/provider/interface.go). Removing it, along with the resolveAzureSubscriptionID/resolveGCPProjectID fallbacks, is a go-side change. The platform no longer sets the field, so that change is unblocked here.
  • purchase still passes the built provider as ProviderConfig.ProviderOverride into CreateAndValidateProvider, as before. Passing the typed config straight to the factory there would move construction to purchase time, so it is left unchanged.

Maintainer: please mirror the issue's triage labels on this PR: impact/internal, type/chore, triaged, priority/p3, severity/low, urgency/eventually, effort/m.

🤖 Generated with Claude Code

…e registry

The purchase and scheduler credentialed paths called the concrete Azure
and GCP constructors directly, and the Azure path pinned the subscription
through the deprecated ProviderConfig.Profile field. Add a small
internal/providerfactory package that builds these providers through the
cloud-commitments-go provider registry, using the typed
AzureSubscriptionID/GCPProjectID fields and the AzureTokenCredential/
GCPTokenSource credential slots, and use it at those four call sites.

The registry-built providers are identical to the legacy ones (tests
compare them field by field against the real registered factories). One
deliberate difference: NewGCP rejects an empty project ID instead of
letting the registry's GCP factory ask the cloud for the credential's
default project.

A guard test fails if any non-test file under cmd/ or internal/ calls the
concrete constructors again or sets ProviderConfig.Profile.

The RI exchange handler's concrete AWS/Azure imports are left for a
follow-up.

Refs LeanerCloud#41

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added impact/internal Team-internal only type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline effort/m Days labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant