Skip to content

feat(providers/azure): enumerate subscriptions in GetServiceClient (closes #553) - #561

Closed
cristim wants to merge 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-553-azure-multi-account
Closed

feat(providers/azure): enumerate subscriptions in GetServiceClient (closes #553)#561
cristim wants to merge 2 commits into
feat/multicloud-web-frontendfrom
feat/issue-553-azure-multi-account

Conversation

@cristim

@cristim cristim commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Cache the ARM subscriptions list in AzureProvider after the first successful GetAccounts call so that GetServiceClient, GetRecommendationsClient, and GetRegions do not each trigger a separate network round-trip in the same request
  • Add MultiSubscriptionRecommendationsClient that fans out recommendation collection across all accessible subscriptions concurrently (errgroup) with per-subscription error isolation
  • Update GetRecommendationsClient to return the multi-subscription client when no subscription is pinned, bringing Azure to parity with the AWS provider which automatically spans the whole organisation

What changed (per file)

providers/azure/provider.go

  • New cachedAccounts / accountsMu fields on AzureProvider for thread-safe subscription caching
  • New getOrFetchAccounts (double-checked-lock cache) and fetchAccountsLocked (ARM API) helpers
  • GetAccounts delegates to getOrFetchAccounts -- identical behaviour, now cache-backed
  • GetServiceClient uses getOrFetchAccounts instead of GetAccounts (avoids duplicate call)
  • GetRecommendationsClient returns MultiSubscriptionRecommendationsClient when 2+ subscriptions are discovered (1-sub path unchanged; pinned-subscription path unchanged)
  • New exported InvalidateAccountsCache() for tests

providers/azure/recommendations.go

  • New MultiSubscriptionRecommendationsClient struct with NewMultiSubscriptionRecommendationsClient constructor
  • Implements provider.RecommendationsClient (GetRecommendations, GetRecommendationsForService, GetAllRecommendations)
  • Fan-out via errgroup; per-subscription errors are logged as warnings and successful results from other subscriptions are still returned; if all subscriptions fail the last error is propagated

providers/azure/provider_test.go

  • TestAzureProvider_GetRecommendationsClient_WithSubscriptionLookup extended with 3 new sub-tests: multi-sub returns *MultiSubscriptionRecommendationsClient; single-sub returns *RecommendationsClientAdapter; pinned subscription always returns single adapter
  • TestAzureProvider_GetAccounts_CacheHit: verifies second GetAccounts call does not invoke the subscriptions API again
  • TestAzureProvider_InvalidateAccountsCache: verifies cache invalidation forces a fresh API call

providers/azure/recommendations_test.go

  • TestNewMultiSubscriptionRecommendationsClient_EmptyAccounts: constructor rejects empty account list
  • TestNewMultiSubscriptionRecommendationsClient_BuildsAdaptersPerAccount: adapter slice has correct length and subscriptionIDs
  • TestMultiSubscriptionRecommendationsClient_AllFail: cancelled context propagates error
  • TestMultiSubscriptionRecommendationsClient_InterfaceCompliance: compile-time interface assertion

AWS-vs-Azure parity narrative

AWS Cost Explorer spans the whole organisation automatically (via AccountScope: Linked). Before this PR, Azure's GetRecommendationsClient would silently use only the first discovered subscription when no subscription was pinned, meaning multi-subscription tenants would see incomplete recommendations. After this PR, GetRecommendationsClient fans out across every accessible subscription the authenticated principal can see, matching the AWS behaviour.

The purchase execution path (internal/purchase/execution.go) and the scheduler (internal/scheduler/scheduler.go) always pin a subscription via ProviderConfig.AzureSubscriptionID, so the multi-sub code path is not triggered there -- those callers already handle per-account fan-out externally.

Test plan

  • go test ./providers/azure/... -count=1 -short passes (113 tests, up from 104)
  • go test ./providers/aws/... -count=1 -short passes (647 tests, no regression)
  • go build ./... succeeds (no downstream compile breaks)
  • Integration: create a service principal with Reader on 2+ subscriptions, set ambient credentials, call GetRecommendationsClient without pinning a subscription -- verify both subscriptions appear in the returned recommendations

Closes #553
Refs #473

Summary by CodeRabbit

  • New Features

    • Multi-subscription recommendations: when no subscription is pinned, recommendations are discovered and aggregated across all accessible Azure subscriptions; a single-subscription mode remains when exactly one subscription is configured or pinned.
  • Performance Improvements

    • Subscription discovery results are cached to reduce redundant API calls; cache can be invalidated to force refresh.
  • Tests

    • Added unit tests covering multi-subscription behavior and caching/invalidation.

Review Change Stack

…ionsClient (closes #553)

GetAccounts now caches the ARM subscriptions list after the first successful
call so that GetServiceClient, GetRecommendationsClient, and GetRegions do not
each trigger a separate network round-trip in the same request. The cache is
keyed to the provider lifetime; InvalidateAccountsCache() clears it for tests
that need a fresh fetch.

GetRecommendationsClient is brought to parity with the AWS provider: when no
subscription is pinned (ProviderConfig.AzureSubscriptionID is empty), all
accessible subscriptions are discovered via GetAccounts and a
MultiSubscriptionRecommendationsClient is returned so recommendations are
collected across every subscription the authenticated principal can see. When
exactly one subscription is found the existing single-adapter path is taken to
avoid unnecessary overhead.

MultiSubscriptionRecommendationsClient fans out GetRecommendations to each
per-subscription RecommendationsClientAdapter concurrently (errgroup), isolates
per-subscription errors (logs warnings, keeps successful results), and
propagates parent context cancellation after the fan-out completes.

GetServiceClient continues to use the first discovered subscription when no
subscription is pinned -- purchase execution (internal/purchase) and the
scheduler (internal/scheduler) already pin a subscription per call via
ProviderConfig.AzureSubscriptionID, so the multi-sub behaviour is not needed
there.

9 new unit tests cover: multi-sub client returned when 2+ subscriptions found,
single adapter returned for exactly 1 subscription, pinned subscription always
returns single adapter, cache hit after second GetAccounts call, cache
invalidation forces re-fetch, constructor rejects empty account list, adapter
count and subscriptionID assignment, all-fail error propagation, and interface
compliance.

Refs #473
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Azure provider now discovers and caches all accessible subscriptions, returning either single- or multi-subscription recommendation clients depending on account count. Multi-subscription recommendations aggregate results across subscriptions concurrently with graceful handling of partial failures.

Changes

Multi-subscription Azure provider support

Layer / File(s) Summary
Subscription cache mechanism
providers/azure/provider.go
AzureProvider adds accountsMu and cachedAccounts fields; InvalidateAccountsCache clears the cache; getOrFetchAccounts implements double-checked locking to fetch subscriptions once from ARM and reuse results; GetAccounts now returns cached accounts instead of fetching on each call.
Multi-subscription recommendations client
providers/azure/recommendations.go
New MultiSubscriptionRecommendationsClient creates one RecommendationsClientAdapter per account, fans out GetRecommendations requests concurrently via errgroup, merges successful results, logs per-subscription warnings, and returns error only if all subscriptions fail. Convenience methods forward to GetRecommendations with service-specific or all-service parameters.
Provider client methods integration
providers/azure/provider.go
GetServiceClient uses cached account discovery (getOrFetchAccounts) as fallback when subscription is unpinned. GetRecommendationsClient discovers all subscriptions via the cache and returns a single-subscription adapter for one account or MultiSubscriptionRecommendationsClient for multiple accounts.
Test coverage for caching and multi-subscription behavior
providers/azure/provider_test.go, providers/azure/recommendations_test.go
Tests verify subscription caching (repeated GetAccounts calls hit cache once, InvalidateAccountsCache forces re-fetch), multi-subscription client validation (rejects empty accounts, creates adapters per account, handles context cancellation), GetRecommendationsClient returns correct client type by subscription count, and interface compliance of multi-subscription client.

Sequence Diagram

sequenceDiagram
  participant Code
  participant GetRecClient as GetRecommendationsClient
  participant Cache as getOrFetchAccounts
  participant ARM as Azure ARM API
  participant MultiClient as MultiSubscriptionRecommendationsClient
  participant ErrGroup as concurrent adapters
  Code->>GetRecClient: call (no subscription pinned)
  GetRecClient->>Cache: fetch all subscriptions
  Cache->>Cache: check local cache
  alt cache hit
    Cache-->>GetRecClient: return cached accounts
  else cache miss
    Cache->>ARM: list subscriptions
    ARM-->>Cache: subscription list
    Cache->>Cache: store in cache
    Cache-->>GetRecClient: return accounts
  end
  alt single account
    GetRecClient-->>Code: return RecommendationsClientAdapter
  else multiple accounts
    GetRecClient->>MultiClient: NewMultiSubscriptionRecommendationsClient
    MultiClient->>ErrGroup: fan out GetRecommendations per adapter
    ErrGroup-->>MultiClient: results from all subscriptions
    MultiClient-->>GetRecClient: aggregated recommendations
    GetRecClient-->>Code: return MultiSubscriptionRecommendationsClient
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

urgency/this-sprint, impact/many, effort/m

🐰 A cache to speed the Azure way,
Where subscriptions dance the day,
Many accounts, one grand request,
Concurrent hops—the multi-sub quest!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: subscription enumeration in GetServiceClient with caching added to the Azure provider.
Linked Issues check ✅ Passed Code changes fully implement issue #553 acceptance criteria: GetAccounts returns cached subscriptions, service clients use enumerated subscriptions, MultiSubscriptionRecommendationsClient provides parallelized collection, and unit tests verify functionality.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #553 scope: subscription discovery caching, multi-subscription client, service client updates, and corresponding test coverage with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-553-azure-multi-account

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

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/all-users Affects every user effort/l Weeks type/feat New capability triaged Item has been triaged labels May 20, 2026
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
providers/azure/recommendations_test.go (1)

402-421: 💤 Low value

Clarify test name to match implementation.

The test name suggests it verifies "all sub-adapters fail during fan-out," but the implementation uses a pre-cancelled context to trigger the parent context error check (lines 418-420 acknowledge this). These are distinct code paths: testing context cancellation vs. testing the error-aggregation logic when all per-subscription API calls fail but the context remains valid.

Consider renaming to TestMultiSubscriptionRecommendationsClient_CancelledContext or similar to accurately reflect what the test covers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/azure/recommendations_test.go` around lines 402 - 421, Rename the
test TestMultiSubscriptionRecommendationsClient_AllFail to reflect it exercises
a pre-cancelled context path (e.g.,
TestMultiSubscriptionRecommendationsClient_CancelledContext) and update its
comment/description accordingly; locate the test function named
TestMultiSubscriptionRecommendationsClient_AllFail (which constructs accounts,
calls NewMultiSubscriptionRecommendationsClient and invokes
GetAllRecommendations with a cancelled ctx) and change the function name and top
comment to indicate "cancelled context" behavior so the name matches the
implementation and intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@providers/azure/recommendations_test.go`:
- Around line 402-421: Rename the test
TestMultiSubscriptionRecommendationsClient_AllFail to reflect it exercises a
pre-cancelled context path (e.g.,
TestMultiSubscriptionRecommendationsClient_CancelledContext) and update its
comment/description accordingly; locate the test function named
TestMultiSubscriptionRecommendationsClient_AllFail (which constructs accounts,
calls NewMultiSubscriptionRecommendationsClient and invokes
GetAllRecommendations with a cancelled ctx) and change the function name and top
comment to indicate "cancelled context" behavior so the name matches the
implementation and intent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5559bd50-4402-452e-93ea-9bc28cfe18e5

📥 Commits

Reviewing files that changed from the base of the PR and between 2a38e25 and 4f509d3.

📒 Files selected for processing (4)
  • providers/azure/provider.go
  • providers/azure/provider_test.go
  • providers/azure/recommendations.go
  • providers/azure/recommendations_test.go

Rename TestMultiSubscriptionRecommendationsClient_AllFail to
TestMultiSubscriptionRecommendationsClient_CancelledContext and update the
comment to accurately reflect that the test exercises a pre-cancelled context
path, not the "all adapters fail" error-aggregation scenario.

This clarifies the test's intent and prevents confusion about which code path
is actually being exercised.

Addresses CodeRabbit nitpick on PR #561.
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit Review Response

Nitpick addressed:

providers/azure/recommendations_test.go: Renamed TestMultiSubscriptionRecommendationsClient_AllFail to TestMultiSubscriptionRecommendationsClient_CancelledContext with updated comment to accurately reflect the test's focus on cancelled-context behavior rather than adapter failure aggregation.

Verification:

  • Test suite passes: go test ./... → 355 passed
  • Build succeeds: go build ./...
  • Commit: c1b17e521

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the clean follow-up! The rename from TestMultiSubscriptionRecommendationsClient_AllFail to TestMultiSubscriptionRecommendationsClient_CancelledContext is a good improvement — the new name accurately describes what the test actually exercises. I'll kick off a fresh review of the latest commit now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
providers/azure/recommendations_test.go (1)

417-421: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the specific cancellation error, not just any error.

At Line 420, require.Error(t, err) is too broad for this test’s stated intent. Please also assert context.Canceled so the test fails on unrelated errors.

Suggested patch
 	_, err = client.GetAllRecommendations(ctx)
 	// With a cancelled context GetRecommendations propagates context.Canceled
 	// before the sub-adapter fan-out error-accounting path runs.
 	require.Error(t, err)
+	assert.ErrorIs(t, err, context.Canceled)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@providers/azure/recommendations_test.go` around lines 417 - 421, The test
currently asserts any error from client.GetAllRecommendations(ctx) but intends
to ensure the context cancellation is propagated; replace the broad
require.Error(t, err) check with an assertion that the error is specifically
context.Canceled (e.g., using require.ErrorIs or errors.Is) so the test fails on
unrelated errors; update the assertion near the call to GetAllRecommendations in
the test to assert errors.Is(err, context.Canceled) or require.ErrorIs(t, err,
context.Canceled).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@providers/azure/recommendations_test.go`:
- Around line 417-421: The test currently asserts any error from
client.GetAllRecommendations(ctx) but intends to ensure the context cancellation
is propagated; replace the broad require.Error(t, err) check with an assertion
that the error is specifically context.Canceled (e.g., using require.ErrorIs or
errors.Is) so the test fails on unrelated errors; update the assertion near the
call to GetAllRecommendations in the test to assert errors.Is(err,
context.Canceled) or require.ErrorIs(t, err, context.Canceled).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c6deb81-bbcf-4e41-94ba-8061d5528de1

📥 Commits

Reviewing files that changed from the base of the PR and between 4f509d3 and c1b17e5.

📒 Files selected for processing (1)
  • providers/azure/recommendations_test.go

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

Superseded by PR #591 (merged: feat(azure): multi-subscription enumeration parity with AWS Organizations). PR #591 shipped GetServiceClientForAccount + GetRecommendationsClientForAccount fan-out building blocks for the same #553 ask. The subscription-list caching this PR adds is a useful enhancement but the conflict surface is too large to rebase cleanly. If caching is still needed, file a follow-up issue scoped to just the cache layer on top of the already-merged GetAccounts.

@cristim cristim closed this May 20, 2026
@cristim
cristim deleted the feat/issue-553-azure-multi-account branch June 3, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/all-users Affects every user priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant