Skip to content

feat(azure): multi-subscription enumeration parity with AWS Organizations (refs #473) - #591

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/azure-multi-subscription
May 20, 2026
Merged

feat(azure): multi-subscription enumeration parity with AWS Organizations (refs #473)#591
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/azure-multi-subscription

Conversation

@cristim

@cristim cristim commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Fix GetAccounts IsDefault field: was always false; now set to true for the subscription matching (in priority order) the explicit config, AZURE_SUBSCRIPTION_ID env var, or the sole visible subscription -- matching AWS behaviour where the STS-identified account is always the default.
  • Fix GetServiceClient / GetRecommendationsClient fallback: both methods previously picked accounts[0] silently; they now use the default-marked subscription via getDefaultSubscriptionID.
  • Add GetServiceClientForAccount(ctx, service, region, subscriptionID): enables callers that already have an account from GetAccounts to obtain a service client per subscription without an extra round-trip -- the building block for fan-out across all subscriptions.
  • Add GetRecommendationsClientForAccount(ctx, subscriptionID): same pattern for recommendations clients.
  • Extract newServiceClientForSubscription: removes the duplicated switch service {} block that previously appeared in GetServiceClient and would have been repeated in GetServiceClientForAccount.
  • 27 new tests: unit tests for resolveDefaultSubscription, integration tests for IsDefault correctness through GetAccounts, and happy/error-path tests for both new methods.

Test plan

  • go test ./providers/azure/... -- 379 tests pass (was 352; 27 new)
  • Existing tests unchanged: no API surface regressions
  • GetServiceClientForAccount with empty subscriptionID returns an error
  • GetRecommendationsClientForAccount with empty subscriptionID returns an error
  • Single-subscription tenant: sole subscription is IsDefault: true
  • Multi-subscription tenant with explicit config: correct subscription is IsDefault: true, others are false

Relation to #473

This closes the "multi-account" parity gap row in the issue's table. The remaining gaps (Savings Plans, Redshift/Synapse, MemoryDB, convertible RI exchange, RI utilisation reporting) are tracked in #473 and will land in separate PRs.

Summary by CodeRabbit

  • New Features

    • Added explicit methods to create Azure service and recommendations clients for a specified subscription.
  • Improvements

    • Standardized default Azure subscription selection with ordered fallbacks (config, env, single-subscription) and applied it consistently for region lookup and client creation.
  • Tests

    • Added unit tests covering default-subscription resolution, account default flags, and the new client-creation behaviors.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@cristim has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c95f8aa9-9ce3-4895-bf14-2f3e9cdc4d52

📥 Commits

Reviewing files that changed from the base of the PR and between f1d5ef6 and 2596041.

📒 Files selected for processing (2)
  • providers/azure/provider.go
  • providers/azure/provider_test.go
📝 Walkthrough

Walkthrough

The PR makes Azure subscription default selection deterministic (config, env, single-subscription fallback), applies it to region lookups and client creation, and adds explicit-account client constructors plus unit tests validating resolution and client behavior.

Changes

Azure Subscription Default Selection and Account-Specific Clients

Layer / File(s) Summary
Default subscription resolution core logic
providers/azure/provider.go
Import os for env variables, document default subscription priority in GetAccounts, initialize discovered accounts with IsDefault:false, implement resolveDefaultSubscription and getDefaultSubscriptionID, and use the computed default for GetRegions.
Service client construction for explicit subscriptions
providers/azure/provider.go
Refactor GetServiceClient to resolve and use getDefaultSubscriptionID when p.subscriptionID is unset, delegate to a shared newServiceClientForSubscription, and add GetServiceClientForAccount which validates non-empty subscriptionID.
Recommendations client construction for explicit subscriptions
providers/azure/provider.go
Refactor GetRecommendationsClient to default via getDefaultSubscriptionID and add GetRecommendationsClientForAccount for explicit non-empty subscriptionID.
Test helpers and default resolution validation
providers/azure/provider_test.go
Add makeSubscriptionsPager test helper; add TestResolveDefaultSubscription, TestAzureProvider_GetAccounts_IsDefault, and TestGetDefaultSubscriptionID covering empty/single/multiple subscriptions and config/env scenarios.
Account-specific client construction tests
providers/azure/provider_test.go
Add TestAzureProvider_GetServiceClientForAccount and TestAzureProvider_GetRecommendationsClientForAccount validating success for valid explicit subscription IDs, errors for empty IDs, and "Azure is not configured" when credentials are missing.

Sequence Diagram

sequenceDiagram
  participant Caller as Caller
  participant GetAccounts as GetAccounts
  participant AzureAPI as Azure API
  participant resolveDefault as resolveDefaultSubscription
  participant GetRegions as GetRegions
  participant GetServiceClient as GetServiceClient
  Caller->>GetAccounts: GetAccounts(ctx)
  GetAccounts->>AzureAPI: list subscriptions
  AzureAPI-->>GetAccounts: subscriptions list
  GetAccounts->>resolveDefault: resolveDefaultSubscription(subscriptions, config, env)
  resolveDefault-->>GetAccounts: accounts with IsDefault set
  Caller->>GetRegions: GetRegions(ctx, accounts)
  GetRegions->>GetRegions: getDefaultSubscriptionID(accounts)
  GetRegions->>AzureAPI: query locations for default subscription
  AzureAPI-->>GetRegions: locations list
  Caller->>GetServiceClient: GetServiceClient(ctx, service, region)
  GetServiceClient->>GetAccounts: GetAccounts(ctx) (if needed)
  GetServiceClient->>GetServiceClient: getDefaultSubscriptionID(accounts)
  GetServiceClient->>AzureAPI: create service client for subscription
  AzureAPI-->>GetServiceClient: service client
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #553: Implements multi-subscription enumeration and per-subscription client initialization matching the issue's objectives.

Suggested labels

effort/m

Poem

🐇 I nibbled env vars in morning light,
I found the default subscription just right.
Explicit accounts now sing in tune,
Regions and clients hum under the moon.
Hooray — one rabbit, one azure rune!

🚥 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: adding multi-subscription enumeration parity with AWS Organizations by fixing the IsDefault field behavior and adding new account-specific client methods.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/azure-multi-subscription

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

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

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
providers/azure/provider.go (1)

322-332: 💤 Low value

Consider adding a defensive guard for empty accounts slice.

While all current callers check len(accounts) == 0 before invoking this helper, line 331 will panic if accounts is empty and no default is marked. Adding a guard makes the function safer for future callers.

🛡️ Defensive guard
 func getDefaultSubscriptionID(accounts []common.Account) string {
+	if len(accounts) == 0 {
+		return ""
+	}
 	for _, a := range accounts {
 		if a.IsDefault {
 			return a.ID
 		}
 	}
 	return accounts[0].ID
 }
🤖 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/provider.go` around lines 322 - 332, getDefaultSubscriptionID
currently assumes accounts is non-empty and will panic when accounts == nil or
len(accounts) == 0; add a defensive guard at the top of getDefaultSubscriptionID
to return an empty string when the slice is empty (e.g., if len(accounts) == 0 {
return "" }) so callers won't panic, and keep the existing loop that returns
a.ID when a.IsDefault or falls back to accounts[0].ID for backward compatibility
when non-empty.
providers/azure/provider_test.go (1)

1040-1097: ⚡ Quick win

Add an explicit env-fallback test for GetAccounts default marking.

Current coverage validates configured ID and no-config paths, but not the AZURE_SUBSCRIPTION_ID fallback path that this PR introduces.

Suggested test case
 func TestAzureProvider_GetAccounts_IsDefault(t *testing.T) {
+	t.Run("env subscriptionID is marked IsDefault when config is empty", func(t *testing.T) {
+		t.Setenv("AZURE_SUBSCRIPTION_ID", "sub-2")
+		p := &AzureProvider{cred: &mockTokenCredential{}}
+		p.SetSubscriptionsClient(&mockSubscriptionsClient{
+			listPagerFunc: func(_ *armsubscriptions.ClientListOptions) SubscriptionsPager {
+				return makeSubscriptionsPager(
+					[]string{"sub-1", "sub-2", "sub-3"},
+					[]string{"Sub 1", "Sub 2", "Sub 3"},
+				)
+			},
+		})
+
+		accounts, err := p.GetAccounts(context.Background())
+		require.NoError(t, err)
+		require.Len(t, accounts, 3)
+		assert.False(t, accounts[0].IsDefault)
+		assert.True(t, accounts[1].IsDefault)
+		assert.False(t, accounts[2].IsDefault)
+	})
🤖 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/provider_test.go` around lines 1040 - 1097, Add a unit test
in TestAzureProvider_GetAccounts_IsDefault that verifies the
AZURE_SUBSCRIPTION_ID env fallback: create an AzureProvider without
subscriptionID, set os.Setenv("AZURE_SUBSCRIPTION_ID","sub-2") (and defer
os.Unsetenv), set the mockSubscriptionsClient using makeSubscriptionsPager with
multiple IDs ["sub-1","sub-2","sub-3"], call p.GetAccounts(ctx) and assert that
only the account with ID "sub-2" has IsDefault true while others are false; use
the same mockTokenCredential, mockSubscriptionsClient, makeSubscriptionsPager
helpers and require/assert patterns as the existing subtests.
🤖 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.

Inline comments:
In `@providers/azure/provider_test.go`:
- Around line 981-987: The helper makeSubscriptionsPager assumes names and ids
are same length causing an index panic; add a guard at the top of
makeSubscriptionsPager that checks len(ids)==len(names) and fail fast with a
clear message (e.g., panic or test.Fatalf) if they differ so callers get an
informative error instead of an opaque index out of range; modify the function
body around makeSubscriptionsPager to perform this validation before the for
loop that references names[i].
- Around line 1040-1097: The tests in TestAzureProvider_GetAccounts_IsDefault
are flaky because GetAccounts reads the AZURE_SUBSCRIPTION_ID env var; update
each subtest to explicitly control that env var before invoking
AzureProvider.GetAccounts (use os.Setenv to set the desired value or os.Unsetenv
to clear it) and restore the original value after the subtest finishes; apply
this change in the three subtests so the AzureProvider (and its
SetSubscriptionsClient/mockSubscriptionsClient) sees a deterministic
subscriptionID for the assertions.

---

Nitpick comments:
In `@providers/azure/provider_test.go`:
- Around line 1040-1097: Add a unit test in
TestAzureProvider_GetAccounts_IsDefault that verifies the AZURE_SUBSCRIPTION_ID
env fallback: create an AzureProvider without subscriptionID, set
os.Setenv("AZURE_SUBSCRIPTION_ID","sub-2") (and defer os.Unsetenv), set the
mockSubscriptionsClient using makeSubscriptionsPager with multiple IDs
["sub-1","sub-2","sub-3"], call p.GetAccounts(ctx) and assert that only the
account with ID "sub-2" has IsDefault true while others are false; use the same
mockTokenCredential, mockSubscriptionsClient, makeSubscriptionsPager helpers and
require/assert patterns as the existing subtests.

In `@providers/azure/provider.go`:
- Around line 322-332: getDefaultSubscriptionID currently assumes accounts is
non-empty and will panic when accounts == nil or len(accounts) == 0; add a
defensive guard at the top of getDefaultSubscriptionID to return an empty string
when the slice is empty (e.g., if len(accounts) == 0 { return "" }) so callers
won't panic, and keep the existing loop that returns a.ID when a.IsDefault or
falls back to accounts[0].ID for backward compatibility when non-empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 08d8d50a-cc76-4df7-a05a-d8df8345683f

📥 Commits

Reviewing files that changed from the base of the PR and between bc8831b and fdf95f4.

📒 Files selected for processing (2)
  • providers/azure/provider.go
  • providers/azure/provider_test.go

Comment thread providers/azure/provider_test.go
Comment thread providers/azure/provider_test.go
cristim added 2 commits May 20, 2026 20:41
…ions (refs #473)

- Fix GetAccounts to set IsDefault on the correct subscription (priority:
  explicit config > AZURE_SUBSCRIPTION_ID env var > sole subscription),
  matching AWS behaviour where the STS-identified account is always default.
- Fix GetServiceClient and GetRecommendationsClient to use the default
  subscription rather than silently picking accounts[0].
- Add GetServiceClientForAccount(ctx, service, region, subscriptionID) so
  callers iterating all subscriptions from GetAccounts can get a service
  client per subscription without triggering an extra GetAccounts round-trip.
- Add GetRecommendationsClientForAccount(ctx, subscriptionID) for the same
  fan-out pattern.
- Extract newServiceClientForSubscription to eliminate the duplicated switch
  across the two GetServiceClient variants.
- Add 27 tests: resolveDefaultSubscription unit tests, IsDefault integration
  tests via GetAccounts, and happy/error-path tests for the two new methods.
- Add defensive guard in getDefaultSubscriptionID for empty accounts
  slice (panic-safe for future callers)
- Guard makeSubscriptionsPager against ids/names length mismatch with
  an explicit panic message instead of an opaque index out of range
- Pin AZURE_SUBSCRIPTION_ID env var in each GetAccounts IsDefault
  subtest so results are deterministic in any shell environment
- Add new subtest covering the env-var fallback path in GetAccounts
  (previously untested via GetAccounts directly)
@cristim
cristim force-pushed the feat/azure-multi-subscription branch from fdf95f4 to f1d5ef6 Compare May 20, 2026 18:41
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit review - addressed findings

All 4 findings actioned in commit f1d5ef6. Summary:

Finding File Verdict Action
makeSubscriptionsPager ids/names length guard provider_test.go:981 Actionable Added panic("makeSubscriptionsPager: ids and names length mismatch") guard before the loop
TestAzureProvider_GetAccounts_IsDefault env flakiness provider_test.go:1040-1097 Actionable Added t.Setenv("AZURE_SUBSCRIPTION_ID", ...) to all 3 subtests to isolate from ambient shell env
Add env-fallback subtest for GetAccounts provider_test.go:1040 (nitpick) Actionable Added new subtest "env subscriptionID is marked IsDefault when config is empty" covering the AZURE_SUBSCRIPTION_ID path through GetAccounts
getDefaultSubscriptionID empty-slice guard provider.go:325 (nitpick) Actionable Added if len(accounts) == 0 { return "" } guard at top of function

Tests: 380 passed, build clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@cristim: Thanks for the thorough follow-up — all four fixes look well-targeted. The panic guard in makeSubscriptionsPager, the t.Setenv isolation, the new env-fallback subtest, and the empty-slice guard in getDefaultSubscriptionID collectively tighten both correctness and test reliability. Triggering a fresh review of commit f1d5ef6 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.

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

Actionable comments posted: 3

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

Inline comments:
In `@providers/azure/provider_test.go`:
- Around line 1029-1040: The tests calling resolveDefaultSubscription(accounts,
"") must be made env-deterministic by ensuring AZURE_SUBSCRIPTION_ID is unset or
controlled during the subtests; update both subtests ("single subscription gets
IsDefault when no explicit ID" and "multiple subscriptions with no explicit ID
stay non-default") to temporarily clear os.Getenv("AZURE_SUBSCRIPTION_ID") (or
set it to empty) for the duration of the test and restore the original value
afterwards so resolveDefaultSubscription sees no inherited subscription id and
assertions on accounts[].IsDefault are deterministic.

In `@providers/azure/provider.go`:
- Around line 425-431: When resolving subscriptionID, don't mask errors from
p.GetAccounts; if p.GetAccounts(ctx) returns a non-nil err, return that error
(or wrap it) instead of converting every failure to "no Azure subscriptions
found"; only return the "no Azure subscriptions found" message when err==nil and
len(accounts)==0 and still use getDefaultSubscriptionID(accounts) when accounts
exist. Apply the same fix to the other lookup block that uses p.GetAccounts(ctx)
(the other occurrence referenced in the review) so auth/API failures are
preserved there as well.
- Around line 322-335: Change getDefaultSubscriptionID to stop returning
accounts[0] when no account is marked default: if no IsDefault is found return
an empty string (keep the existing empty check for len(accounts)==0). Then
update callers (e.g., GetRegions, GetServiceClient, GetRecommendationsClient) to
treat an empty subscription ID as an error and fail fast with a clear message
such as "multiple Azure subscriptions found; set AzureSubscriptionID or
AZURE_SUBSCRIPTION_ID" so the caller surface is deterministic instead of
silently using the first paged account.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4aaf9f1a-3fd4-444a-bb2b-47ea4eeb0f5b

📥 Commits

Reviewing files that changed from the base of the PR and between fdf95f4 and f1d5ef6.

📒 Files selected for processing (2)
  • providers/azure/provider.go
  • providers/azure/provider_test.go

Comment thread providers/azure/provider_test.go
Comment thread providers/azure/provider.go
Comment thread providers/azure/provider.go
Drop the accounts[0] fallback from getDefaultSubscriptionID; return "" when
no account is marked default. Update GetRegions, GetServiceClient, and
GetRecommendationsClient to fail fast with a clear message when the resolved
subscription ID is empty. Preserve GetAccounts errors instead of collapsing
them into "no subscriptions found". Add t.Setenv("AZURE_SUBSCRIPTION_ID", "")
to the two resolveDefaultSubscription subtests that were missing it.
@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.

@cristim
cristim merged commit c16b2dd into feat/multicloud-web-frontend May 20, 2026
5 checks passed
@cristim
cristim deleted the feat/azure-multi-subscription branch June 3, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/many Affects most users 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