Skip to content

Pin AKS credential pipeline to the deployment subscription - #19219

Merged
Mitch Denny (mitchdenny) merged 7 commits into
mainfrom
mitchdenny-fix-aks-credential-subscription
Aug 12, 2026
Merged

Mitch Denny (mitchdenny) merged 7 commits into
mainfrom
mitchdenny-fix-aks-credential-subscription

Conversation

@mitchdenny

@mitchdenny Mitch Denny (mitchdenny) commented Aug 11, 2026

Copy link
Copy Markdown
Member

Description

When deploying an AKS environment, the post-provisioning step that fetches cluster credentials invoked the Azure CLI without --subscription. Both az aks get-credentials and the az resource list resource-group fallback therefore ran against whatever subscription the ambient az CLI happened to default to, rather than the subscription Aspire selected for the deployment.

This is easy to hit in practice: anyone whose az account show default differs from the subscription they deploy to (multi-subscription tenants, CI agents, or after an az account set elsewhere) would either get a confusing "cluster not found" failure or, worse, silently pull a kubeconfig for a same-named cluster in a different subscription and deploy into it.

The resource-group lookup compounded the problem. It read IConfiguration["Azure:ResourceGroup"], which is only populated at host startup. Provisioning writes the resource group into deployment state during the pipeline run, so on a first deploy that config value is empty — the fallback az resource list query always fired, and it was the unscoped query.

What changed for users

  • aspire deploy against an AKS environment now always targets the subscription Aspire selected, regardless of the ambient az CLI default. No more wrong-subscription credential fetches or spurious "cluster not found" errors on first deploy.

  • The deploy summary's reconnect command is now copy-pasteable on a machine with a different CLI default:

    🔑 Connect to cluster
    az aks get-credentials --resource-group <rg> --name <cluster> --subscription <subscription-id>
    
  • If the Azure subscription genuinely cannot be resolved from deployment state, the step now fails fast with an actionable message instead of silently falling back to the ambient CLI context:

    Could not resolve the Azure subscription selected for deployment. Ensure Azure
    provisioning has completed, or set the Azure:SubscriptionId configuration value.
    

    In practice this should not surface during a normal deploy: the aks-get-credentials-{name} step declares DependsOnSteps = [AzureEnvironmentResource.ProvisionInfrastructureStepName], so provisioning (which persists SubscriptionId) always completes first.

Implementation

GetAzureDeploymentContextAsync reads SubscriptionId/ResourceGroup from the Azure deployment-state section via IDeploymentStateManager rather than IConfiguration. This is the key correctness point: SaveSectionAsync updates the same in-memory _state that AcquireSectionAsync reads back, so the values are current even on a first deploy when nothing has been written to disk-backed configuration yet. Both Azure CLI call sites then pass --subscription explicitly, and the subscription ID goes through the existing ValidateAzureResourceName defense-in-depth check before being embedded in a command line.

The az command runner is injected into GetResourceGroupAsync and the new FetchKubeConfigAsync so tests can assert the exact command lines without spawning the CLI. This seam is load-bearing rather than cosmetic — see the testing note below.

Testing

Unit tests in tests/Aspire.Hosting.Azure.Kubernetes.Tests cover:

  • Reading subscription/resource group from current deployment state.
  • Failing with an actionable message when the subscription is absent.
  • The saved-resource-group path short-circuiting without invoking az at all.
  • The resource-group fallback query being scoped to the deployment subscription.
  • The credential fetch being scoped to the deployment subscription.
  • az failure surfacing as an InvalidOperationException.

An earlier revision of these tests asserted only the output of the argument-builder helpers. That was verified to be inadequate: reverting the --subscription fix at the call site left the entire suite green, because nothing proved the pipeline actually called those helpers. The tests now drive the real call paths through an injected runner and assert the captured command lines, so a regression that drops --subscription fails the suite.

Aspire.Hosting.Azure.Kubernetes builds with 0 warnings; 73/73 tests pass.

Security considerations

This change affects how cluster credentials are acquired, so calling it out for reviewer awareness:

  • Net positive on scoping. Pinning --subscription removes an ambient-authority failure mode where a kubeconfig could be fetched from an unintended subscription that happens to contain a same-named cluster.
  • Command construction. The subscription ID is read from persisted deployment state and embedded in an az argument string. It is validated with the pre-existing ValidateAzureResourceName regex (^[a-zA-Z0-9\-_\.\(\)]+$) before use, matching how the cluster name and resource group were already handled.
  • No change to credential storage. Kubeconfig content is still fetched to stdout via --file - and written by Aspire to a temp file with owner-only (0600) permissions on Unix; that behavior is untouched.
  • Subscription IDs are identifiers rather than secrets, and are already logged elsewhere during provisioning, so including one in the deploy summary does not expose new sensitive data.

Fixes #19216

Checklist

  • Is this feature complete?

    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?

    • Yes
    • No
  • Did you add public API?

    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No

    AzureBicepResourceScope.HasResourceGroup changes from internal to public. AzureBicepResourceScope is already a public sealed type and AzureBicepResource.Scope is already public, but ResourceGroup is a public property that throws for subscription- and tenant-scoped resources, with no public way to test the scope first. This closes that guard gap. Flagging for API review.

  • Does the change make any security assumptions or guarantees?

    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Copilot AI balanced review requested due to automatic review settings August 11, 2026 04:09
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19219

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19219"

@github-actions github-actions Bot added the area-integrations Issues pertaining to Aspire Integrations packages label Aug 11, 2026

Copilot AI 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.

Pull request overview

Pins AKS credential acquisition and resource-group discovery to Aspire’s deployment subscription.

Changes:

  • Reads Azure context from deployment state.
  • Adds --subscription to Azure CLI calls and reconnect output.
  • Adds focused unit tests with an injected CLI runner.
Show a summary per file
File Description
AzureKubernetesEnvironmentResource.AksPipeline.cs Scopes AKS CLI operations to the deployment subscription.
AzureKubernetesInfrastructureTests.cs Tests deployment-state lookup and CLI arguments.
Aspire.Hosting.Azure.Kubernetes.Tests.csproj Includes the shared in-memory state manager.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

@github-actions

This comment has been minimized.

@mitchdenny

Copy link
Copy Markdown
Member Author

PR Testing Report

PR Information

Artifact Version Verification

  • Expected Commit: 7ac00b8d1f71b5ae4995104b5990ea230e83b087
  • Dogfood CLI reports: 13.6.0-pr.19219.g7ac00b8d
  • Source checkout: local HEAD == origin/mitchdenny-fix-aks-credential-subscription == PR headRefOid, clean working tree
  • Status: ✅ Verified (SHA 7ac00b8d present in the artifact version stamp)

Changes Analyzed

Files Changed

  • src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs (+106/−31)
  • tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesInfrastructureTests.cs (+135)
  • tests/Aspire.Hosting.Azure.Kubernetes.Tests/Aspire.Hosting.Azure.Kubernetes.Tests.csproj (+1)

Change Categories

  • Hosting integration changes (Azure Kubernetes / AKS deploy pipeline)
  • Test changes
  • CLI changes
  • Dashboard changes
  • Template changes
  • Client/Component changes
  • VS Code extension changes
  • CI infrastructure changes

Test Scenarios Executed

Scenario 1: Dogfood artifact version verification

Objective: Confirm the tested bits are the PR's bits.
Coverage Type: Happy path
Status: ✅ Passed

Installed via the PR dogfood script into an isolated --install-path (with --skip-path --skip-extension).

$ .../dogfood/pr-19219/bin/aspire --version
13.6.0-pr.19219.g7ac00b8d

Source checkout independently confirmed at the same SHA with a clean tree.


Scenario 2: The fix is present in the shipped NuGet package

Objective: Verify the fix actually compiled into the artifact users would consume, not just the source tree.
Coverage Type: Artifact verification
Status: ✅ Passed

Extracted Aspire.Hosting.Azure.Kubernetes.13.6.0-pr.19219.g7ac00b8d.nupkg from the PR hive and inspected lib/net8.0/Aspire.Hosting.Azure.Kubernetes.dll (246,784 bytes) for UTF-16 string literals:

Literal Present
aks get-credentials
--subscription
resource list --resource-type
Could not resolve the Azure subscription

The shipped composite format string is:

aks get-credentials --resource-group "{0}" --name "{1}" --file - --subscription "{2}"

Scenario 3: Full test suite at PR head

Objective: Confirm the suite passes on exactly the PR commit.
Coverage Type: Happy path
Status: ✅ Passed

Test run summary: Passed!
  total: 73   failed: 0   succeeded: 73   skipped: 0

Cross-platform coverage came from this PR's own CI run (31457579207), where
Hosting.Azure.Kubernetes passed on both ubuntu-latest and windows-latest.


Scenario 4: Real az CLI contract validation

Objective: Prove the generated argument strings are actually valid az invocations, and that
--subscription genuinely overrides the ambient CLI default. Unit tests assert string equality and
structurally cannot catch a malformed-but-well-formatted command line.
Coverage Type: Happy path / integration
Status: ✅ Passed

Environment: az 2.86.0, authenticated, ambient default subscription 731ff15d-… ("Subscription 1"), 151 subscriptions accessible.

4a — the core claim, demonstrated against live Azure. Same command, with and without the flag:

# (A) no --subscription  == PRE-FIX behavior
$ az resource list --query "[0].id" -o tsv
/subscriptions/731ff15d-4cfe-4c85-b779-e6f988b090ae/resourceGroups/NetworkWatcherRG/...

# (B) explicit --subscription  == POST-FIX behavior
$ az resource list --query "[0].id" -o tsv --subscription 9ab12ed0-87cd-4402-9e18-9b67142c6c30
/subscriptions/9ab12ed0-87cd-4402-9e18-9b67142c6c30/resourceGroups/NetworkWatcherRG/...

The returned resource IDs carry different subscription GUIDs. This is precisely the reported bug (A
silently targets the ambient default) and the fix (B targets the subscription Aspire selected).

4b — the exact generated resource-group query parses, including the unquoted --query [0].resourceGroup:

$ az resource list --resource-type Microsoft.ContainerService/managedClusters \
    --name "no-such-cluster" --query [0].resourceGroup -o tsv --subscription "<sub>"
# exit=0, empty result (expected: no such cluster)

4c — the exact generated get-credentials command parses and reaches the control plane:

$ az aks get-credentials --resource-group "no-such-rg-aspire-test" --name "no-such-cluster" \
    --file - --subscription "<sub>"
ERROR: (ResourceGroupNotFound) Resource group 'no-such-rg-aspire-test' could not be found.

Parse-error check (unrecognized arguments / invalid choice / expected one argument): 0 matches.
ResourceGroupNotFound is a semantic error, which proves az authenticated and queried the
explicitly targeted subscription rather than rejecting the argument shape.


Scenario 5: Missing subscription in deployment state (unhappy path)

Objective: Verify a missing subscription fails fast and actionably instead of silently falling back to the ambient CLI context.
Coverage Type: Unhappy path
Status: ✅ Passed

AzureDeploymentContextRequiresSubscription passes. The step throws:

Could not resolve the Azure subscription selected for deployment. Ensure Azure
provisioning has completed, or set the Azure:SubscriptionId configuration value.

Expected Unhappy-Path Outcome: actionable InvalidOperationException naming Azure:SubscriptionId; no silent ambient fallback. Confirmed.


Scenario 6: Azure CLI failure (unhappy path)

Objective: Verify a non-zero az exit is surfaced rather than swallowed.
Coverage Type: Unhappy path
Status: ✅ Passed

FetchKubeConfigThrowsWhenAzureCliFails passes — the failure surfaces as InvalidOperationException.

Also verified GetResourceGroupUsesDeploymentStateWithoutQueryingAzure: when the resource group is
already in deployment state, no az process is spawned at all (boundary case — the fallback query
must not fire unnecessarily).


Scenario 7: Anti-tautology regression proof

Objective: Prove the new tests actually fail when the fix is removed. (An earlier revision of these
tests asserted only argument-builder output and left the suite green with the fix reverted.)
Coverage Type: Negative / test-quality validation
Status: ✅ Passed

Injected a subtle, compiling regression — dropped --subscription from BuildGetCredentialsArguments
while keeping the parameter "used" via _ = subscriptionId; so the IDE0060 warnings-as-errors guard
would not trip:

failed Aspire.Hosting.Azure.Tests.AzureKubernetesInfrastructureTests.FetchKubeConfigIsScopedToDeploymentSubscription
  Assert.Equal() Failure: Collections differ at index 0
  Expected: ···""deployment-aks" --file - --subscription "00000000"···
  Actual:   ···""deployment-aks" --file -"

Test run summary: Failed!   total: 73   failed: 1

The correct, specific test caught it. Source was restored from backup; tree verified clean at PR head.

Summary

Scenario Status Notes
1. Artifact version verification ✅ Passed 13.6.0-pr.19219.g7ac00b8d7ac00b8d1f
2. Fix present in shipped nupkg ✅ Passed Format string + new error message in shipped DLL
3. Full suite at PR head ✅ Passed 73/73 local; CI green on ubuntu + windows
4. Real az contract validation ✅ Passed Retargeting proven live; 0 parse errors
5. Missing subscription (unhappy) ✅ Passed Fails fast, actionable message
6. az failure (unhappy) ✅ Passed Surfaced as InvalidOperationException
7. Anti-tautology regression proof ✅ Passed Correct test fails on subtle regression

Overall Result

✅ PR VERIFIED

Known limitations

  • No live aspire deploy to a real AKS cluster was performed. Provisioning an AKS cluster is slow
    and costly, and GetAksCredentialsAsync is only reachable after ProvisionInfrastructureStepName
    completes with real Bicep outputs. The ~15 lines inside GetAksCredentialsAsync that wire the
    resolved subscription into GetResourceGroupAsync / FetchKubeConfigAsync are therefore covered by
    unit tests plus code inspection, not by a live deploy.
  • Scenario 4a demonstrates the retargeting mechanism using az resource list generically, because no
    AKS cluster was available in any accessible subscription. The command and the --subscription global
    argument are the same ones the fix emits, so the result transfers, but it is not an AKS-specific run.
  • Existing AKS deployment E2E tests authenticate with an ambient subscription that already matches the
    deployment subscription, so they structurally cannot observe this bug. Closing that gap needs a second
    subscription in CI and is worth tracking as follow-up.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny

Copy link
Copy Markdown
Member Author

Addendum: Real AKS Cluster Validation

The earlier test report noted one residual gap: no real AKS cluster was provisioned, so aks-get-credentials could only ever be observed failing. That gap is now closed.

The discriminating setup

The reason the existing 12 AKS deployment E2E tests structurally cannot catch this bug is that they authenticate with an ambient az subscription that matches the deployment subscription — so the missing --subscription flag is invisible. To make this test actually discriminating, the ambient CLI default was deliberately pointed at a decoy subscription:

Role Subscription Clusters present
Deploy target (AZURE__SUBSCRIPTIONID) 731ff15d… ("Subscription 1") yes
Ambient az default (decoy) 89f4306d… ("Subscription 2") zero

Both in tenant 57c90776…. If the fix doesn't work, the CLI resolves against the decoy and finds nothing.

Test 1 — A/B against a pre-existing real cluster

Against a real running cluster (aks-7dtc7wxxlqdry), with ambient set to the decoy:

Variant Exit Output
A — without --subscription (pre-fix behavior) 3 ResourceGroupNotFound, 0 bytes
B — with --subscription (post-fix behavior) 0 9,716 bytes of kubeconfig

The variant-B kubeconfig was verified genuinely usable — kubectl get nodes returned 2 live Ready nodes against API server aks-dns-4b6m3or2.hcp.westus3.azmk8s.io.

Test 2 — Full aspire deploy provisioning a real cluster

Dogfood CLI 13.6.0-pr.19219.g7ac00b8d (matches PR head 7ac00b8d), aspire-starter app + AddAzureKubernetesEnvironment, --clear-cache to reproduce the issue's "first deploy, empty config" trigger.

✓ Pipeline succeeded — provisioned ACR + AKS cluster aks-cu4gipacx6qkk into the target subscription while ambient still pointed at the decoy.

The credential step, verbatim from the debug log:

14:48:46 (aks-get-credentials-aks) → Fetching AKS credentials for aks
14:48:46 [INF] Fetching AKS credentials: cluster=aks-cu4gipacx6qkk, resourceGroup=aspire-aks-sub-e2e-20260811143418
14:48:46 [DBG] Running: /opt/homebrew/bin/az aks get-credentials \
                 --resource-group "aspire-aks-sub-e2e-20260811143418" \
                 --name "aks-cu4gipacx6qkk" --file - \
                 --subscription "731ff15d-4cfe-4c85-b779-e6f988b090ae"
14:48:48 [INF] AKS credentials written to .../kubeconfig
14:48:48 ✓ AKS credentials fetched for cluster aks-cu4gipacx6qkk (1.2s)

--subscription is pinned to the deployment subscription, not the ambient decoy. And the credentials were not merely fetched — Helm deployed through them successfully:

NAME                                        READY   STATUS    RESTARTS   AGE
aks-dashboard-deployment-6c8b4f6bf6-hsmrm   1/1     Running   0          109s
apiservice-deployment-798f8d958-l84qx       1/1     Running   0          109s
webfrontend-deployment-6645b55f6b-cbtgh     1/1     Running   0          109s

Third user-facing fix worth calling out

Beyond the two az call sites, the PR also fixes the copy-paste hint in the deploy summary. Previously it printed:

az aks get-credentials --resource-group <rg> --name <cluster>

A user following that verbatim would hit the exact same bug manually, against whatever subscription their CLI happened to default to. It now includes --subscription.

Incidental findings (not blockers, worth follow-up)

  1. Default workload node pool SKU is Intel-hardcoded. AksPipeline.cs:154 creates the implicit workload pool as Standard_D2s_v5, and AddNodePool's default parameter matches. In a subscription with standardDSv5Family quota of 0 (common), deploy fails with ErrCode_InsufficientVCPUQuota even after overriding the system pool — the implicit workload pool is easy to miss. Worth considering a clearer error or a more available default.
  2. E2E coverage still can't observe this class of bug. All AKS deployment E2E tests deploy to the same subscription their ambient CLI is authenticated against. Catching subscription-scoping regressions in CI would need a second subscription (or an assertion on the emitted az argument vector — which is what the 6 unit tests added in this PR do).

Cleanup

Test resource group deleted; ambient az default restored. The pre-existing unrelated akstest2 resource group was left untouched.


Result: ✅ Verified against real Azure infrastructure. The previously documented residual gap is closed.

@mitchdenny

Copy link
Copy Markdown
Member Author

Addendum — deployment E2E run against PR bits

Ran Aspire.Deployment.EndToEnd.Tests.AksWithAzureResourcesDeploymentTests.DeployAksWithAzureResources against a real AKS cluster.

Result: ✅ 1/1 passed, 11m 58s, exit code 0.

Versions under test

Component Version
Aspire CLI 13.6.0-pr.19219.g7ac00b8d
Aspire.Hosting.Azure.Kubernetes 13.6.0-pr.19219.g7ac00b8d
Aspire.Hosting.Azure.KeyVault 13.6.0-pr.19219.g7ac00b8d
Aspire.Hosting.Azure.Storage 13.6.0-pr.19219.g7ac00b8d

Resolved from the PR hive (~/.aspire/hives/pr-19219/packages). Cluster aks-5ov52jcyuj62w, RG e2e-aksazure-20260811061743-1, subscription 731ff15d…, westus3.

Evidence for the changed code path

16:28:13 (aks-get-credentials-aks) → Starting aks-get-credentials-aks...
16:28:13 (aks-get-credentials-aks) i [INF] Fetching AKS credentials: cluster=aks-5ov52jcyuj62w, resourceGroup=e2e-aksazure-20260811061743-1
16:28:16 (aks-get-credentials-aks) i [INF] AKS credentials written to /var/folders/.../aspire-aks3Ef...
16:28:16 (aks-get-credentials-aks) ✓ AKS credentials fetched for cluster aks-5ov52jcyuj62w (3.1s)
16:28:53 (helm-deploy-aks)         ✓ Helm release production deployed to namespace default (12.0s)
                                   ✅ Pipeline succeeded

The user-facing hint now carries the subscription:

🔑 Connect to cluster: az aks get-credentials --resource-group e2e-aksazure-20260811061743-1 \
     --name aks-5ov52jcyuj62w --subscription 731ff15d-4cfe-4c85-b779-e6f988b090ae

Helm deploying successfully is the real signal — it proves the kubeconfig fetched by the pinned az call is valid and points at the right cluster. Test self-cleaned; resource group deletion confirmed complete.

Scope caveat

This is a regression test, not a discriminating one. The harness itself runs az group create / az aks list without --subscription, so the ambient CLI subscription must match the deployment subscription for the test to work at all — meaning this run could not have distinguished fixed from unfixed code. The discriminating proof remains the decoy-subscription A/B reported earlier, where the unfixed path failed with ResourceGroupNotFound against a deliberately mismatched ambient subscription.

Unrelated harness bug found

The first attempt reported a green pass while actually exercising 13.5.0+cfbf1c43 (release/13.5) — code that provably does not contain this fix. Root cause is in tests/Shared/CliInstallStrategy.cs, independent of this PR: PullRequest mode installs to ~/.aspire/dogfood/pr-<N>/bin but the harness only prepends ~/.aspire/bin, and nothing asserts the resulting version. Filed as #19223. The run above used a manual CLI swap to work around it.

Mitch Denny (mitchdenny) added a commit that referenced this pull request Aug 11, 2026
Addresses review feedback on #19219: the existing tests all invoked
GetResourceGroupAsync / FetchKubeConfigAsync directly, so nothing executed
GetAksCredentialsAsync itself. Reverting the call site to the pre-fix
unscoped `az` invocation left every test green -- the exact mutation that
shipped as #19216 was undetectable one level up from where we asserted.

Adds two internal test seams at the action boundary (az CLI path resolution
and command execution) and a step-level test that resolves the registered
aks-get-credentials-{name} step from the pipeline and runs its Action. It
asserts the full command line of both `az` calls carry --subscription, plus
the "Connect to cluster" summary hint, which was previously untested.

Verified by mutation: restoring the unscoped call site now fails the new
test while the six helper-level tests still pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4
Copilot AI review requested due to automatic review settings August 11, 2026 09:15

Copilot AI 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.

Review details

Suppressed comments (1)

src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs:295

  • The reconnect command leaves all values unquoted, but valid Azure resource-group names may contain parentheses (for example, team(prod)). In common shells those parentheses are syntax, so the command advertised as copy-pasteable fails even though the actual az invocation above correctly quotes its values. Quote the resource group, cluster name, and subscription ID here, and update the summary assertion accordingly.
                    new MarkdownString(
                        $"`az aks get-credentials --resource-group {resourceGroup} --name {clusterName} --subscription {subscriptionId}`"));
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Found 1 correctness issue: existing AKS resources with a resource-specific Azure scope can acquire credentials from the global deployment scope.

Copilot AI review requested due to automatic review settings August 11, 2026 10:41
@github-actions

This comment has been minimized.

Copilot AI 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.

Review details

Suppressed comments (2)

tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesInfrastructureTests.cs:238

  • This test exercises GetAzureDeploymentContextAsync, but that helper has no production caller; the registered credential step goes through ResolveDeploymentScopeAsync, which duplicates the missing-subscription check. A regression in the actual pipeline branch would therefore leave this test green. Exercise ResolveDeploymentScopeAsync or the registered step with missing state, and remove or reuse the dead helper.
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => AzureKubernetesEnvironmentResource.GetAzureDeploymentContextAsync(
                services,
                TestContext.Current.CancellationToken));

src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs:302

  • The displayed reconnect command is still not always copy-pasteable: ValidateAzureResourceName permits parentheses, which are valid in resource-group names but are shell syntax when unquoted (for example, team(prod) fails in bash). Quote the resource group, cluster name, and subscription exactly as the real Azure CLI invocation does, and update the summary assertion.
                    new MarkdownString(
                        $"`az aks get-credentials --resource-group {resourceGroup} --name {clusterName} --subscription {subscriptionId}`"));
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two credential-scope correctness issues remain before merge:

  • The existing thread on AzureBicepResource.Scope precedence is confirmed by a registered-pipeline-action probe: provisioning targets the configured scope, but credential acquisition still uses the annotation/global state.
  • An explicit deferred scope value that resolves null silently falls back to global deployment state here, while provisioning rejects it.

Focused proof at 89d876c8e3: restore succeeded; 10 targeted AKS tests passed; mutation probes failed as expected for both global-scope regression and ignored resource.Scope. A live cross-subscription AKS deploy was not run because it requires Azure credentials and billable infrastructure.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

This comment has been minimized.

Copilot AI 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.

Review details

Suppressed comments (3)

src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs:302

  • The reconnect command still is not copy-pasteable for every accepted resource-group name. ValidateAzureResourceName explicitly permits parentheses, but an unquoted value such as team(prod) is parsed as shell syntax in Bash/Zsh rather than as the --resource-group argument. Quote the resource group, cluster name, and subscription in the summary command, and update the corresponding assertions.
                    new MarkdownString(
                        $"`az aks get-credentials --resource-group {resourceGroup} --name {clusterName} --subscription {subscriptionId}`"));

tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesInfrastructureTests.cs:238

  • This test exercises GetAzureDeploymentContextAsync, which is now referenced only by tests; the registered credential step calls ResolveDeploymentScopeAsync instead. Consequently, a regression in the live missing-subscription branch or its actionable message would leave this test green. Invoke ResolveDeploymentScopeAsync with both scoped values null so the assertion covers the production path.
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => AzureKubernetesEnvironmentResource.GetAzureDeploymentContextAsync(
                services,
                TestContext.Current.CancellationToken));

src/Aspire.Hosting.Azure/AzureBicepResourceScope.cs:102

  • Changing HasResourceGroup from internal to public adds a new public API on AzureBicepResourceScope, but the PR checklist says no public API was added. Either keep this member internal through an assembly-level implementation approach, or update the checklist and send the new API through the required API review.
    public bool HasResourceGroup => _resourceGroup is not null;
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 11, 2026 22:46
@mitchdenny

Copy link
Copy Markdown
Member Author

/deployment-test

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Deployment tests starting on PR #19219...

This will deploy to real Azure infrastructure. Results will be posted here when complete.

View workflow run

@mitchdenny
Mitch Denny (mitchdenny) enabled auto-merge (squash) August 12, 2026 02:26
@github-actions
github-actions Bot temporarily deployed to deployment-testing August 12, 2026 02:26 Inactive
@github-actions

Copy link
Copy Markdown
Contributor

Tests selector (audit mode)

The full test matrix and all jobs still run in audit mode. The tests and jobs below are what selective CI would run under enforcement.

11 / 100 test projects · 3 jobs, from 4 changed files.

Selected test projects (11 / 100)

Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Azure.Kusto.Tests, Aspire.Hosting.Azure.Tests, Aspire.Hosting.Blazor.Tests, Aspire.Hosting.CodeGeneration.TypeScript.Tests, Aspire.Hosting.Docker.Tests, Aspire.Hosting.Dotnet.Tests, Aspire.Hosting.Foundry.Tests, Aspire.Hosting.Radius.Tests, Aspire.Hosting.Tests, Aspire.Playground.Tests

Selected jobs (3)

deployment-e2e, extension-e2e, typescript-api-compat


How these were chosen — grouped by what changed

⚠️ 10 of the 11 selected test projects come from a single change — src/Aspire.Hosting.Azure/AzureBicepResourceScope.cs.

🔧 src/Aspire.Hosting.Azure/AzureBicepResourceScope.cs (changed source)
1 directly: Aspire.Hosting.Azure.Tests
9 via the project graph: Aspire.Hosting.Azure.Kusto.Tests (2 hops), Aspire.Hosting.Blazor.Tests (3 hops), Aspire.Hosting.CodeGeneration.TypeScript.Tests (2 hops), Aspire.Hosting.Docker.Tests (2 hops), Aspire.Hosting.Dotnet.Tests (3 hops), Aspire.Hosting.Foundry.Tests (2 hops), Aspire.Hosting.Radius.Tests (3 hops), Aspire.Hosting.Tests (2 hops), Aspire.Playground.Tests (2 hops)

🔧 src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs (changed source)
2 directly: Aspire.Hosting.Azure.Kubernetes.Tests, Aspire.Hosting.Azure.Tests

🧪 tests/Aspire.Hosting.Azure.Kubernetes.Tests/Aspire.Hosting.Azure.Kubernetes.Tests.csproj (changed test)
1 directly: Aspire.Hosting.Azure.Kubernetes.Tests

🧪 tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesInfrastructureTests.cs (changed test)
1 directly: Aspire.Hosting.Azure.Kubernetes.Tests

Job reasons

Job Triggered by
deployment-e2e affected project Aspire.Hosting.Azure.Kubernetes
extension-e2e src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs, src/Aspire.Hosting.Azure/AzureBicepResourceScope.cs
• affected project Aspire.Hosting.Azure.Kubernetes
typescript-api-compat affected project Aspire.Hosting.Azure.Kubernetes

Selection computed for commit 57efff2.

Copilot AI 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.

Review details

Suppressed comments (1)

src/Aspire.Hosting.Azure.Kubernetes/AzureKubernetesEnvironmentResource.AksPipeline.cs:306

  • The reconnect command is not copy-pasteable in Windows Command Prompt: cmd.exe does not treat single quotes as delimiters, so the apostrophes become part of the resource-group and cluster-name arguments and az fails to find them. Use double quotes for all three values (as the real command builder does); they work in Command Prompt, PowerShell, bash, and zsh, and update the two summary assertions accordingly.
                    new MarkdownString(
                        $"`az aks get-credentials --resource-group '{resourceGroup}' --name '{clusterName}' --subscription {subscriptionId}`"));
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny
Mitch Denny (mitchdenny) merged commit 122f9e0 into main Aug 12, 2026
1346 of 1358 checks passed
@mitchdenny
Mitch Denny (mitchdenny) deleted the mitchdenny-fix-aks-credential-subscription branch August 12, 2026 03:10
@mitchdenny

Copy link
Copy Markdown
Member Author

/backport to release/13.5

@github-actions github-actions Bot added this to the 13.6 milestone Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/13.5 (link to workflow run)

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Pull request created: #1476

Generated by PR Documentation Check · auto · 40.9 AIC · ⌖ 9.05 AIC · ⊞ 19.6K

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

📝 Documentation has been drafted in microsoft/aspire.dev#1476 targeting release/13.5.

Updated deployment/kubernetes/aks.mdx Troubleshooting section to add --subscription <subscription-id> to the manual az aks get-credentials command, plus an Aside tip explaining why (multi-subscription tenants/CI agents) and pointing to the deploy summary's 🔑 Connect to cluster output. Triggered signal: pr_body_has_cli_flag_mention (PR body mentions --subscription).

Note

This draft PR needs human review before merging.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ CI Failure Analysis: Possible Flaky Test(s)

The CI build failed due to test failure(s) that appear unrelated to the PR changes. These may be flaky tests.

Suspected flaky test(s):

  • extension/src/test/e2eShardMatrix.test.js (Extension host E2E test) in job Tests / Run VS Code extension unit tests (Windows)
    • Error: 1 test failed. Extension host test runner error 1 test failed.
    • Stack Trace (first frames):
      at Context.<anonymous> (d:\a\aspire\aspire\extension\out\test\e2eShardMatrix.test.js:146:9)
      at process.processImmediate (node:internal/timers:504:21)
      Exit code: 1
      
    • Why likely flaky: The PR only modifies Azure Kubernetes AKS pipeline hosting code and Bicep resource scope (src/Aspire.Hosting.Azure.Kubernetes, src/Aspire.Hosting.Azure), none of which relate to the VS Code extension's e2eShardMatrix test. This exact failure signature matches a previously recorded recurring flaky test (cause extension-e2eshardmatrix-windows-flaky) with 2 prior occurrences on Windows runners.

Suggested actions:

  • Re-run the failed CI jobs to confirm if the failure is intermittent
  • If the test continues to fail, consider quarantining it using /quarantine-test <test name> <issue URL>
  • Search existing issues to see if this test is already known to be flaky

You can re-run the failed jobs from the workflow run page.

Jose Perez Rodriguez (joperezr) pushed a commit that referenced this pull request Aug 12, 2026
…tion (#19271)

* Pin AKS credential pipeline to the deployment subscription

The AKS post-provisioning step invoked the Azure CLI without --subscription,
so `az aks get-credentials` and the `az resource list` resource-group fallback
both ran against whatever subscription the ambient `az` CLI defaulted to rather
than the one Aspire selected for the deployment.

The resource group lookup also read IConfiguration["Azure:ResourceGroup"], which
is only populated at host startup. On a first deploy that value is empty, so the
fallback query fired and silently resolved the cluster from the wrong
subscription.

Read the Azure subscription and resource group from IDeploymentStateManager
instead. Provisioning writes them during the pipeline run, and SaveSectionAsync
updates the in-memory state that AcquireSectionAsync reads back, so the values
are current even on a first deploy. Both Azure CLI call sites now pass
--subscription explicitly.

The az command runner is injected into GetResourceGroupAsync and
FetchKubeConfigAsync so tests can assert the exact command lines without
spawning the CLI. This matters: asserting only on the argument-builder helpers
would not have caught the original bug, since nothing would prove the pipeline
actually calls them.

Fixes #19216

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Cover the get-credentials call site, not just its helpers

Addresses review feedback on #19219: the existing tests all invoked
GetResourceGroupAsync / FetchKubeConfigAsync directly, so nothing executed
GetAksCredentialsAsync itself. Reverting the call site to the pre-fix
unscoped `az` invocation left every test green -- the exact mutation that
shipped as #19216 was undetectable one level up from where we asserted.

Adds two internal test seams at the action boundary (az CLI path resolution
and command execution) and a step-level test that resolves the registered
aks-get-credentials-{name} step from the pipeline and runs its Action. It
asserts the full command line of both `az` calls carry --subscription, plus
the "Connect to cluster" summary hint, which was previously untested.

Verified by mutation: restoring the unscoped call site now fails the new
test while the six helper-level tests still pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Honor per-resource scope when fetching AKS credentials

A cluster adopted with AsExistingInResourceGroup(name, rg, subscription) can
live outside the subscription and resource group Aspire deploys the rest of the
app into. The provisioner already targets that per-resource scope, but the
credential pipeline only ever read the global Azure deployment state, so the
Azure CLI calls could authenticate against the wrong subscription -- or find a
same-named cluster in the wrong place.

Resolve the resource's ExistingAzureResourceAnnotation first and fall back to
deployment state only for values it does not pin. When the resource pins a
subscription that differs from the deployment subscription, the saved resource
group is deliberately not inherited: it names a group inside the other
subscription, so the step rediscovers it instead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Honor explicitly assigned scope and reject unresolvable scope values

Two follow-ups on the cross-scope credential fix.

ConfigureInfrastructure can assign AzureBicepResource.Scope directly, and the
provisioner gives that precedence over ExistingAzureResourceAnnotation. The
credential step only looked at the annotation, so a cluster placed in another
subscription via Scope still had its credentials fetched from the app's own
subscription. Mirror the provisioner's precedence: Scope first, annotation
second. AzureBicepResourceScope.HasResourceGroup becomes public so callers can
test the scope before reading ResourceGroup, which throws for subscription- and
tenant-scoped resources.

Resolving a scope value from an IValueProvider that yields null now throws,
matching BicepProvisioner.ResolveScopeValueAsync. Previously it returned null
and the caller silently substituted the app's deployment scope, so an
unavailable scope parameter could fetch a same-named cluster from the wrong
place instead of failing the way provisioning does. Empty is rejected too since
it would otherwise be dropped by the downstream IsNullOrEmpty checks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Reject empty literal scope values, not just empty provider results

The provider path already threw on a null or empty result, but a literal
empty string fell through to `string s => s`. Nothing upstream rejects it:
AsExistingInResourceGroup and the AzureBicepResourceScope constructors only
guard against null. Downstream `string.IsNullOrEmpty` checks then treated the
value as unpinned and silently fell back to the global deployment scope, which
is the same silent-wrong-scope failure the provider check was added to prevent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Address review feedback: dead code, shell-safe hint, stale docs, test hygiene

Removes GetAzureDeploymentContextAsync, which had no production caller. Its two
tests read as coverage for the credential step's subscription handling while
exercising only the dead helper, so they now target ResolveDeploymentScopeAsync
where the check actually runs.

Quotes the resource group and cluster name in the "Connect to cluster" hint.
ValidateAzureResourceName permits parentheses, so a group named team(prod)
produced a hint that bash and zsh reject, despite the hint being advertised as
copy-pasteable. The real invocation already quoted these.

Refreshes GetResourceGroupAsync docs, which still described pre-PR behavior, to
capture the cross-subscription rationale for why discovery now fires.

Tests: disposes the ServiceProvider returned by CreateServicesWithAzureState at
all 8 call sites, and replaces ?.TrySetResult() with an explicit Assert.NotNull
plus unconditional call so a broken precondition fails immediately instead of
degrading into a CI timeout.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

* Fail on ambiguous AKS cluster names instead of picking the first

The resource-group fallback query used `--query [0].resourceGroup`, taking
whichever match Azure happened to return first. AKS cluster names are only
unique within a resource group, not within a subscription, so a subscription
can legitimately hold several clusters with the same name. Silently choosing
one means fetching credentials for, and deploying into, an unrelated cluster.

Query all matches and require exactly one, directing the user to
AsExistingInResourceGroup when the name is ambiguous.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4

---------

Co-authored-by: Mitch Denny <midenn@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4b15f47d-c522-4d08-a976-b3ee69c4ebb4
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-integrations Issues pertaining to Aspire Integrations packages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AKS credential pipeline uses ambient Azure CLI subscription

4 participants