diff --git a/.github/workflows/backmerge-release.yml b/.github/workflows/backmerge-release.yml index 7033eae2fa3..2b1e4336d4a 100644 --- a/.github/workflows/backmerge-release.yml +++ b/.github/workflows/backmerge-release.yml @@ -33,15 +33,15 @@ jobs: - name: Check for changes to backmerge id: check run: | - git fetch origin main release/13.3 - BEHIND_COUNT=$(git rev-list --count origin/main..origin/release/13.3) + git fetch origin main release/13.4 + BEHIND_COUNT=$(git rev-list --count origin/main..origin/release/13.4) echo "behind_count=$BEHIND_COUNT" >> $GITHUB_OUTPUT if [ "$BEHIND_COUNT" -gt 0 ]; then echo "changes=true" >> $GITHUB_OUTPUT - echo "Found $BEHIND_COUNT commits in release/13.3 not in main" + echo "Found $BEHIND_COUNT commits in release/13.4 not in main" else echo "changes=false" >> $GITHUB_OUTPUT - echo "No changes to backmerge - release/13.3 is up-to-date with main" + echo "No changes to backmerge - release/13.4 is up-to-date with main" fi - name: Attempt merge and create branch @@ -52,12 +52,12 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git checkout origin/main - git checkout -b backmerge/release-13.3-to-main + git checkout -b backmerge/release-13.4-to-main # Attempt the merge - if git merge origin/release/13.3 --no-edit; then + if git merge origin/release/13.4 --no-edit; then echo "merge_success=true" >> $GITHUB_OUTPUT - git push origin backmerge/release-13.3-to-main --force + git push origin backmerge/release-13.4-to-main --force echo "Merge successful, branch pushed" else echo "merge_success=false" >> $GITHUB_OUTPUT @@ -72,7 +72,7 @@ jobs: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | # Check if a PR already exists for this branch - EXISTING_PR=$(gh pr list --head backmerge/release-13.3-to-main --base main --json number --jq '.[0].number // empty') + EXISTING_PR=$(gh pr list --head backmerge/release-13.4-to-main --base main --json number --jq '.[0].number // empty') if [ -n "$EXISTING_PR" ]; then echo "PR #$EXISTING_PR already exists, updating it" @@ -80,7 +80,7 @@ jobs: else PR_BODY="## Automated Backmerge - This PR merges changes from \`release/13.3\` back into \`main\`. + This PR merges changes from \`release/13.4\` back into \`main\`. **Commits to merge:** ${{ steps.check.outputs.behind_count }} @@ -94,9 +94,9 @@ jobs: PR_BODY=$(echo "$PR_BODY" | sed 's/^ //') PR_URL=$(gh pr create \ - --head backmerge/release-13.3-to-main \ + --head backmerge/release-13.4-to-main \ --base main \ - --title "[Automated] Backmerge release/13.3 to main" \ + --title "[Automated] Backmerge release/13.4 to main" \ --body "$PR_BODY" \ --assignee joperezr,radical \ --label area-engineering-systems) @@ -142,7 +142,7 @@ jobs: const issueBody = [ '## Backmerge Conflict', '', - 'The automated backmerge from `release/13.3` to `main` failed due to merge conflicts.', + 'The automated backmerge from `release/13.4` to `main` failed due to merge conflicts.', '', '### What to do', '', @@ -150,7 +150,7 @@ jobs: ' ```bash', ' git checkout main', ' git pull origin main', - ' git merge origin/release/13.3', + ' git merge origin/release/13.4', ' ```', '2. Resolve the conflicts', '3. Push the merge commit or create a PR manually', @@ -167,7 +167,7 @@ jobs: await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, - title: '[Backmerge] Merge conflicts between release/13.3 and main', + title: '[Backmerge] Merge conflicts between release/13.4 and main', body: issueBody, assignees: ['joperezr', 'radical'], labels: ['area-engineering-systems', 'backmerge-conflict'] diff --git a/.github/workflows/generate-api-diffs.yml b/.github/workflows/generate-api-diffs.yml index ceb4435c41c..b053c4a3a37 100644 --- a/.github/workflows/generate-api-diffs.yml +++ b/.github/workflows/generate-api-diffs.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: release/13.4 + ref: main - name: Restore and build run: | @@ -40,7 +40,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} branch: update-api-diffs - base: release/13.4 + base: main labels: | NO-MERGE title: "[Automated] Update API Surface Area" diff --git a/docs/cli-staging-validation.md b/docs/cli-staging-validation.md new file mode 100644 index 00000000000..e29034af883 --- /dev/null +++ b/docs/cli-staging-validation.md @@ -0,0 +1,148 @@ +# Validating staging feed routing with a local CLI build + +This document describes how to make a locally built Aspire CLI resolve `Aspire.*` +packages exactly the way an official **staging** (or **stable**) build would, so the +staging feed-routing behavior can be validated end-to-end without an official build. + +## Background + +A staging-identity CLI is an official release-branch build whose own commit always has +a SHA-specific `darc-pub-microsoft-aspire-` feed carrying its matching packages +(prerelease-shaped `13.4.0-preview.*` and stable-shaped `13.4.0` alike). Feed +**provenance** is decided by the CLI's baked build **identity** (`AspireCliChannel`), +while version **filtering** (the channel quality) is decided by the CLI's **version +shape**. See `PackagingService.ShouldUseSharedStagingFeed`. + +A locally built CLI bakes a `local` identity and an unstamped informational version, so +it never synthesizes a staging channel and never derives a darc feed. The two diagnostic +overrides below let you simulate the staging path locally. + +## The two diagnostic overrides + +Both are read by `PackagingService` only (their blast radius is limited to staging +feed-routing decisions — they do **not** change the global identity used for hive or +package-directory lookups): + +| Config key | Purpose | +| --- | --- | +| `overrideCliIdentityChannel` | Forces the identity used for staging-feed routing decisions. Must be a valid channel (`stable`, `staging`, `daily`, `local`, or `pr-`); invalid values are ignored and the real identity is used. | +| `overrideCliInformationalVersion` | Forces the informational version that both the SHA-derivation provider and the version-shape (quality) predicate read. The part after `+` (truncated to 8 chars) builds the darc URL; the version part determines stable-vs-prerelease shape. | + +**Both overrides are required** to reach the darc path from a local build: + +- Identity override alone → the SHA is still unstamped, so the darc URL can't be derived. +- Version override alone → the identity stays `local`, so routing never selects the darc feed. + +When either override is set, the CLI emits a one-time warning so an overridden +identity/feed can't silently resolve packages on a normal invocation. + +## Recipe + +1. Build the CLI locally: + + ```bash + ./build.sh --build /p:SkipNativeBuild=true + ``` + +2. In the apphost directory, set `channel: staging` in `aspire.config.json` (this is what + `aspire add` filters the synthesized channels to): + + ```json + { + "channel": "staging" + } + ``` + +3. Set the two overrides (environment variables are the simplest; they are read + case-insensitively with no prefix): + + ```bash + export overrideCliIdentityChannel=staging + export overrideCliInformationalVersion=13.4.0-preview.1.26280.6+ + ``` + + Use a real release-branch build commit hash so the derived feed actually exists if you + intend to restore; any 8+ char hex suffix works for inspecting the resolved feed URL. + +4. Run `aspire add` with debug logging and confirm the resolved darc feed: + + ```bash + aspire add foundry --debug + ``` + + The logs should show the staging channel resolving `Aspire*` to + `.../darc-pub-microsoft-aspire-/...` rather than the shared + `dnceng/.../dotnet9` daily feed. + +To simulate a **stable**-shaped staging build, use a stable-shaped version override +(e.g. `13.4.0+`); the channel quality becomes `Stable` while the feed +stays the darc feed. + +## Helper scripts + +`eng/scripts/debug-staging.{sh,ps1}` and `eng/scripts/debug-stable.{sh,ps1}` wrap the +recipe above. Both target identity `staging` and expect the **same** darc feed; they +differ only in version shape/quality: + +| Script | Version shape | Expected quality | Scenario | +| --- | --- | --- | --- | +| `debug-staging` | prerelease (`13.4.0-preview.*`) | `Both` | [#17744](https://github.com/microsoft/aspire/issues/17744) — the bug this PR fixes | +| `debug-stable` | stable (`13.4.0`) | `Stable` | [#17527](https://github.com/microsoft/aspire/issues/17527) — stable-shaped release build | + +Each script computes the expected `darc-pub-microsoft-aspire-` feed and supports +three modes: + +- **Validate (default):** runs `aspire add --debug` in a throwaway directory and + asserts the darc feed appears in the resolution log. Exits non-zero if it doesn't. +- **`--print-env` / `-PrintEnv`:** emits `export`/`$env:` lines you apply to your current + shell. Every subsequent `aspire` command then behaves like the simulated build. +- **`--shell` / `-Shell`:** opens an interactive subshell with the overrides applied and + the target CLI first on `PATH`. It also points `NUGET_PACKAGES` at an isolated, per-sha + cache so restores from the simulated staging feed never contaminate your real global + package cache. Exiting the subshell restores normal behavior. + +Common flags: `--sha ` (required, 8–40 hex), `--cli ` (CLI to drive), +`--pr ` (install that PR's full-bundle build first, then target it), `--version `. + +### Interactive validation against an installed PR build + +You don't need a local source build — the easiest carrier is an installed **PR build**, +which is a real full-bundle `~/.aspire` install. Install it, then make it behave like a +staging build for a full `aspire new` / `aspire add` / run flow: + +```bash +# 1. Install the PR's full-bundle build. +./eng/scripts/get-aspire-cli-pr.sh 17743 + +# 2a. Apply staging overrides to the CURRENT shell (every aspire command is staging-flavored): +eval "$(./eng/scripts/debug-stable.sh --sha --print-env)" +aspire new # behaves like the simulated staging build +aspire add foundry +# revert when done: +unset channel overrideCliIdentityChannel overrideCliInformationalVersion + +# 2b. ...or get a throwaway subshell instead (overrides vanish on 'exit'): +./eng/scripts/debug-stable.sh --pr 17743 --sha --shell +``` + +PowerShell is identical with the `.ps1` siblings: + +```powershell +./eng/scripts/get-aspire-cli-pr.ps1 17743 +./eng/scripts/debug-stable.ps1 -Sha -PrintEnv | Invoke-Expression +# ...or: +./eng/scripts/debug-stable.ps1 -Pr 17743 -Sha -Shell +``` + +The overrides are scoped to `PackagingService` feed routing and only ever live in the +shell/subshell environment, so nothing is written to global or per-project config. + +## Validation matrix + +| Identity | Version shape | Expected feed | Expected quality | +| --- | --- | --- | --- | +| `staging` | prerelease | `darc-pub-microsoft-aspire-` | `Both` | +| `staging` | stable | `darc-pub-microsoft-aspire-` | `Stable` | +| `daily` | any | shared `dnceng/.../dotnet9` daily feed | `Both` | +| `local` / `pr-` | any | local/PR hive + implicit (no staging synthesis) | n/a | +| `stable` | stable | nuget.org | `Stable` | diff --git a/docs/contributing.md b/docs/contributing.md index 15ea02c434c..4b12c05b9bf 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -102,6 +102,8 @@ dotnet test --no-launch-profile -- \ To test changes from a specific pull request locally, see [dogfooding-pull-requests.md](/docs/dogfooding-pull-requests.md) for instructions on installing Aspire CLI and NuGet packages built by that PR's CI run. +To validate how the CLI resolves `Aspire.*` packages for **staging** and **stable** release-branch builds (including making an installed PR build behave like a staging build), see [cli-staging-validation.md](/docs/cli-staging-validation.md). + ## Coding Agents Aspire uses GitHub Copilot automatic code review on pull requests. We expect Copilot review comments to be reviewed and addressed before merging, either by making the requested change or by explaining why a suggested change is not needed. diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 23f22623e87..5ef192d329a 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -3,6 +3,24 @@ parameters: displayName: 'Package VS Code Extension as Pre-Release' type: boolean default: false + # Operator-controlled override for the AspireCliChannel baked into native CLI + # binaries by build_sign_native.yml. The default 'auto' lets the pipeline pick + # stable / staging / daily / pr- from Build.Reason and Build.SourceBranch + # (where release/* and internal/release/* branches always resolve to 'staging' + # so stabilizing dogfood builds aren't mis-baked as 'stable' — see + # https://github.com/microsoft/aspire/issues/17527). Set this to 'stable' when + # kicking off the official GA ship build from a release/* branch so the + # distributed binary identifies as stable and `aspire init` writes a + # nuget.org-only nuget.config matching the promoted package set. + - name: aspireCliChannelOverride + displayName: 'Aspire CLI channel override (auto = derive from branch; set to stable for the GA ship build)' + type: string + default: 'auto' + values: + - auto + - stable + - staging + - daily trigger: batch: true @@ -158,6 +176,7 @@ extends: - osx-x64 codeSign: true teamName: $(_TeamName) + aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }} extraBuildArgs: >- /p:Configuration=$(_BuildConfig) $(_SignArgs) @@ -173,6 +192,7 @@ extends: # no need to sign ELF binaries on linux codeSign: false teamName: $(_TeamName) + aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }} extraBuildArgs: >- /p:Configuration=$(_BuildConfig) $(_OfficialBuildIdArgs) @@ -185,6 +205,7 @@ extends: - win-arm64 codeSign: true teamName: $(_TeamName) + aspireCliChannelOverride: ${{ parameters.aspireCliChannelOverride }} extraBuildArgs: >- /p:Configuration=$(_BuildConfig) $(_SignArgs) diff --git a/eng/pipelines/templates/build_sign_native.yml b/eng/pipelines/templates/build_sign_native.yml index e06831ab7d6..eb2af1e8e34 100644 --- a/eng/pipelines/templates/build_sign_native.yml +++ b/eng/pipelines/templates/build_sign_native.yml @@ -6,6 +6,19 @@ parameters: extraBuildArgs: '' codeSign: false teamName: '' + # Optional override for AspireCliChannel. Accepted values: 'auto' (default; + # let computeCliChannel below pick stable/staging/daily/pr- from Build.Reason + # and Build.SourceBranch), 'stable', 'staging', 'daily'. Set this to 'stable' + # when running the official ship pipeline so the GA CLI binary is baked with + # AspireCliChannel=stable (and aspire init then writes a nuget.org-only + # nuget.config, which is correct because the packages have been promoted to + # nuget.org). For routine stabilizing builds from a release/* branch — which + # also set DotNetFinalVersionKind=release — leave this on 'auto' so the + # channel falls through to 'staging' and aspire init writes a nuget.config + # that maps Aspire.* to the staging feed. See + # https://github.com/microsoft/aspire/issues/17527 for the bug that made this + # override necessary. + aspireCliChannelOverride: 'auto' jobs: @@ -84,15 +97,32 @@ jobs: $reason = '$(Build.Reason)' $sourceBranch = '$(Build.SourceBranch)' $prNumber = '$(System.PullRequest.PullRequestNumber)' + # Template-time substitution: the value is the resolved + # aspireCliChannelOverride parameter literal, never a runtime + # variable. Quoting protects an empty/default value. + $override = '${{ parameters.aspireCliChannelOverride }}' Write-Host "Build.Reason: '$reason'" Write-Host "Build.SourceBranch: '$sourceBranch'" Write-Host "System.PullRequest.PullRequestNumber: '$prNumber'" + Write-Host "aspireCliChannelOverride: '$override'" $versionKind = & "$(Build.SourcesDirectory)/$(dotnetScript)" msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind $versionKind = $versionKind.Trim() Write-Host "DotNetFinalVersionKind: '$versionKind'" - if ($reason -eq 'PullRequest') { + if ($override -and $override -ne 'auto') { + # Operator override path. Validate against the same accepted set + # that IdentityChannelReader.IsValidChannel enforces at CLI startup + # so a typo here fails the pipeline step rather than producing a + # binary that refuses to boot. pr- is intentionally excluded + # from the override set — PR builds always come from the + # PullRequest reason arm below. + if ($override -notin @('stable', 'staging', 'daily')) { + throw "aspireCliChannelOverride='$override' is not one of: auto, stable, staging, daily." + } + $channel = $override.ToLowerInvariant() + } + elseif ($reason -eq 'PullRequest') { # Defense in depth: validate digit-only PR number rather than just # non-emptiness. If the agent ever returns the literal macro string # (e.g. '$(System.PullRequest.PullRequestNumber)' unresolved) this @@ -105,10 +135,21 @@ jobs: # Bake the resolved hive label directly into AspireCliChannel. The CLI # consumes this verbatim and avoids the legacy "pr" + parsed-PrNumber join. $channel = "pr-$prNumber" - } elseif ($versionKind -eq 'release') { - $channel = 'stable' } elseif ($sourceBranch -match '^refs/heads/(release|internal/release)/') { + # Release/internal-release branches always produce staging artifacts — + # they are published to the staging feed for dogfooding and only later + # promoted to nuget.org. This must be checked BEFORE the + # `versionKind == release` arm, because a release-branch build also sets + # StabilizePackageVersion=true (→ DotNetFinalVersionKind=release) once + # we are stabilizing for ship. Without this ordering, the stabilized + # staging build would bake AspireCliChannel=stable and `aspire init` + # would drop a nuget.config with no staging feed mapping, causing + # `aspire add` to resolve Aspire.* packages from nuget.org (older + # versions) or fail to resolve the +sha-pinned Aspire.AppHost.Sdk. + # See https://github.com/microsoft/aspire/issues/17527. $channel = 'staging' + } elseif ($versionKind -eq 'release') { + $channel = 'stable' } else { # main and any other branch fall through to daily $channel = 'daily' diff --git a/eng/scripts/debug-aspire-channel.ps1 b/eng/scripts/debug-aspire-channel.ps1 new file mode 100644 index 00000000000..e521d31be87 --- /dev/null +++ b/eng/scripts/debug-aspire-channel.ps1 @@ -0,0 +1,238 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Shared implementation for debug-staging.ps1 and debug-stable.ps1. + +.DESCRIPTION + Makes an EASY-TO-GET Aspire CLI build behave like an official release-branch + staging build for validating package feed routing, WITHOUT producing a real + official build or stamping a binary locally. + + The recommended carrier is a PR build (a real, full self-extracting ~/.aspire + install) acquired with eng/scripts/get-aspire-cli-pr.ps1 . Any installed + 'aspire' (or a locally built one via -Cli) works just as well, because the + behavior is driven entirely by two diagnostic config overrides read by + PackagingService (see docs/cli-staging-validation.md): + + overrideCliIdentityChannel - forces the identity used for staging-feed + routing decisions (here: 'staging'). + overrideCliInformationalVersion - forces the informational version the SHA + derivation and version-shape (quality) + checks read, e.g. 13.4.0-preview.1.x+. + + Both flow into IConfiguration from environment variables (used here) OR from + aspire.config.json, and are scoped to staging feed routing only. A CLI run + with them set emits a one-time warning so they can never silently mis-route a + normal invocation. + + The script runs 'aspire add --debug' in a throwaway directory whose + aspire.config.json pins channel: staging, then asserts the debug log contains + Resolved 'staging' channel: feed=, quality= + The 'aspire add' step is expected to fail later (there is no real apphost + project in the scratch directory); only the feed-routing log line is validated. +#> + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Default version shapes for the 13.4 release branch. Override with -Version. +$script:DefaultStagingVersion = '13.4.0-preview.1.26280.6' +$script:DefaultStableVersion = '13.4.0' + +function Invoke-DebugChannel { + [CmdletBinding()] + param( + # 'staging' (prerelease-shaped) | 'stable' (stable-shaped) + [Parameter(Mandatory = $true)][ValidateSet('staging', 'stable')][string]$Kind, + [string]$Sha, + [string]$Pr, + [string]$Cli, + [string]$Version, + [string]$Identity = 'staging', + [string]$Package = 'foundry', + [switch]$Shell, + [switch]$PrintEnv, + [string[]]$PassThrough = @() + ) + + switch ($Kind) { + 'staging' { $kindLabel = 'staging (prerelease-shaped)'; $defaultVersion = $script:DefaultStagingVersion; $expectedQuality = 'Both' } + 'stable' { $kindLabel = 'staging (stable-shaped)'; $defaultVersion = $script:DefaultStableVersion; $expectedQuality = 'Stable' } + } + + if ([string]::IsNullOrEmpty($Sha)) { + Write-Error '-Sha is required.' + return + } + + # The darc feed name is built from the first 8 chars of the commit hash, so + # require at least that many hex characters (full hashes are accepted). + if ($Sha -notmatch '^[0-9a-fA-F]{8,40}$') { + Write-Error "-Sha must be 8-40 hexadecimal characters (got '$Sha')." + return + } + + if ([string]::IsNullOrEmpty($Version)) { $Version = $defaultVersion } + + $sha8 = $Sha.Substring(0, 8).ToLowerInvariant() + $expectedFeed = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-$sha8/nuget/v3/index.json" + $infoVersion = "$Version+$Sha" + + # -PrintEnv: emit shell-applicable env assignments and stop. CLI-agnostic on + # purpose -- the three keys drive ANY 'aspire' on PATH. Intended use: + # ./debug-staging.ps1 -Sha -PrintEnv | Invoke-Expression + if ($PrintEnv) { + Write-Output "# $kindLabel build (sha $sha8, feed darc-pub-microsoft-aspire-$sha8, quality $expectedQuality)." + Write-Output "# Apply to your current PowerShell session, then run aspire commands normally." + Write-Output "`$env:channel = 'staging'" + Write-Output "`$env:overrideCliIdentityChannel = '$Identity'" + Write-Output "`$env:overrideCliInformationalVersion = '$infoVersion'" + Write-Output "# To revert:" + Write-Output "# Remove-Item Env:channel, Env:overrideCliIdentityChannel, Env:overrideCliInformationalVersion" + return + } + + $scriptDir = Split-Path -Parent $PSCommandPath + + # Optionally install the PR build first; it becomes the default target CLI. + if (-not [string]::IsNullOrEmpty($Pr)) { + Write-Host ">> Installing PR #$Pr build via get-aspire-cli-pr.ps1 ..." + & (Join-Path $scriptDir 'get-aspire-cli-pr.ps1') $Pr + } + + if ([string]::IsNullOrEmpty($Cli)) { + $onPath = Get-Command aspire -ErrorAction SilentlyContinue + $installed = Join-Path $HOME '.aspire/bin/aspire' + if ($onPath) { + $Cli = $onPath.Source + } + elseif (Test-Path $installed) { + $Cli = $installed + } + else { + Write-Error "No aspire CLI found. Install a PR build (-Pr ), pass -Cli , or put 'aspire' on PATH." + return + } + } + if (-not (Test-Path $Cli)) { + Write-Error "CLI path '$Cli' does not exist." + return + } + # Resolve to an absolute path because the validation step runs from a scratch + # working directory, where a relative -Cli would no longer resolve. + $Cli = (Resolve-Path $Cli).Path + + Write-Host '' + Write-Host "Simulating an official $kindLabel build" + Write-Host " CLI: $Cli" + Write-Host " identity override: $Identity" + Write-Host " version override: $infoVersion" + Write-Host " expected feed: $expectedFeed" + Write-Host " expected quality: $expectedQuality" + Write-Host '' + + # -Shell: start a child PowerShell where the target CLI behaves like this build + # for every 'aspire' command. The overrides live only in the child process' + # environment, so closing it fully restores normal behavior. The CLI's directory + # is put first on PATH so a bare 'aspire' resolves to the target build. + if ($Shell) { + $cliDir = Split-Path -Parent $Cli + # Redirect NuGet's global packages folder to an isolated, per-sha directory + # so packages restored from the simulated staging feed (which can collide in + # version with packages already cached from real feeds) never contaminate the + # developer's real global cache (~/.nuget/packages by default). The directory + # is keyed by the simulated sha so repeat sessions reuse the same isolated + # cache, and is left in place on exit (it lives under the system temp dir). + $nugetPackages = Join-Path ([System.IO.Path]::GetTempPath()) (Join-Path 'aspire-debug-nuget' $sha8) + New-Item -ItemType Directory -Path $nugetPackages -Force | Out-Null + Write-Host '>> Launching a child PowerShell. Run aspire new, aspire add, etc.' + Write-Host " 'aspire' resolves to: $Cli" + Write-Host " NuGet packages cache: $nugetPackages (isolated from your global cache)" + Write-Host " Type 'exit' to leave and restore normal CLI behavior." + Write-Host '' + $env:channel = 'staging' + $env:overrideCliIdentityChannel = $Identity + $env:overrideCliInformationalVersion = $infoVersion + $env:NUGET_PACKAGES = $nugetPackages + $env:PATH = "$cliDir$([System.IO.Path]::PathSeparator)$env:PATH" + & (Get-Process -Id $PID).Path -NoExit -NoLogo + return + } + + # Throwaway working directory pinned to channel: staging so 'aspire add' + # filters to the synthesized staging channel. No real apphost project lives + # here, so 'add' will ultimately fail after feed routing has already been + # logged -- that is expected. + $scratch = Join-Path ([System.IO.Path]::GetTempPath()) ("aspire-debug-" + [System.Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $scratch | Out-Null + try { + @' +{ + "channel": "staging" +} +'@ | Set-Content -Path (Join-Path $scratch 'aspire.config.json') -Encoding utf8 + + $log = Join-Path $scratch 'aspire-debug.log' + Write-Host ">> Running: aspire add $Package --debug $($PassThrough -join ' ')" + Write-Host ' (feed routing is logged before the add step fails on the missing apphost)' + Write-Host '' + + # The overrides are scoped to THIS invocation only (set then removed), so + # they can't leak into the developer's other aspire commands. 'aspire add' + # is allowed to exit non-zero; success is decided by the log. + $previousChannel = $env:overrideCliIdentityChannel + $previousVersion = $env:overrideCliInformationalVersion + $previousLocation = Get-Location + try { + $env:overrideCliIdentityChannel = $Identity + $env:overrideCliInformationalVersion = $infoVersion + Set-Location $scratch + $cliArgs = @('add', $Package, '--debug') + $PassThrough + & $Cli @cliArgs *> $log + } + finally { + Set-Location $previousLocation + $env:overrideCliIdentityChannel = $previousChannel + $env:overrideCliInformationalVersion = $previousVersion + } + + $logText = Get-Content -Path $log -Raw -ErrorAction SilentlyContinue + if ($null -eq $logText) { $logText = '' } + + # Echo the resolution + override-warning lines for visibility. + Get-Content -Path $log -ErrorAction SilentlyContinue | + Where-Object { $_ -match "diagnostic overrides are active|Resolved 'staging' channel|Refusing to synthesize|Could not synthesize" } | + ForEach-Object { Write-Host $_ } + Write-Host '' + + $expectedLine = "Resolved 'staging' channel: feed=$expectedFeed" + if ($logText -notmatch [regex]::Escape($expectedLine)) { + Write-Host $logText + Write-Error "FAILED: did not resolve the expected darc feed. Expected: $expectedLine, quality=$expectedQuality" + return + } + if ($logText -notmatch [regex]::Escape("$expectedLine, quality=$expectedQuality")) { + Write-Error "FAILED: resolved the darc feed but quality was not '$expectedQuality'." + return + } + + Write-Host "PASSED: $kindLabel build resolves Aspire.* from the darc feed with quality=$expectedQuality." + Write-Host '' + Write-Host "Equivalent persistent 'config options' (drop into the apphost's aspire.config.json" + Write-Host 'to simulate this build interactively with an installed PR build):' + Write-Host '' + Write-Host @" +{ + "channel": "staging", + "overrideCliIdentityChannel": "$Identity", + "overrideCliInformationalVersion": "$infoVersion" +} +"@ + Write-Host '' + Write-Host 'Remove those override keys when you are done -- they are for local validation only.' + } + finally { + Remove-Item -Path $scratch -Recurse -Force -ErrorAction SilentlyContinue + } +} diff --git a/eng/scripts/debug-aspire-channel.sh b/eng/scripts/debug-aspire-channel.sh new file mode 100755 index 00000000000..46c799de764 --- /dev/null +++ b/eng/scripts/debug-aspire-channel.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash + +# Shared implementation for debug-staging.sh and debug-stable.sh. +# +# Purpose +# ------- +# Make an EASY-TO-GET Aspire CLI build behave like an official release-branch +# **staging** build for the purpose of validating package feed routing, WITHOUT +# having to produce a real official build or stamp a binary locally. +# +# The recommended carrier is a PR build (a real, full self-extracting `~/.aspire` +# install) acquired with `eng/scripts/get-aspire-cli-pr.sh `. Any installed +# `aspire` (or a locally built one via `--cli`) works just as well, because the +# behavior is driven entirely by two diagnostic config overrides read by +# `PackagingService` (see docs/cli-staging-validation.md): +# +# overrideCliIdentityChannel - forces the identity used for staging-feed +# routing decisions (here: `staging`). +# overrideCliInformationalVersion - forces the informational version the SHA +# derivation and version-shape (quality) +# checks read, e.g. `13.4.0-preview.1.x+`. +# +# Both flow into IConfiguration from environment variables (used here) OR from +# aspire.config.json, and are scoped to staging feed routing only -- they do NOT +# change the global identity used for hive/packages directory lookups. A CLI run +# with them set emits a one-time warning so they can never silently mis-route a +# normal invocation. +# +# What this script asserts +# ------------------------ +# It runs `aspire add --debug` in a throwaway directory whose +# aspire.config.json pins `channel: staging`, then asserts the debug log contains +# Resolved 'staging' channel: feed=, quality= +# where the feed is the SHA-specific darc-pub-microsoft-aspire- +# feed. The `aspire add` step is expected to fail later (there is no real apphost +# project in the scratch directory); only the feed-routing log line is validated. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Default version shapes for the 13.4 release branch. Override with --version. +readonly DEFAULT_STAGING_VERSION="13.4.0-preview.1.26280.6" +readonly DEFAULT_STABLE_VERSION="13.4.0" + +say() { printf '%s\n' "$*"; } +say_err() { printf 'error: %s\n' "$*" >&2; } + +print_usage() { + local invoked_as="$1" + cat < [options] [-- ] + +Simulates an official ${KIND_LABEL} build and validates that the CLI resolves +Aspire.* packages from the SHA-specific darc feed for . + +Required: + --sha Commit hash of the darc feed to target (>= 8 hex chars). + Use a real release-branch build commit if you intend to + actually restore packages; any 8+ hex value is fine for + inspecting/asserting the resolved feed URL. + +Options: + --pr Install that PR's build first (via get-aspire-cli-pr.sh) + and target it. Omit to use an already-installed CLI. + --cli Path to the aspire CLI to drive. Default: 'aspire' on + PATH, else ~/.aspire/bin/aspire. + --version Override the informational version (without +). + Default for ${KIND_LABEL}: ${DEFAULT_VERSION}. + --identity Override the identity used for staging-feed routing. + Default: staging. + --package Package to use for the validation 'aspire add'. + Default: foundry. + --shell Instead of the one-shot validation, drop into an + interactive subshell where the target CLI behaves like + this ${KIND_LABEL} build for EVERY 'aspire' command + (aspire new, add, run, ...). The overrides are exported + only into that subshell; they vanish when you exit it. + --print-env Print the 'export' lines for this build to stdout so you + can apply them to your current shell, e.g. + eval "\$(${invoked_as} --sha --print-env)" + Every 'aspire' command in that shell then behaves like + this build until you 'unset' the variables (printed too). + -h, --help Show this help. + +Anything after '--' is passed through to the aspire invocation. + +Examples: + ${invoked_as} --sha 1a2b3c4d5e6f7a8b + ${invoked_as} --pr 17743 --sha 1a2b3c4d5e6f7a8b + ${invoked_as} --cli ./artifacts/bin/Aspire.Cli/Debug/net10.0/aspire --sha 1a2b3c4d + # Install a PR build and explore it interactively as a staging build: + ${invoked_as} --pr 17743 --sha 1a2b3c4d5e6f7a8b --shell +USAGE +} + +# run_debug_channel [args...] +# kind: "staging" (prerelease-shaped) | "stable" (stable-shaped) +run_debug_channel() { + local kind="$1"; shift + local invoked_as="$1"; shift + + case "$kind" in + staging) KIND_LABEL="staging (prerelease-shaped)"; DEFAULT_VERSION="$DEFAULT_STAGING_VERSION"; EXPECTED_QUALITY="Both" ;; + stable) KIND_LABEL="staging (stable-shaped)"; DEFAULT_VERSION="$DEFAULT_STABLE_VERSION"; EXPECTED_QUALITY="Stable" ;; + *) say_err "unknown kind '$kind'"; return 2 ;; + esac + + local sha="" pr="" cli_path="" version="" identity="staging" package="foundry" + local mode="validate" + local -a passthrough=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --sha) sha="${2:-}"; shift 2 ;; + --pr) pr="${2:-}"; shift 2 ;; + --cli) cli_path="${2:-}"; shift 2 ;; + --version) version="${2:-}"; shift 2 ;; + --identity) identity="${2:-}"; shift 2 ;; + --package) package="${2:-}"; shift 2 ;; + --shell) mode="shell"; shift ;; + --print-env) mode="printenv"; shift ;; + -h|--help) print_usage "$invoked_as"; return 0 ;; + --) shift; passthrough=("$@"); break ;; + *) say_err "unknown argument '$1'"; print_usage "$invoked_as" >&2; return 2 ;; + esac + done + + if [[ -z "$sha" ]]; then + say_err "--sha is required." + print_usage "$invoked_as" >&2 + return 2 + fi + + # The darc feed name is built from the first 8 chars of the commit hash, so + # require at least that many hex characters (full hashes are accepted). + if [[ ! "$sha" =~ ^[0-9a-fA-F]{8,40}$ ]]; then + say_err "--sha must be 8-40 hexadecimal characters (got '$sha')." + return 2 + fi + + [[ -n "$version" ]] || version="$DEFAULT_VERSION" + + # Lowercase the first 8 chars to match how PackagingService derives the feed. + local sha8 + sha8="$(printf '%s' "${sha:0:8}" | tr '[:upper:]' '[:lower:]')" + local expected_feed="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-${sha8}/nuget/v3/index.json" + local info_version="${version}+${sha}" + + # --print-env: emit shell-applicable export/unset lines and stop. This mode is + # CLI-agnostic on purpose -- the three keys drive ANY 'aspire' on PATH, so the + # developer can install/upgrade the carrier build independently. Intended use: + # eval "$(debug-staging.sh --sha --print-env)" + if [[ "$mode" == "printenv" ]]; then + cat <> Installing PR #${pr} build via get-aspire-cli-pr.sh ..." + "${SCRIPT_DIR}/get-aspire-cli-pr.sh" "$pr" + fi + + if [[ -z "$cli_path" ]]; then + if command -v aspire >/dev/null 2>&1; then + cli_path="$(command -v aspire)" + elif [[ -x "$HOME/.aspire/bin/aspire" ]]; then + cli_path="$HOME/.aspire/bin/aspire" + else + say_err "No aspire CLI found. Install a PR build (--pr ), pass --cli , or put 'aspire' on PATH." + return 1 + fi + fi + if [[ ! -x "$cli_path" ]]; then + say_err "CLI path '$cli_path' is not executable." + return 1 + fi + # Resolve to an absolute path because the validation step runs from a scratch + # working directory, where a relative --cli would no longer resolve. + case "$cli_path" in + /*) : ;; + *) cli_path="$(cd "$(dirname "$cli_path")" && pwd)/$(basename "$cli_path")" ;; + esac + + say "" + say "Simulating an official ${KIND_LABEL} build" + say " CLI: $cli_path" + say " identity override: $identity" + say " version override: $info_version" + say " expected feed: $expected_feed" + say " expected quality: $EXPECTED_QUALITY" + say "" + + # --shell: drop into an interactive subshell where the target CLI behaves like + # this build for EVERY 'aspire' command. The overrides live only in this child + # shell's environment, so exiting it fully restores normal behavior -- nothing + # is written to global/aspire.config.json. The target CLI's directory is put + # first on PATH so a bare 'aspire' resolves to it (handles --cli pointing at a + # local build as well as an installed PR build). + if [[ "$mode" == "shell" ]]; then + local cli_dir + cli_dir="$(dirname "$cli_path")" + # Redirect NuGet's global packages folder to an isolated, per-sha directory + # so packages restored from the simulated staging feed (which can collide in + # version with packages already cached from real feeds) never contaminate the + # developer's real global cache (~/.nuget/packages by default). The directory + # is keyed by the simulated sha so repeat sessions reuse the same isolated + # cache, and is left in place on exit (it lives under the system temp dir). + local nuget_packages="${TMPDIR:-/tmp}/aspire-debug-nuget/${sha8}" + mkdir -p "$nuget_packages" + say ">> Launching an interactive subshell. Run 'aspire new', 'aspire add', etc." + say " 'aspire' resolves to: $cli_path" + say " NuGet packages cache: $nuget_packages (isolated from your global cache)" + say " Type 'exit' to leave and restore normal CLI behavior." + say "" + channel="staging" \ + overrideCliIdentityChannel="$identity" \ + overrideCliInformationalVersion="$info_version" \ + NUGET_PACKAGES="$nuget_packages" \ + PATH="${cli_dir}:${PATH}" \ + ASPIRE_DEBUG_BUILD_PROMPT="aspire(${kind}:${sha8})" \ + "${SHELL:-/bin/bash}" -i + return $? + fi + + # Throwaway working directory pinned to channel: staging so 'aspire add' + # filters to the synthesized staging channel. Created securely; cleaned up + # on exit. No real apphost project lives here, so 'add' will ultimately fail + # after feed routing has already been logged -- that is expected. + local scratch + scratch="$(mktemp -d)" + trap 'rm -rf "$scratch"' RETURN + cat > "${scratch}/aspire.config.json" <> Running: aspire add ${package} --debug ${passthrough[*]:-}" + say " (feed routing is logged before the add step fails on the missing apphost)" + say "" + + # The overrides are scoped to THIS invocation only (no export, no persisted + # config), so they can't leak into the developer's other aspire commands. + # 'aspire add' is allowed to exit non-zero; success is decided by the log. + set +e + ( cd "$scratch" && \ + overrideCliIdentityChannel="$identity" \ + overrideCliInformationalVersion="$info_version" \ + "$cli_path" add "$package" --debug "${passthrough[@]+"${passthrough[@]}"}" ) > "$log" 2>&1 + set -e + + # Echo the resolution + override-warning lines for visibility. + grep -E "diagnostic overrides are active|Resolved 'staging' channel|Refusing to synthesize|Could not synthesize" "$log" || true + say "" + + local resolved_line + resolved_line="$(grep -F "Resolved 'staging' channel: feed=${expected_feed}" "$log" || true)" + if [[ -z "$resolved_line" ]]; then + say_err "FAILED: did not resolve the expected darc feed." + say_err "Expected: Resolved 'staging' channel: feed=${expected_feed}, quality=${EXPECTED_QUALITY}" + say_err "See full debug log for details:" + sed 's/^/ /' "$log" >&2 + return 1 + fi + + if [[ "$resolved_line" != *"quality=${EXPECTED_QUALITY}"* ]]; then + say_err "FAILED: resolved the darc feed but quality was not '${EXPECTED_QUALITY}'." + say_err " $resolved_line" + return 1 + fi + + say "PASSED: ${KIND_LABEL} build resolves Aspire.* from the darc feed with quality=${EXPECTED_QUALITY}." + say "" + say "Equivalent persistent 'config options' (drop into the apphost's aspire.config.json" + say "to simulate this build interactively with an installed PR build):" + say "" + cat <= 8 hex chars). Use a real + release-branch build commit to actually restore packages; any 8+ hex value + is fine for inspecting/asserting the resolved feed URL. + +.PARAMETER Pr + Install that PR's build first (via get-aspire-cli-pr.ps1) and target it. + Omit to use an already-installed CLI. + +.PARAMETER Cli + Path to the aspire CLI to drive. Default: 'aspire' on PATH, else + ~/.aspire/bin/aspire. + +.PARAMETER Version + Override the informational version (without +). Default: 13.4.0. + +.PARAMETER Identity + Override the identity used for staging-feed routing. Default: staging. + +.PARAMETER Package + Package to use for the validation 'aspire add'. Default: foundry. + +.PARAMETER PassThrough + Extra arguments passed through to the aspire invocation. + +.EXAMPLE + ./debug-stable.ps1 -Sha 1a2b3c4d5e6f7a8b + +.EXAMPLE + ./debug-stable.ps1 -Pr 17743 -Sha 1a2b3c4d5e6f7a8b +#> + +[CmdletBinding()] +param( + [string]$Sha, + [string]$Pr, + [string]$Cli, + [string]$Version, + [string]$Identity = 'staging', + [string]$Package = 'foundry', + [switch]$Shell, + [switch]$PrintEnv, + [Parameter(ValueFromRemainingArguments = $true)][string[]]$PassThrough = @() +) + +. (Join-Path (Split-Path -Parent $PSCommandPath) 'debug-aspire-channel.ps1') + +Invoke-DebugChannel -Kind 'stable' -Sha $Sha -Pr $Pr -Cli $Cli -Version $Version ` + -Identity $Identity -Package $Package -Shell:$Shell -PrintEnv:$PrintEnv -PassThrough $PassThrough diff --git a/eng/scripts/debug-stable.sh b/eng/scripts/debug-stable.sh new file mode 100755 index 00000000000..47f797efeb8 --- /dev/null +++ b/eng/scripts/debug-stable.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Simulate an official STABLE-shaped staging build (e.g. 13.4.0) and validate that +# the CLI resolves Aspire.* from its SHA-specific darc feed. +# +# This is the scenario from https://github.com/microsoft/aspire/issues/17527: +# a stable-shaped release-branch build still resolves from its own darc feed +# (quality=Stable), not nuget.org. +# +# See docs/cli-staging-validation.md for the full validation matrix. + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/debug-aspire-channel.sh" + +run_debug_channel stable "debug-stable.sh" "$@" diff --git a/eng/scripts/debug-staging.ps1 b/eng/scripts/debug-staging.ps1 new file mode 100644 index 00000000000..d44261462f9 --- /dev/null +++ b/eng/scripts/debug-staging.ps1 @@ -0,0 +1,63 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Simulate an official PRERELEASE-shaped staging build (e.g. 13.4.0-preview.*) + and validate that the CLI resolves Aspire.* from its SHA-specific darc feed. + +.DESCRIPTION + This is the scenario from https://github.com/microsoft/aspire/issues/17744: + a prerelease-shaped staging build must use the darc-pub-microsoft-aspire- + feed (quality=Both), NOT the shared daily feed. + + See docs/cli-staging-validation.md for the full validation matrix. + +.PARAMETER Sha + Commit hash of the darc feed to target (>= 8 hex chars). Use a real + release-branch build commit to actually restore packages; any 8+ hex value + is fine for inspecting/asserting the resolved feed URL. + +.PARAMETER Pr + Install that PR's build first (via get-aspire-cli-pr.ps1) and target it. + Omit to use an already-installed CLI. + +.PARAMETER Cli + Path to the aspire CLI to drive. Default: 'aspire' on PATH, else + ~/.aspire/bin/aspire. + +.PARAMETER Version + Override the informational version (without +). Default: 13.4.0-preview.1.26280.6. + +.PARAMETER Identity + Override the identity used for staging-feed routing. Default: staging. + +.PARAMETER Package + Package to use for the validation 'aspire add'. Default: foundry. + +.PARAMETER PassThrough + Extra arguments passed through to the aspire invocation. + +.EXAMPLE + ./debug-staging.ps1 -Sha 1a2b3c4d5e6f7a8b + +.EXAMPLE + ./debug-staging.ps1 -Pr 17743 -Sha 1a2b3c4d5e6f7a8b +#> + +[CmdletBinding()] +param( + [string]$Sha, + [string]$Pr, + [string]$Cli, + [string]$Version, + [string]$Identity = 'staging', + [string]$Package = 'foundry', + [switch]$Shell, + [switch]$PrintEnv, + [Parameter(ValueFromRemainingArguments = $true)][string[]]$PassThrough = @() +) + +. (Join-Path (Split-Path -Parent $PSCommandPath) 'debug-aspire-channel.ps1') + +Invoke-DebugChannel -Kind 'staging' -Sha $Sha -Pr $Pr -Cli $Cli -Version $Version ` + -Identity $Identity -Package $Package -Shell:$Shell -PrintEnv:$PrintEnv -PassThrough $PassThrough diff --git a/eng/scripts/debug-staging.sh b/eng/scripts/debug-staging.sh new file mode 100755 index 00000000000..483fa594fd5 --- /dev/null +++ b/eng/scripts/debug-staging.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Simulate an official PRERELEASE-shaped staging build (e.g. 13.4.0-preview.*) and +# validate that the CLI resolves Aspire.* from its SHA-specific darc feed. +# +# This is the scenario from https://github.com/microsoft/aspire/issues/17744: +# a prerelease-shaped staging build must use the darc-pub-microsoft-aspire- +# feed (quality=Both), NOT the shared daily feed. +# +# See docs/cli-staging-validation.md for the full validation matrix. + +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/debug-aspire-channel.sh" + +run_debug_channel staging "debug-staging.sh" "$@" diff --git a/eng/scripts/verify-cli-archive.ps1 b/eng/scripts/verify-cli-archive.ps1 index 321e1cb555f..842eafcb95d 100644 --- a/eng/scripts/verify-cli-archive.ps1 +++ b/eng/scripts/verify-cli-archive.ps1 @@ -8,8 +8,7 @@ 2. Extracts the CLI archive to a temp location 3. Verifies the archive shape contains the native CLI payload and no install-route sidecar 4. Runs 'aspire --version' to validate the binary executes - 5. Runs 'aspire new aspire-starter' to test C# starter creation - 6. Cleans up temp directories + 5. Cleans up temp directories .PARAMETER ArchivePath Path to the CLI archive (.zip or .tar.gz) @@ -117,29 +116,6 @@ function Test-ArchiveSidecar { Write-Step "$ridFamily-* archive correctly omits the install-route sidecar." } -function Test-CSharpStarterProject { - param( - [Parameter(Mandatory = $true)][string]$AspireBin, - [Parameter(Mandatory = $true)][string]$ProjectRoot - ) - - Write-Step "Running 'aspire new aspire-starter --name VerifyApp --output $ProjectRoot --non-interactive --nologo --suppress-agent-init'..." - & $AspireBin new aspire-starter --name VerifyApp --output $ProjectRoot --non-interactive --nologo --suppress-agent-init 2>&1 | Write-Host - if ($LASTEXITCODE -ne 0) { - Write-Err "'aspire new aspire-starter' failed with exit code $LASTEXITCODE" - exit 1 - } - - $appHostDir = Join-Path $ProjectRoot "VerifyApp.AppHost" - if (-not (Test-Path $appHostDir)) { - Write-Err "Expected project directory 'VerifyApp.AppHost' not found after 'aspire new aspire-starter'" - Get-ChildItem $ProjectRoot | Format-Table - exit 1 - } - - Write-Ok "'aspire new aspire-starter' created project successfully" -} - $userHome = Get-UserHome $verifyTmpDir = $null $aspireBackup = $null @@ -248,16 +224,20 @@ try { Write-Host " Version: $versionOutput" Write-Ok "'aspire --version' succeeded" - # Step 4: Create starter project with aspire new. The C# starter exercises - # template engine + bundle self-extraction without requiring a NuGet restore. - # The TypeScript starter check (#17274) is intentionally not invoked here: - # its packager-managed AppHost bootstrap triggers a NuGet restore whose - # transitive deps resolve to api.nuget.org, which the 1ES signed-build agent - # has no egress to. Re-adding it requires either an internal NuGet mirror - # config or skipping the auto-restore. Tracked in #17345. - $csharpProjectDir = Join-Path $verifyTmpDir "VerifyApp" - New-Item -ItemType Directory -Path $csharpProjectDir -Force | Out-Null - Test-CSharpStarterProject -AspireBin $aspireBin -ProjectRoot $csharpProjectDir + # Note: 'aspire new aspire-starter' was previously invoked here to exercise the + # template engine + bundle self-extraction path. It has been removed because the + # template lookup is not actually offline — it queries NuGet feeds via + # TemplateNuGetConfigService.ResolveTemplatePackageAsync. The step only ever + # succeeded on release branches because builds were mis-baked with + # AspireCliChannel=stable, which routed the lookup to nuget.org and found a + # previously-shipped Aspire.ProjectTemplates version. Once #17528 corrected the + # release-branch builds to bake AspireCliChannel=staging, the implicit identity + # channel switched to a staging feed that is not reachable from the 1ES signed- + # build agent, and the step started failing with "No template versions were + # found." This mirrors the prior removal of the TypeScript starter check + # (#17274 / tracked in #17345) for the same egress reason. Re-adding meaningful + # starter coverage in the signed-build verifier requires either an internal + # NuGet mirror or a no-network template path. Write-Host "" Write-Host "==========================================" diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index cf62829b474..e8349783a1a 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -178,9 +178,17 @@ CommandResult AddCommandFromExitCode(int exitCode) ? ProfilingTelemetry.Values.AddPackageMatchKindExact : ProfilingTelemetry.Values.AddPackageMatchKindNone; - if (filteredPackagesWithShortName.Count == 0 && integrationName is not null && version is not null && !_hostEnvironment.SupportsInteractiveInput) + // Non-interactive mode never falls back to fuzzy search: in interactive mode the user picks + // from the fuzzy candidates, but a script/CI invocation would otherwise silently auto-select + // distinctPackages.First() in GetPackageByInteractiveFlow and install the wrong package + // (https://github.com/microsoft/aspire/issues/17724). Refusing with an actionable error + // forces the caller to supply an exact package id or friendly name. + if (filteredPackagesWithShortName.Count == 0 && integrationName is not null && !_hostEnvironment.SupportsInteractiveInput) { - throw new EmptyChoicesException(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.SpecifiedVersionRequiresExactPackageMatch, integrationName)); + var message = version is not null + ? string.Format(CultureInfo.CurrentCulture, AddCommandStrings.SpecifiedVersionRequiresExactPackageMatch, integrationName) + : string.Format(CultureInfo.CurrentCulture, AddCommandStrings.NonInteractiveRequiresExactPackageMatch, integrationName); + throw new EmptyChoicesException(message); } if (filteredPackagesWithShortName.Count == 0 && integrationName is not null) @@ -197,16 +205,33 @@ CommandResult AddCommandFromExitCode(int exitCode) : ProfilingTelemetry.Values.AddPackageMatchKindNone; } - // If we didn't match any, show a complete list. If we matched one, and its + // If the user supplied a partial/fuzzy search term, keep the package prompt even when + // the fallback only found one candidate; otherwise `aspire add kube` can silently add + // the lone fuzzy match without asking the interactive user to confirm it. + var promptForSingleFuzzyPackage = packageMatchKind == ProfilingTelemetry.Values.AddPackageMatchKindFuzzy; + + // If we didn't match any, show a complete list. If we matched one, and it's // an exact match, then we still prompt, but it will only prompt for // the version. If there is more than one match then we prompt. (string FriendlyName, NuGetPackage Package, PackageChannel Channel) selectedNuGetPackage; selectedNuGetPackage = filteredPackagesWithShortName.Count switch { - 0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(effectiveAppHostProjectFile.Directory!, packagesWithShortName, integrationName, version, cancellationToken), - 1 when filteredPackagesWithShortName[0].Package.Version == version + 0 => await GetPackageByInteractiveFlowWithNoMatchesMessage( + effectiveAppHostProjectFile.Directory!, + packagesWithShortName, + integrationName, + version, + cancellationToken, + promptForSinglePackage: integrationName is not null), + 1 when packageMatchKind == ProfilingTelemetry.Values.AddPackageMatchKindExact + && filteredPackagesWithShortName[0].Package.Version == version => filteredPackagesWithShortName[0], - _ => await GetPackageByInteractiveFlow(effectiveAppHostProjectFile.Directory!, filteredPackagesWithShortName, version, cancellationToken) + _ => await GetPackageByInteractiveFlow( + effectiveAppHostProjectFile.Directory!, + filteredPackagesWithShortName, + version, + cancellationToken, + promptForSingleFuzzyPackage) }; using (var selectPackageActivity = _profilingTelemetry.StartAddSelectPackage(integrationName, version)) { @@ -357,14 +382,21 @@ CommandResult AddCommandFromExitCode(int exitCode) return versions; } - private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken) + private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow( + DirectoryInfo workingDirectory, + IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, + string? preferredVersion, + CancellationToken cancellationToken, + bool promptForSinglePackage = false) { var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id).ToArray(); - // If there is only one package, we can skip the prompt and just use it. + // Exact matches can skip the package prompt when one package remains. Fuzzy/no-match + // fallbacks opt into prompting so interactive users confirm the candidate first. // In non-interactive mode, auto-select the first package. var selectedPackage = distinctPackages.Length switch { + 1 when promptForSinglePackage && _hostEnvironment.SupportsInteractiveInput => await PromptForIntegrationAsync(distinctPackages, cancellationToken), 1 => distinctPackages.First(), > 1 when !_hostEnvironment.SupportsInteractiveInput => distinctPackages.First(), > 1 => await PromptForIntegrationAsync(distinctPackages, cancellationToken), @@ -440,14 +472,20 @@ CommandResult AddCommandFromExitCode(int exitCode) return await _prompter.PromptForIntegrationVersionAsync(packages, cancellationToken); } - private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, string? preferredVersion, CancellationToken cancellationToken) + private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage( + DirectoryInfo workingDirectory, + IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, + string? searchTerm, + string? preferredVersion, + CancellationToken cancellationToken, + bool promptForSinglePackage = false) { if (searchTerm is not null) { InteractionService.DisplaySubtleMessage(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.NoPackagesMatchedSearchTerm, searchTerm)); } - return await GetPackageByInteractiveFlow(workingDirectory, possiblePackages, preferredVersion, cancellationToken); + return await GetPackageByInteractiveFlow(workingDirectory, possiblePackages, preferredVersion, cancellationToken, promptForSinglePackage); } } diff --git a/src/Aspire.Cli/Commands/CacheCommand.cs b/src/Aspire.Cli/Commands/CacheCommand.cs index 22d01fe6d7e..4cb30de7a6b 100644 --- a/src/Aspire.Cli/Commands/CacheCommand.cs +++ b/src/Aspire.Cli/Commands/CacheCommand.cs @@ -40,6 +40,14 @@ protected override Task ExecuteAsync(ParseResult parseResult, Can filesDeleted += ClearDirectoryContents(ExecutionContext.CacheDirectory); filesDeleted += ClearDirectoryContents(ExecutionContext.SdksDirectory); filesDeleted += ClearDirectoryContents(ExecutionContext.PackagesDirectory); + // Wipe the staging NuGet package cache too. Producers (PrebuiltAppHostServer's + // temporary nuget.config for the staging channel) deposit SHA-keyed package + // caches under /.nugetpackages/; clearing them lets users + // recover wedged staging restores without filesystem surgery. We hand the parent + // directory to ClearDirectoryContents so each SHA subdirectory is wiped while + // the parent itself stays in place for the next staging restore. + filesDeleted += ClearDirectoryContents( + new DirectoryInfo(CliPathHelper.GetStagingNuGetPackagesDirectory(ExecutionContext.AspireHomeDirectory))); filesDeleted += ClearDirectoryContents( ExecutionContext.LogsDirectory, skipFile: f => f.FullName.Equals(currentLogFilePath, StringComparison.OrdinalIgnoreCase)); diff --git a/src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs b/src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs index 17646b8a434..54779e410c9 100644 --- a/src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs +++ b/src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs @@ -23,13 +23,24 @@ internal sealed class IntegrationPackageSearchService( public async Task> GetIntegrationPackagesWithChannelsAsync(DirectoryInfo workingDirectory, string? configuredChannel, CancellationToken cancellationToken) { + // `configuredChannel` (from a polyglot apphost's aspire.config.json) is forwarded + // as `requestedChannelName` so PackagingService can synthesize the staging channel + // for out-of-tree apphosts whose directory wasn't picked up by + // ConfigurationHelper.RegisterSettingsFiles. var allChannels = await packagingService.GetChannelsAsync(cancellationToken, configuredChannel); - if (!string.IsNullOrEmpty(configuredChannel)) - { - allChannels = allChannels.Where(c => string.Equals(c.Name, configuredChannel, StringComparison.OrdinalIgnoreCase)); - } - + // Channels included in the search: + // * Implicit channel: always. + // * Explicit channels (stable, daily, staging, custom): when PR hives exist OR the + // apphost has pinned an explicit channel via aspire.config.json. + // + // What this method MUST NOT do is narrow the explicit channel set to just the pinned + // channel. That was the root cause of https://github.com/microsoft/aspire/issues/17724 + // and https://github.com/microsoft/aspire/issues/17725: a TS apphost pinned to a + // Quality.Stable channel ended up with prerelease=false queries everywhere and + // prerelease-only packages (e.g. Aspire.Hosting.Foundry) became invisible. The implicit + // channel (Quality.Both) must always participate so prerelease packages are reachable + // even when the explicit pin is Stable-quality. var hasHives = executionContext.GetHiveCount() > 0; var channels = hasHives || !string.IsNullOrEmpty(configuredChannel) ? allChannels diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index f7cf931cb75..9a2f47624df 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -177,6 +177,49 @@ private async Task PromptForAppHostLanguageAsync(IReadOnlyList s return selected.LanguageId; } + private static NuGetPackage? TryGetCurrentCliTemplateVersionPackage(PackageChannel selectedChannel, NuGetPackage[] packages, bool hasPrHives) + { + if (VersionHelper.TryGetCurrentCliVersionMatch( + packages, + p => p.Version, + out var cliVersionPackage, + channelName: selectedChannel.Name, + hasPrHives: hasPrHives)) + { + return cliVersionPackage; + } + + if (packages.Length > 0 && + selectedChannel.Type is PackageChannelType.Explicit && + !string.Equals(selectedChannel.Name, PackageChannelNames.Stable, StringComparisons.ChannelName) && + !VersionHelper.IsLocalBuildChannel(selectedChannel.Name)) + { + // Prerelease channels (daily, staging) filter the shipped stable package out of channel + // search even when the channel's feed mappings can still restore it (they fall back to + // nuget.org). For those channels, pinning to the running CLI's SDK version keeps the + // bundled server and the restored Aspire packages in lock-step. Without this, `aspire new + // --channel daily` on a shipped 13.4 CLI floats templates to a 13.5 daily preview, which + // then breaks the bundled 13.4 AppHost server with `Aspire.TypeSystem, Version=13.5.0.0` + // assembly load errors followed by `No language support found for: typescript/nodejs`. + // + // The stable channel is excluded here on purpose: it does not apply that filter, so a + // "no exact match" outcome means the CLI version is genuinely not published on the stable + // feed (the CLI is daily-shape, staging-shape, or PR-shape `13.4.0-pr.X.gY`). Forcing + // it through would either contradict the user's explicit `--channel stable` request or + // write an unpublishable version into `apphost.cs` that NuGet restore cannot satisfy. + // Fall through to the OrderByDescending picker so the user gets the highest shipped + // stable package they actually asked for. + return new NuGetPackage + { + Id = TemplateNuGetConfigService.TemplatesPackageName, + Version = VersionHelper.GetDefaultSdkVersion(), + Source = selectedChannel.SourceDetails + }; + } + + return null; + } + private async Task<(bool Success, string? LanguageId)> ResolveSelectedLanguageAsync(ITemplate template, ParseResult parseResult, CancellationToken cancellationToken) { var explicitLanguageId = ParseExplicitLanguageId(parseResult); @@ -379,14 +422,7 @@ private async Task ResolveCliTemplateVersionAsync( .ToArray(); var hasPrHives = ExecutionContext.GetHiveCount() > 0; - NuGetPackage? package = VersionHelper.TryGetCurrentCliVersionMatch( - packages, - p => p.Version, - out var cliVersionPackage, - channelName: selectedChannel.Name, - hasPrHives: hasPrHives) - ? cliVersionPackage - : null; + var package = TryGetCurrentCliTemplateVersionPackage(selectedChannel, packages, hasPrHives); package ??= packages .OrderByDescending(p => Semver.SemVersion.Parse(p.Version, Semver.SemVersionStyles.Strict), Semver.SemVersion.PrecedenceComparer) @@ -439,6 +475,15 @@ protected override async Task ExecuteAsync(ParseResult parseResul } var version = parseResult.GetValue(s_versionOption); + // Precedence for the channel written into TemplateInputs.Channel: + // 1. Explicit --channel argument (user override always wins). + // 2. Channel returned by ResolveCliTemplateVersionAsync (CLI-runtime templates). + // 3. The running CLI's IdentityChannel, when it matches a registered Explicit + // channel — needed for TemplateRuntime.DotNet starters (aspire-starter, + // aspire-starter-csharp-typescript) which otherwise resolve + // Aspire.ProjectTemplates from the Implicit (nuget.org) channel regardless + // of CLI identity, and also for CLI-runtime templates invoked with --version + // which short-circuits the resolver below. string? resolvedChannelName = null; if (ShouldResolveCliTemplateVersion(template) && string.IsNullOrWhiteSpace(version)) @@ -453,13 +498,30 @@ protected override async Task ExecuteAsync(ParseResult parseResul resolvedChannelName = resolveResult.ChannelName; } + // Apply the channel precedence as a single coalesce. The identity fallback lives + // here, not inside ResolveCliTemplateVersionAsync, because that resolver only runs + // on the CLI-runtime / no-explicit-version branch above. The two paths that need + // the identity hint are precisely the ones the resolver does NOT visit: + // * TemplateRuntime.DotNet templates (aspire-starter family) — the bug this fix + // addresses; without forwarding, DotNetTemplateFactory searches only the + // Implicit (nuget.org) channel regardless of CLI identity. + // * CLI-runtime templates invoked with --version, which short-circuits the + // resolver and would otherwise leave inputs.Channel null. + // Keeping the fallback out of the resolver also keeps the resolver's role narrow: + // it performs version negotiation across channels and reports the channel that won; + // the identity hint is a different policy ("label the project with the CLI's own + // channel") that should not influence version selection. + resolvedChannelName = parseResult.GetValue(_channelOption) + ?? resolvedChannelName + ?? await ResolveIdentityChannelNameAsync(cancellationToken); + var inputs = new TemplateInputs { Name = parseResult.GetValue(s_nameOption), Output = parseResult.GetValue(s_outputOption), Source = source, Version = version, - Channel = parseResult.GetValue(_channelOption) ?? resolvedChannelName, + Channel = resolvedChannelName, Language = selectedLanguageId }; var templateResult = await template.ApplyTemplateAsync(inputs, parseResult, cancellationToken); @@ -483,6 +545,33 @@ private static bool ShouldResolveCliTemplateVersion(ITemplate template) return template.Runtime is TemplateRuntime.Cli; } + /// + /// Resolves to a registered channel name + /// from the packaging service. Returns the channel name when an Explicit channel matches the + /// identity (e.g. daily, staging, stable, pr-<N>); returns + /// when there is no identity, when no Explicit channel matches, or + /// when only the Implicit (nuget.org) channel is registered. A result + /// intentionally lets the downstream template path consult the Implicit channel and avoids + /// writing a per-project channel pin into the new project's NuGet configuration. + /// + private async Task ResolveIdentityChannelNameAsync(CancellationToken cancellationToken) + { + var identity = ExecutionContext.IdentityChannel; + if (string.IsNullOrWhiteSpace(identity)) + { + return null; + } + + var channels = await _packagingService.GetChannelsAsync(cancellationToken, identity); + var match = channels.FirstOrDefault(c => + string.Equals(c.Name, identity, StringComparisons.ChannelName)); + + // Only persist Explicit channel names — Implicit channels (the nuget.org fallback) + // are deliberately left unpinned so `aspire add` and later restores use ambient + // NuGet configuration. Mirrors the same rule applied at the end of + // ResolveCliTemplateVersionAsync. + return match is { Type: PackageChannelType.Explicit } ? match.Name : null; + } } internal interface INewCommandPrompter diff --git a/src/Aspire.Cli/Packaging/NuGetConfigMerger.cs b/src/Aspire.Cli/Packaging/NuGetConfigMerger.cs index 2b3fd441efd..38d578b5aeb 100644 --- a/src/Aspire.Cli/Packaging/NuGetConfigMerger.cs +++ b/src/Aspire.Cli/Packaging/NuGetConfigMerger.cs @@ -1032,7 +1032,13 @@ private static void AddGlobalPackagesFolderConfiguration(NuGetConfigContext conf AddGlobalPackagesFolderConfiguration(configContext.Configuration); } - internal static void AddGlobalPackagesFolderConfiguration(XElement configuration) + // Default workspace-relative cache used when no explicit path is supplied. Matches the + // long-standing 'aspire init / aspire new' convention of putting a per-workspace + // .nugetpackages folder next to the merged nuget.config so staging-vs-stable cache + // poisoning doesn't bleed into the user's global ~/.nuget/packages folder. + internal const string DefaultGlobalPackagesFolderValue = ".nugetpackages"; + + internal static void AddGlobalPackagesFolderConfiguration(XElement configuration, string? globalPackagesFolderValue = null) { // Check if config section already exists var config = configuration.Element("config"); @@ -1048,10 +1054,13 @@ internal static void AddGlobalPackagesFolderConfiguration(XElement configuration if (existingGlobalPackagesFolder is null) { - // Add globalPackagesFolder configuration + // Add globalPackagesFolder configuration. Callers (e.g. PrebuiltAppHostServer's + // temporary nuget.config) supply an absolute path when the config file itself is + // ephemeral so the cached packages outlive the config — otherwise NuGet would + // resolve the relative ".nugetpackages" under the about-to-be-deleted temp dir. var globalPackagesFolderAdd = new XElement("add"); globalPackagesFolderAdd.SetAttributeValue("key", "globalPackagesFolder"); - globalPackagesFolderAdd.SetAttributeValue("value", ".nugetpackages"); + globalPackagesFolderAdd.SetAttributeValue("value", globalPackagesFolderValue ?? DefaultGlobalPackagesFolderValue); config.Add(globalPackagesFolderAdd); } } diff --git a/src/Aspire.Cli/Packaging/PackageChannel.cs b/src/Aspire.Cli/Packaging/PackageChannel.cs index 330821ff384..c619b2f7afb 100644 --- a/src/Aspire.Cli/Packaging/PackageChannel.cs +++ b/src/Aspire.Cli/Packaging/PackageChannel.cs @@ -85,12 +85,15 @@ public async Task> GetTemplatePackagesAsync(DirectoryI .DistinctBy(p => $"{p.Id}-{p.Version}"); // When doing a `dotnet package search` the results may include stable packages even when searching for - // prerelease packages. This filters out this noise. + // prerelease packages. Keep the current CLI/SDK version so shipped CLIs can resolve their + // matching template package from daily/staging feeds, then filter out the remaining noise. + var currentCliVersion = VersionHelper.GetDefaultSdkVersion(); var filteredPackages = packages.Where(p => new { SemVer = SemVersion.Parse(p.Version), Quality = Quality } switch { { Quality: PackageChannelQuality.Both } => true, { Quality: PackageChannelQuality.Stable, SemVer: { IsPrerelease: false } } => true, { Quality: PackageChannelQuality.Prerelease, SemVer: { IsPrerelease: true } } => true, + { Quality: PackageChannelQuality.Prerelease, SemVer: { IsPrerelease: false } } when string.Equals(p.Version, currentCliVersion, StringComparison.OrdinalIgnoreCase) => true, _ => false }); diff --git a/src/Aspire.Cli/Packaging/PackagingService.cs b/src/Aspire.Cli/Packaging/PackagingService.cs index 38d148d9c78..c242229d0c6 100644 --- a/src/Aspire.Cli/Packaging/PackagingService.cs +++ b/src/Aspire.Cli/Packaging/PackagingService.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Cli.Acquisition; using Aspire.Cli.Configuration; using Aspire.Cli.NuGet; using Aspire.Cli.Resources; @@ -26,9 +27,10 @@ internal interface IPackagingService /// /// On a CLI whose baked AspireCliChannel identity is daily, local, or /// pr-<N>, there is no deterministic way to produce a real staging feed: - /// the SHA-specific darc feed (darc-pub-microsoft-aspire-<hash>) only exists - /// for stable release branch builds, and falling back to the shared daily feed silently - /// resolves daily packages instead of staging ones. To avoid that downgrade + /// those identities are not officially published release-branch builds, so no SHA-specific + /// darc feed (darc-pub-microsoft-aspire-<hash>) carries their packages, and + /// falling back to the shared daily feed silently resolves daily packages instead of staging + /// ones. To avoid that downgrade /// (see ), the service refuses /// to fabricate a staging channel from those identities unless the caller has set /// overrideStagingFeed or enabled the staging feature flag. @@ -44,12 +46,44 @@ internal class PackagingService : IPackagingService // tests via InternalsVisibleTo so a single literal change can't drift. internal const string OverrideStagingFeedConfigKey = "overrideStagingFeed"; + // Diagnostic overrides for validating staging FEED ROUTING from a locally built CLI without + // having to produce a real official staging build. They are intentionally scoped to the + // staging-feed decisions in this service (they do NOT change the global + // CliExecutionContext.IdentityChannel used for hive/packages directory lookups), so a plain + // local dev build can be made to derive and resolve from a real darc-pub-microsoft-aspire- + // feed exactly the way an official staging build would. See docs/cli-staging-validation.md. + // + // overrideCliIdentityChannel - forces the identity used for staging-feed decisions + // (validated against the known channel set). Set to + // `staging` to exercise the staging-identity darc path. + // overrideCliInformationalVersion - forces the AssemblyInformationalVersion that the SHA + // derivation and version-shape (quality) checks read, + // e.g. `13.4.0-preview.1.26280.6+`. + // + // NOTE: These only route to a feed; they do not create one. They are typically useful only + // once the darc-pub-microsoft-aspire- feed actually exists for the specific commit/version + // you are emulating (i.e. an official build for that SHA has been published). Until then the + // derived feed URL resolves to nothing and restore will fail to find packages. + internal const string OverrideCliIdentityChannelConfigKey = "overrideCliIdentityChannel"; + internal const string OverrideCliInformationalVersionConfigKey = "overrideCliInformationalVersion"; + private readonly CliExecutionContext _executionContext; private readonly INuGetPackageCache _nuGetPackageCache; private readonly IFeatures _features; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly Func _processPathProvider; + // Predicate used by staging-channel synthesis to decide whether the running CLI is built + // from a stable-shaped version (no semver prerelease tag). Defaults to inspecting the + // current Aspire.Cli assembly's InformationalVersion; tests inject a deterministic value + // because the version baked into the test-host assembly varies by build configuration. + private readonly Func _isStableShapedCliVersion; + // Provides the running CLI's AssemblyInformationalVersion (which carries the + + // build metadata used to derive the SHA-specific darc-pub-microsoft-aspire- staging + // feed). Defaults to reading the Aspire.Cli assembly; tests inject a deterministic value + // because the version baked into the test-host assembly varies by build configuration, which + // otherwise makes the derived darc feed URL non-deterministic (and therefore un-assertable). + private readonly Func _cliInformationalVersionProvider; // Cached result of the staging-channel availability check. The inputs (CLI identity, // overrideStagingFeed, StagingChannelEnabled feature) are effectively static for the @@ -64,7 +98,9 @@ public PackagingService( IFeatures features, IConfiguration configuration, ILogger logger, - Func? processPathProvider = null) + Func? processPathProvider = null, + Func? isStableShapedCliVersion = null, + Func? cliInformationalVersionProvider = null) { _executionContext = executionContext; _nuGetPackageCache = nuGetPackageCache; @@ -72,6 +108,8 @@ public PackagingService( _configuration = configuration; _logger = logger; _processPathProvider = processPathProvider ?? (() => Environment.ProcessPath); + _isStableShapedCliVersion = isStableShapedCliVersion ?? IsStableShapedCliVersionDefault; + _cliInformationalVersionProvider = cliInformationalVersionProvider ?? GetCliInformationalVersionDefault; _stagingUnavailableReasonCache = new Lazy(ComputeStagingChannelUnavailableReason); } @@ -82,9 +120,16 @@ public PackagingService( // a project's aspire.config.json pins `channel: staging` on a daily/local CLI. private int _stagingRefusalLogged; private int _stagingResolutionLogged; + private int _stagingFeedDerivationFailedLogged; + private int _stagingDiagnosticOverrideLogged; public Task> GetChannelsAsync(CancellationToken cancellationToken = default, string? requestedChannelName = null) { + // Emit the diagnostic-override warning up front so any invocation that has the overrides set + // leaves a trace, regardless of whether a staging channel ends up being synthesized below + // (e.g. an override that ultimately resolves to a non-staging identity still warns). + WarnIfStagingDiagnosticOverridesActive(); + var defaultChannel = PackageChannel.CreateImplicitChannel(_nuGetPackageCache, _features, _logger); var stableChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Stable, PackageChannelQuality.Stable, new[] @@ -129,11 +174,49 @@ public Task> GetChannelsAsync(CancellationToken canc // need the channel materialized before they can match it below. var stagingChannelConfigured = string.Equals(_configuration["channel"], PackageChannelNames.Staging, StringComparisons.ChannelName); var stagingChannelRequested = string.Equals(requestedChannelName, PackageChannelNames.Staging, StringComparisons.ChannelName); - var stagingIdentityChannel = string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); + var stagingIdentityChannel = string.Equals(GetEffectiveIdentityChannel(), PackageChannelNames.Staging, StringComparisons.ChannelName); var stagingFeatureEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false); if (stagingFeatureEnabled || stagingChannelConfigured || stagingChannelRequested || stagingIdentityChannel) { - var defaultQuality = stagingChannelConfigured || stagingChannelRequested || stagingIdentityChannel ? PackageChannelQuality.Both : PackageChannelQuality.Stable; + // Default quality selection rules (per staging entry point). NOTE: quality controls + // version FILTERING only (which versions in the feed are eligible); it no longer + // selects the feed itself. Feed PROVENANCE is identity-driven inside + // ShouldUseSharedStagingFeed — a staging-identity CLI always resolves Aspire.* from its + // own SHA-specific darc-pub-microsoft-aspire- feed. + // - Explicit user opt-in (`stagingChannelConfigured`, `stagingChannelRequested`): Both. + // The user picked staging deliberately; they get the broadest matching window. + // - `stagingFeatureEnabled` only (no other staging signal): Stable. Preserves the + // pre-existing behavior of the staging feature flag. + // - `stagingIdentityChannel` (the running CLI itself self-identifies as staging): + // follows the CLI build's version shape so the eligible version window matches the + // packages the build actually shipped. + // * Stable-shaped (e.g. "13.4.0", produced during release stabilization when + // StabilizePackageVersion=true) → Stable, so resolution prefers the stable-shaped + // packages on the darc feed (the #17527 scenario). + // * Prerelease-shaped (e.g. "13.4.0-preview.1.123") → Both, so prerelease-tagged + // packages on the darc feed remain eligible. + PackageChannelQuality defaultQuality; + if (stagingIdentityChannel) + { + // When the running CLI's identity itself is staging, the synthesized channel's + // quality MUST follow the CLI build's version shape regardless of how synthesis + // was triggered. `init` and many other commands pass requestedChannelName=staging + // when identity is staging, so checking `stagingChannelRequested` first would + // short-circuit this path and re-introduce the #17527 version-filtering mismatch on + // stabilizing builds. + defaultQuality = _isStableShapedCliVersion() + ? PackageChannelQuality.Stable + : PackageChannelQuality.Both; + } + else if (stagingChannelConfigured || stagingChannelRequested) + { + defaultQuality = PackageChannelQuality.Both; + } + else + { + defaultQuality = PackageChannelQuality.Stable; + } + var stagingChannel = CreateStagingChannel(defaultQuality); if (stagingChannel is not null) { @@ -216,6 +299,122 @@ prDirectory.Parent is not { } dogfoodDirectory || return packagesDirectory.Exists ? packagesDirectory : null; } + // Returns true when the running CLI's version is stable-shaped (no semver prerelease tag). + // Used by the staging-channel synthesis to route stabilizing builds to the SHA-derived darc + // feed instead of the shared dotnet9 daily feed. Falls back to false on any error so we + // preserve the historical Both/shared-feed behavior rather than silently misrouting. + private static bool IsStableShapedCliVersionFromAssembly() + { + try + { + var version = VersionHelper.GetDefaultSdkVersion(); + return !string.IsNullOrEmpty(version) && !version.Contains('-'); + } + catch + { + return false; + } + } + + // Reads the running CLI assembly's AssemblyInformationalVersion, which carries the + + // build metadata used to derive the SHA-specific darc-pub-microsoft-aspire- staging feed. + // Returns null on any error so callers degrade gracefully (no derived feed) rather than throwing. + private static string? GetCliInformationalVersionFromAssembly() + { + try + { + return Assembly.GetExecutingAssembly() + .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .OfType() + .FirstOrDefault()?.InformationalVersion; + } + catch + { + return null; + } + } + + // Default version-shape predicate. Honors the overrideCliInformationalVersion diagnostic + // override (so a locally built CLI can present as stable- or prerelease-shaped for staging + // validation) before falling back to the real assembly version. + private bool IsStableShapedCliVersionDefault() + { + var overrideVersion = _configuration[OverrideCliInformationalVersionConfigKey]; + if (!string.IsNullOrEmpty(overrideVersion)) + { + // Stable-shaped == no semver prerelease tag. Strip build metadata (+) first so a + // commit hash that happens to contain '-' can't be misread as a prerelease tag. Example: + // "13.4.0-preview.1.26280.6+abcd-ef12" -> version part "13.4.0-preview.1.26280.6" -> prerelease + // "13.4.0+abcd-ef12" -> version part "13.4.0" -> stable + return !StripBuildMetadata(overrideVersion).Contains('-'); + } + + return IsStableShapedCliVersionFromAssembly(); + } + + // Default informational-version provider. Honors the overrideCliInformationalVersion diagnostic + // override (so the SHA-specific darc feed can be derived deterministically from a locally built + // CLI) before falling back to the real assembly informational version. + private string? GetCliInformationalVersionDefault() + { + var overrideVersion = _configuration[OverrideCliInformationalVersionConfigKey]; + if (!string.IsNullOrEmpty(overrideVersion)) + { + return overrideVersion; + } + + return GetCliInformationalVersionFromAssembly(); + } + + private static string StripBuildMetadata(string version) + { + var plusIndex = version.IndexOf('+'); + return plusIndex >= 0 ? version[..plusIndex] : version; + } + + // Returns the identity channel used for staging-feed routing decisions. Normally this is the + // CLI build's baked identity (CliExecutionContext.IdentityChannel). For local validation of + // staging feed routing, overrideCliIdentityChannel can force a different identity (validated + // against the known channel set via IdentityChannelReader.IsValidChannel) WITHOUT changing the + // global identity used elsewhere (hive/packages directory lookups), keeping the blast radius + // limited to feed provenance. Invalid override values are ignored — we fall back to the real + // identity, mirroring how overrideStagingFeed ignores malformed URLs. + private string GetEffectiveIdentityChannel() + { + var overrideChannel = _configuration[OverrideCliIdentityChannelConfigKey]; + if (!string.IsNullOrEmpty(overrideChannel) && IdentityChannelReader.IsValidChannel(overrideChannel)) + { + return overrideChannel; + } + + return _executionContext.IdentityChannel; + } + + // Emits a single warning when either staging diagnostic override is active, so a normal CLI + // invocation can't silently resolve Aspire.* from an overridden identity/feed without a trace + // in the logs. Emitted at most once per process to avoid noise across repeated GetChannelsAsync + // calls. + private void WarnIfStagingDiagnosticOverridesActive() + { + var identityOverride = _configuration[OverrideCliIdentityChannelConfigKey]; + var versionOverride = _configuration[OverrideCliInformationalVersionConfigKey]; + if (string.IsNullOrEmpty(identityOverride) && string.IsNullOrEmpty(versionOverride)) + { + return; + } + + if (Interlocked.Exchange(ref _stagingDiagnosticOverrideLogged, 1) == 0) + { + _logger.LogWarning( + "Staging feed-routing diagnostic overrides are active: {IdentityKey}={IdentityValue}, {VersionKey}={VersionValue}. " + + "These are intended only for local validation of staging feed routing and must not be set on a normal CLI.", + OverrideCliIdentityChannelConfigKey, + string.IsNullOrEmpty(identityOverride) ? "(unset)" : identityOverride, + OverrideCliInformationalVersionConfigKey, + string.IsNullOrEmpty(versionOverride) ? "(unset)" : versionOverride); + } + } + private PackageChannel? CreateStagingChannel(PackageChannelQuality defaultQuality) { // Refuse to synthesize a staging channel on CLI identities that cannot produce a real @@ -237,16 +436,29 @@ prDirectory.Parent is not { } dogfoodDirectory || var stagingQuality = GetStagingQuality(defaultQuality); var hasExplicitFeedOverride = !string.IsNullOrEmpty(_configuration[OverrideStagingFeedConfigKey]); - // When quality is Prerelease or Both and no explicit feed override is set, - // use the shared daily feed instead of the SHA-specific feed. SHA-specific - // darc-pub-* feeds are only created for stable-quality builds, so a non-Stable - // quality without an explicit feed override can only work with the shared feed. - var useSharedFeed = !hasExplicitFeedOverride && - stagingQuality is not PackageChannelQuality.Stable; + // Feed PROVENANCE is decided by the CLI build identity; version FILTERING is decided by + // quality. These are independent concerns and must not be conflated (see + // https://github.com/microsoft/aspire/issues/16652 for the original misroute, and the + // staging-identity prerelease regression that motivated separating them). + var effectiveIdentityChannel = GetEffectiveIdentityChannel(); + var useSharedFeed = ShouldUseSharedStagingFeed(hasExplicitFeedOverride, stagingQuality, effectiveIdentityChannel); var stagingFeedUrl = GetStagingFeedUrl(useSharedFeed); if (stagingFeedUrl is null) { + // Reaching here means synthesis was allowed (IsStagingChannelSynthesisAllowed passed) but the + // feed URL could not be produced. The only way that happens without an explicit override is the + // darc path failing to derive a commit hash from the CLI's AssemblyInformationalVersion (null, + // or no '+' build metadata). For a staging-identity CLI this should not occur on an + // officially published build, so surface it as a warning rather than silently dropping the + // channel — otherwise the caller just sees a missing 'staging' channel with no diagnostic + // (GetStagingChannelUnavailableReason() returns null because synthesis was permitted). + if (Interlocked.Exchange(ref _stagingFeedDerivationFailedLogged, 1) == 0) + { + _logger.LogWarning( + "Could not synthesize 'staging' package channel: failed to derive a staging feed URL for CLI identity '{Identity}' (no commit hash in the CLI version and no overrideStagingFeed set).", + effectiveIdentityChannel); + } return null; } @@ -277,6 +489,42 @@ prDirectory.Parent is not { } dogfoodDirectory || /// public string? GetStagingChannelUnavailableReason() => _stagingUnavailableReasonCache.Value; + // Decides whether the synthesized staging channel routes Aspire.* at the SHARED dnceng/dotnet9 + // daily feed (true) or at the SHA-specific darc-pub-microsoft-aspire- feed (false). + // + // The rule is identity-driven, NOT version-shape-driven: + // * Explicit overrideStagingFeed -> false. The caller named an exact feed; GetStagingFeedUrl + // returns it verbatim, so the shared-vs-darc distinction is moot. + // * staging IDENTITY -> false (always its own darc feed, any version shape). A CLI + // whose baked AspireCliChannel is `staging` is an officially published release-branch build, + // and darc publishes a per-commit darc-pub-microsoft-aspire- feed for EVERY such + // build — prerelease-shaped 13.4.0-preview.* and stable-shaped 13.4.0 alike. That feed is + // derived from the CLI's own commit, so it always carries the CLI's matching packages. + // Falling back to the shared dotnet9 daily feed (which only carries main-branch daily + // packages) silently resolves the wrong packages for polyglot apphosts while C# apphosts — + // whose nuget.config has the darc feed baked in — resolve correctly. That asymmetry is the + // bug this method fixes. (A missing darc feed for an officially published staging build is a + // publish/infra failure that should surface as an unresolved package, not be masked by a + // silent downgrade to daily packages.) + // * any other identity opting into staging (stable identity via config pin / StagingChannelEnabled + // feature) -> keep the historical quality-based routing: non-Stable quality uses the shared + // feed, Stable quality uses the SHA feed. Those identities do not own a release-branch darc + // feed of their own, so this preserves prior behavior unchanged. + private static bool ShouldUseSharedStagingFeed(bool hasExplicitFeedOverride, PackageChannelQuality stagingQuality, string identityChannel) + { + if (hasExplicitFeedOverride) + { + return false; + } + + if (string.Equals(identityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName)) + { + return false; + } + + return stagingQuality is not PackageChannelQuality.Stable; + } + private string? ComputeStagingChannelUnavailableReason() { if (IsStagingChannelSynthesisAllowed()) @@ -287,7 +535,7 @@ prDirectory.Parent is not { } dogfoodDirectory || return string.Format( CultureInfo.CurrentCulture, PackagingStrings.StagingChannelUnavailableOnDailyCli, - _executionContext.IdentityChannel); + GetEffectiveIdentityChannel()); } private bool IsStagingChannelSynthesisAllowed() @@ -314,8 +562,8 @@ private bool IsStagingChannelSynthesisAllowed() // For daily, local, and pr- identities, falling back to either the SHA feed (no real // darc feed exists) or the shared daily feed silently resolves daily packages — the // exact bug tracked by https://github.com/microsoft/aspire/issues/16652. - return string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Stable, StringComparisons.ChannelName) - || string.Equals(_executionContext.IdentityChannel, PackageChannelNames.Staging, StringComparisons.ChannelName); + return string.Equals(GetEffectiveIdentityChannel(), PackageChannelNames.Stable, StringComparisons.ChannelName) + || string.Equals(GetEffectiveIdentityChannel(), PackageChannelNames.Staging, StringComparisons.ChannelName); } private string? GetStagingFeedUrl(bool useSharedFeed) @@ -332,19 +580,18 @@ private bool IsStagingChannelSynthesisAllowed() // Invalid URL, fall through to default behavior } - // Use the shared daily feed when builds aren't marked stable + // Use the shared daily feed when the routing policy selected it (see ShouldUseSharedStagingFeed). if (useSharedFeed) { return "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"; } - // Extract commit hash from assembly version to build staging feed URL - // Staging feed URL template: https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-{commitHash}/nuget/v3/index.json - var assembly = Assembly.GetExecutingAssembly(); - var informationalVersion = assembly - .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) - .OfType() - .FirstOrDefault()?.InformationalVersion; + // Derive the SHA-specific staging feed from the CLI's own commit hash, carried in the + // AssemblyInformationalVersion build metadata after '+'. Example informational version: + // 13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12 + // yields the feed: + // https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json + var informationalVersion = _cliInformationalVersionProvider(); if (informationalVersion is null) { diff --git a/src/Aspire.Cli/Packaging/TemporaryNuGetConfig.cs b/src/Aspire.Cli/Packaging/TemporaryNuGetConfig.cs index ff73f8f9d02..66ccd658cb8 100644 --- a/src/Aspire.Cli/Packaging/TemporaryNuGetConfig.cs +++ b/src/Aspire.Cli/Packaging/TemporaryNuGetConfig.cs @@ -18,7 +18,7 @@ private TemporaryNuGetConfig(FileInfo configFile) public FileInfo ConfigFile => _configFile; - public static async Task CreateAsync(PackageMapping[] mappings, bool configureGlobalPackagesFolder = false) + public static async Task CreateAsync(PackageMapping[] mappings, bool configureGlobalPackagesFolder = false, string? globalPackagesFolderValue = null) { var tempDirectory = Directory.CreateTempSubdirectory("aspire-nuget-config").FullName; try @@ -28,7 +28,7 @@ public static async Task CreateAsync(PackageMapping[] mapp await GenerateNuGetConfigAsync(mappings, configFile); if (configureGlobalPackagesFolder) { - await AddGlobalPackagesFolderToConfigAsync(configFile); + await AddGlobalPackagesFolderToConfigAsync(configFile, globalPackagesFolderValue); } return new TemporaryNuGetConfig(configFile); } @@ -125,7 +125,7 @@ private static async Task GenerateNuGetConfigAsync(PackageMapping[] mappings, Fi await xmlWriter.WriteEndDocumentAsync(); } - private static async Task AddGlobalPackagesFolderToConfigAsync(FileInfo configFile) + private static async Task AddGlobalPackagesFolderToConfigAsync(FileInfo configFile, string? globalPackagesFolderValue) { XDocument document; await using (var stream = configFile.OpenRead()) @@ -139,7 +139,7 @@ private static async Task AddGlobalPackagesFolderToConfigAsync(FileInfo configFi document.Add(configuration); } - NuGetConfigMerger.AddGlobalPackagesFolderConfiguration(configuration); + NuGetConfigMerger.AddGlobalPackagesFolderConfiguration(configuration, globalPackagesFolderValue); var content = document.Declaration is null ? document.ToString() diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs index d87fc185867..41599bcc26e 100644 --- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs +++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs @@ -689,7 +689,8 @@ private void ThrowIfStagingUnavailable(string? requestedChannel) return await TemporaryNuGetConfig.CreateAsync( PackageSourceOverrideMappings.Create(packageSourceOverride, matchedChannel), - configureGlobalPackagesFolder); + configureGlobalPackagesFolder, + configureGlobalPackagesFolder ? ResolveStableGlobalPackagesFolder(packageSourceOverride) : null); } if (string.IsNullOrEmpty(requestedChannel)) @@ -745,7 +746,69 @@ private void ThrowIfStagingUnavailable(string? requestedChannel) // restore honors the channel's package source mappings. Let IO/XML failures // surface instead of silently falling back to the caller's unmapped sources, // which could otherwise restore from an unintended feed. - return await TemporaryNuGetConfig.CreateAsync(channel.Mappings, channel.ConfigureGlobalPackagesFolder); + return await TemporaryNuGetConfig.CreateAsync( + channel.Mappings, + channel.ConfigureGlobalPackagesFolder, + channel.ConfigureGlobalPackagesFolder ? ResolveStableGlobalPackagesFolder(GetPrimaryFeedUrl(channel.Mappings)) : null); + } + + /// + /// Returns the absolute globalPackagesFolder path to write into a temporary NuGet.config + /// when the resolved channel asks for per-build cache isolation (today: staging). + /// + /// + /// The default is a relative + /// .nugetpackages path that NuGet resolves next to the nuget.config it came from. For + /// the workspace-merge flow that's fine — the merged config is + /// persistent. For 's + /// the config file lives in a Directory.CreateTempSubdirectory("aspire-nuget-config") folder + /// that recursively deletes after restore. NuGet + /// would have just populated <temp>/.nugetpackages/<id>/<version>/ + /// with the staging assemblies, would have baked those + /// paths into integration-package-probe-manifest.json, and aspire-managed would then + /// try to load assemblies the dispose just removed — observed as a hang during DI / assembly + /// loading on macOS osx-arm64 polyglot staging builds. Anchoring the override at a stable + /// per-build location keeps the cached packages alive for as long as any manifest references + /// them. + /// + /// The cache lives under (i.e. the + /// ASPIRE_HOME override when set, otherwise ~/.aspire) rather than under + /// so that two AppHosts running on the same machine against + /// the same staging build can share a single restore — the unit of cache isolation here is + /// the staging build, not the individual restore command. + /// + /// The cache subdirectory is keyed by a truncated hash of the resolved feed URL (first 8 + /// hex chars of over the trimmed/lower-cased URL). + /// Two staging builds of the same release branch — which share the same stable-shaped semver + /// (e.g. 13.4.0) but ship from different darc feeds — therefore each get their own + /// cache. A user pointing the same CLI at multiple overrideStagingFeed values during + /// dev/test also gets a distinct cache per feed, instead of one bucket silently shared across + /// feeds. NuGet identifies packages by (id, version) only, so without that per-feed + /// key the second feed's restore would silently reuse the first feed's now-stale + /// 13.4.0 assemblies. When is null or empty (defensive — + /// both call sites currently always pass a real URL) the key falls back to "default" + /// so the path is still well-formed. + /// + private string ResolveStableGlobalPackagesFolder(string? feedUrl) + { + var cacheKey = CliPathHelper.ComputeStagingFeedCacheKey(feedUrl) ?? "default"; + return Path.Combine( + CliPathHelper.GetStagingNuGetPackagesDirectory(_executionContext.AspireHomeDirectory), + cacheKey); + } + + /// + /// Returns the URL we use as the cache-key input when materializing a temp nuget.config from + /// a . Prefers the explicit Aspire* mapping (the staging + /// channel's primary feed and the one whose restored assemblies actually need cache + /// isolation), falling back to the first mapping for forward compatibility with channel + /// shapes we don't yet emit. + /// + private static string GetPrimaryFeedUrl(PackageMapping[] mappings) + { + var aspire = mappings.FirstOrDefault(m => + string.Equals(m.PackageFilter, "Aspire*", StringComparison.OrdinalIgnoreCase)); + return aspire?.Source ?? mappings[0].Source; } private async Task ResolveLocalPackageSourceOverrideAsync(string? requestedChannel, CancellationToken cancellationToken) diff --git a/src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs index a7500a3e539..af0d2d5b50a 100644 --- a/src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs @@ -309,6 +309,14 @@ public static string SpecifiedVersionRequiresExactPackageMatch } } + public static string NonInteractiveRequiresExactPackageMatch + { + get + { + return ResourceManager.GetString("NonInteractiveRequiresExactPackageMatch", resourceCulture); + } + } + public static string UsePrereleasePackages { get diff --git a/src/Aspire.Cli/Resources/AddCommandStrings.resx b/src/Aspire.Cli/Resources/AddCommandStrings.resx index 68668c59988..57d957d69b6 100644 --- a/src/Aspire.Cli/Resources/AddCommandStrings.resx +++ b/src/Aspire.Cli/Resources/AddCommandStrings.resx @@ -223,6 +223,10 @@ When --version is specified in non-interactive mode, the integration name must exactly match a package or friendly name. No exact match was found for '{0}'. {0} is the integration name provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + Use pre-release packages diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.cs.xlf index 46f20819789..e0d9d7c231f 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.cs.xlf @@ -102,6 +102,11 @@ Vašemu hledaného termínu {0} neodpovídají žádné balíčky. Zobrazují se všechny dostupné balíčky. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Balíček {0}::{1} byl úspěšně přidán. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.de.xlf index 1b986286cb6..740263347ac 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.de.xlf @@ -102,6 +102,11 @@ Es stimmten keine Pakete mit Ihrem Suchbegriff „{0}“ überein. Alle verfügbaren Pakete werden angezeigt. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Das Paket {0}::{1} wurde erfolgreich hinzugefügt. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.es.xlf index 0343b44bd5a..a20674b790d 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.es.xlf @@ -102,6 +102,11 @@ No se encontraron paquetes que coincidan con el término de búsqueda "{0}". Mostrando todos los paquetes disponibles. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. El paquete {0}::{1} se agregó correctamente. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.fr.xlf index 2f58c55a87d..7ee12de8e91 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.fr.xlf @@ -102,6 +102,11 @@ Aucun package ne correspond à votre terme de recherche « {0} ». Affichage de tous les packages disponibles. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Le package {0}::{1} a été ajouté avec succès. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.it.xlf index 5370c007db5..bdadac4b1f2 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.it.xlf @@ -102,6 +102,11 @@ Nessun pacchetto corrisponde al termine di ricerca '{0}'. Visualizzazione di tutti i pacchetti disponibili. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Caricamento del pacchetto {0}::{1} completato. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ja.xlf index 09ba10d2665..88bac2a1384 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ja.xlf @@ -102,6 +102,11 @@ 検索語句 '{0}' に一致するパッケージはありません。使用可能なすべてのパッケージを表示しています。 {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. パッケージ {0}::{1} が正常に追加されました。 diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ko.xlf index b451547d234..ca225a1a1c5 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ko.xlf @@ -102,6 +102,11 @@ 검색어 '{0}'와(과) 일치하는 패키지가 없습니다. 사용 가능한 모든 패키지를 표시합니다. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. 패키지 {0}::{1}을(를) 추가했습니다. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pl.xlf index e7ff599247f..fad9c56ae1c 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pl.xlf @@ -102,6 +102,11 @@ Żadne pakiety nie pasują do wyszukiwanego terminu „{0}”. Wyświetlanie wszystkich dostępnych pakietów. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Pakiet {0}::{1} został pomyślnie dodany. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pt-BR.xlf index df4ac017669..52c4a17ef42 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.pt-BR.xlf @@ -102,6 +102,11 @@ Nenhum pacote correspondeu ao termo de pesquisa '{0}'. Mostrando todos os pacotes disponíveis. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. O pacote {0}::{1} foi adicionado com êxito. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ru.xlf index 6a27acb343f..0f3ad17c9ec 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.ru.xlf @@ -102,6 +102,11 @@ Не найдено пакетов, соответствующих вашему запросу "{0}". Показаны все доступные пакеты. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. Пакет {0}::{1} загружен успешно. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.tr.xlf index 5fcd28173ae..674a07014ec 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.tr.xlf @@ -102,6 +102,11 @@ '{0}' arama teriminizle eşleşen herhangi bir paket bulunamadı. Kullanılabilir tüm paketler gösteriliyor. {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. {0}::{1} paketi başarıyla eklendi. diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hans.xlf index 9876170360d..644700e5762 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hans.xlf @@ -102,6 +102,11 @@ 没有与搜索词“{0}”匹配的包。正在显示所有可用包。 {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. 已成功添加包 {0}::{1}。 diff --git a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hant.xlf index 097c4c679ad..0ffbc3cd4db 100644 --- a/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/AddCommandStrings.zh-Hant.xlf @@ -102,6 +102,11 @@ 沒有任何套件符合您搜尋的字詞 '{0}'。顯示所有可用的套件。 {0} is the search term provided by the user. + + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + No exact match was found for '{0}'. In non-interactive mode the integration name must exactly match a package id or friendly name; fuzzy fallback is disabled to prevent silently selecting the wrong package. + {0} is the integration name provided by the user. + The package {0}::{1} was added successfully. 已成功新增套件 {0}::{1}。 diff --git a/src/Aspire.Cli/Utils/CliPathHelper.cs b/src/Aspire.Cli/Utils/CliPathHelper.cs index fdb2114f17a..431868a3661 100644 --- a/src/Aspire.Cli/Utils/CliPathHelper.cs +++ b/src/Aspire.Cli/Utils/CliPathHelper.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO.Hashing; +using System.Text; using Aspire.Cli.Acquisition; using Aspire.Hosting.Backchannel; using Microsoft.Extensions.Logging; @@ -11,6 +13,26 @@ internal static class CliPathHelper { internal const string AspireHomeEnvironmentVariable = "ASPIRE_HOME"; + /// + /// Name of the directory under ASPIRE_HOME that holds NuGet package caches keyed by + /// a stable hash of the resolved staging feed URL. Two staging builds of the same release + /// branch share the same stable-shaped semver (e.g. 13.4.0) but ship from different + /// darc feeds; an overrideStagingFeed setting can also point the same CLI at any + /// arbitrary feed. Each distinct feed URL therefore gets its own feed-hash subdirectory + /// here to avoid (id, version) cache collisions in NuGet. aspire cache clear + /// wipes the feed-hash subdirectories so users can recover wedged staging restores without + /// manual filesystem surgery. + /// + internal const string StagingNuGetPackagesFolderName = ".nugetpackages"; + + /// + /// Default number of hex characters used for staging feed cache keys. 8 keeps deep + /// integration cache paths well under Windows MAX_PATH while still giving roughly + /// 4 billion buckets — orders of magnitude more than the handful of staging feeds any one + /// user ever sees, so collisions are negligible in practice. + /// + internal const int DefaultStagingFeedCacheKeyLength = 8; + // The maximum age before a leftover CLI socket file in the runtime sockets directory is // pruned. 24 hours is comfortably longer than any legitimate Aspire CLI run and short enough // that stale entries don't pile up indefinitely after crashes (see issue #16709). @@ -38,6 +60,53 @@ internal static string GetDefaultAspireHomeDirectory(string? configuredAspireHom : configuredAspireHome; } + /// + /// Returns the absolute path to the staging NuGet package cache root + /// (<ASPIRE_HOME>/.nugetpackages). Producers (the + /// PrebuiltAppHostServer temp nuget.config) write feed-hash-keyed + /// subdirectories under this root; the aspire cache clear command wipes those + /// subdirectories. Centralized here so both call sites agree on the location. + /// + internal static string GetStagingNuGetPackagesDirectory(DirectoryInfo aspireHomeDirectory) + { + ArgumentNullException.ThrowIfNull(aspireHomeDirectory); + return Path.Combine(aspireHomeDirectory.FullName, StagingNuGetPackagesFolderName); + } + + /// + /// Returns a stable lowercase hex cache key derived from , + /// truncated to characters. Returns when + /// the URL is null, empty, or whitespace-only. + /// + /// + /// Used by PrebuiltAppHostServer to compute the per-feed + /// globalPackagesFolder subdirectory under + /// <ASPIRE_HOME>/.nugetpackages. Keying on the feed URL (rather than the CLI + /// commit SHA) means that the same CLI talking to two different override staging feeds + /// gets two distinct caches, which is important because staging packages from different + /// feeds share the same stable-shaped (id, version) tuple and would otherwise + /// collide in NuGet's cache. + /// + /// The URL is trimmed and lower-cased before hashing so harmless variations (trailing + /// whitespace from a config file, hostname casing) don't fragment the cache. Hashing the + /// URL with (non-cryptographic but very high quality) keeps any + /// embedded credentials out of the on-disk directory name even when the feed URL itself + /// contains them. + /// + internal static string? ComputeStagingFeedCacheKey(string? feedUrl, int length = DefaultStagingFeedCacheKeyLength) + { + if (string.IsNullOrWhiteSpace(feedUrl) || length <= 0) + { + return null; + } + + var normalized = feedUrl.Trim().ToLowerInvariant(); + var bytes = Encoding.UTF8.GetBytes(normalized); + // XxHash3 emits 8 bytes (64 bits) -> 16 hex chars; truncate to the requested length. + var hex = Convert.ToHexString(XxHash3.Hash(bytes)).ToLowerInvariant(); + return length >= hex.Length ? hex : hex[..length]; + } + internal static string? TryGetAspireHomeDirectoryFromInstallRoute(string? processPath, ILogger? logger = null) { if (string.IsNullOrEmpty(processPath)) diff --git a/src/Aspire.Cli/Utils/VersionHelper.cs b/src/Aspire.Cli/Utils/VersionHelper.cs index dfb85cb73eb..02d3f78e6ea 100644 --- a/src/Aspire.Cli/Utils/VersionHelper.cs +++ b/src/Aspire.Cli/Utils/VersionHelper.cs @@ -24,7 +24,7 @@ public static bool IsLocalBuildChannel(string? channelName) } /// - /// Finds the candidate that exactly matches the current CLI/SDK version when running against local build channels or hives. + /// Finds the candidate that exactly matches the current CLI/SDK version when a channel has already been selected or local hives are present. /// public static bool TryGetCurrentCliVersionMatch( IEnumerable candidates, @@ -36,7 +36,7 @@ public static bool TryGetCurrentCliVersionMatch( ArgumentNullException.ThrowIfNull(candidates); ArgumentNullException.ThrowIfNull(versionSelector); - if (!hasPrHives && !IsLocalBuildChannel(channelName)) + if (!hasPrHives && string.IsNullOrWhiteSpace(channelName)) { match = default; return false; diff --git a/src/Aspire.Hosting.Azure.AppConfiguration/api/Aspire.Hosting.Azure.AppConfiguration.cs b/src/Aspire.Hosting.Azure.AppConfiguration/api/Aspire.Hosting.Azure.AppConfiguration.cs index f8b14093b9b..3a1dc1508b7 100644 --- a/src/Aspire.Hosting.Azure.AppConfiguration/api/Aspire.Hosting.Azure.AppConfiguration.cs +++ b/src/Aspire.Hosting.Azure.AppConfiguration/api/Aspire.Hosting.Azure.AppConfiguration.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class AzureAppConfigurationExtensions { - [AspireExport(Description = "Adds an Azure App Configuration resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureAppConfiguration(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures Azure App Configuration to run with the local emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureEmulator = null) { throw null; } - [AspireExport(Description = "Adds a data bind mount for the App Configuration emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string? path = null) { throw null; } - [AspireExport(Description = "Adds a data volume for the App Configuration emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - [AspireExport(Description = "Sets the host port for the App Configuration emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } [AspireExportIgnore(Reason = "AppConfigurationBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureAppConfigurationRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj index 52e78c556c6..92848c8485c 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj +++ b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Aspire.Hosting.Azure.AppContainers/BaseContainerAppContext.cs b/src/Aspire.Hosting.Azure.AppContainers/BaseContainerAppContext.cs index c0d14ac555f..e252d0b7705 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/BaseContainerAppContext.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/BaseContainerAppContext.cs @@ -222,6 +222,15 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null) if (value is EndpointReference ep) { + // The referenced endpoint may belong to a resource deployed to a different compute + // environment (for example a Foundry hosted agent). In that case delegate to the owning + // compute environment instead of looking it up in this environment's local endpoint map. + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + ep, [_containerAppEnvironmentContext.Environment], out var crossExpr)) + { + return ProcessValue(crossExpr, secretType, parent); + } + var context = ep.Resource == resource ? this : _containerAppEnvironmentContext.GetContainerAppContext(ep.Resource); @@ -274,6 +283,12 @@ BicepValue GetHostValue(string? prefix = null, string? suffix = null) if (value is EndpointReferenceExpression epExpr) { + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + epExpr, [_containerAppEnvironmentContext.Environment], out var crossExpr)) + { + return ProcessValue(crossExpr, secretType, parent); + } + var context = epExpr.Endpoint.Resource == resource ? this : _containerAppEnvironmentContext.GetContainerAppContext(epExpr.Endpoint.Resource); diff --git a/src/Aspire.Hosting.Azure.AppContainers/api/Aspire.Hosting.Azure.AppContainers.cs b/src/Aspire.Hosting.Azure.AppContainers/api/Aspire.Hosting.Azure.AppContainers.cs index f2eae891342..3ade723fd4c 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/api/Aspire.Hosting.Azure.AppContainers.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/api/Aspire.Hosting.Azure.AppContainers.cs @@ -10,68 +10,68 @@ namespace Aspire.Hosting { public static partial class AzureContainerAppContainerExtensions { - [AspireExport("publishContainerAsAzureContainerApp", MethodName = "publishAsAzureContainerApp", Description = "Configures the container resource to be published as an Azure Container App")] + [AspireExport("publishContainerAsAzureContainerApp", MethodName = "publishAsAzureContainerApp")] public static ApplicationModel.IResourceBuilder PublishAsAzureContainerApp(this ApplicationModel.IResourceBuilder container, System.Action configure) where T : ApplicationModel.ContainerResource { throw null; } } public static partial class AzureContainerAppExecutableExtensions { - [AspireExport("publishExecutableAsAzureContainerApp", MethodName = "publishAsAzureContainerApp", Description = "Configures the executable resource to be published as an Azure Container App")] + [AspireExport("publishExecutableAsAzureContainerApp", MethodName = "publishAsAzureContainerApp")] public static ApplicationModel.IResourceBuilder PublishAsAzureContainerApp(this ApplicationModel.IResourceBuilder executable, System.Action configure) where T : ApplicationModel.ExecutableResource { throw null; } } public static partial class AzureContainerAppExtensions { - [AspireExport(Description = "Adds an Azure Container App Environment resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureContainerAppEnvironment(this IDistributedApplicationBuilder builder, string name) { throw null; } [System.Obsolete("Use AddAzureContainerAppEnvironment instead. This method will be removed in a future version.")] public static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructure(this IDistributedApplicationBuilder builder) { throw null; } - [AspireExport(Description = "Configures resources to use azd naming conventions")] + [AspireExport] + public static ApplicationModel.IResourceBuilder WithAcrPullIdentity(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder identityBuilder) { throw null; } + + [AspireExport] public static ApplicationModel.IResourceBuilder WithAzdResourceNaming(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Configures the container app environment to use a specific Log Analytics Workspace")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithAzureLogAnalyticsWorkspace(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder workspaceBuilder) { throw null; } - [AspireExport(Description = "Configures resources to use compact naming for length-constrained Azure resources")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREACANAMING001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithCompactResourceNaming(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Configures whether the Aspire dashboard is included in the container app environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, bool enable = true) { throw null; } - [AspireExport(Description = "Configures whether HTTP endpoints are upgraded to HTTPS")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHttpsUpgrade(this ApplicationModel.IResourceBuilder builder, bool upgrade = true) { throw null; } } public static partial class AzureContainerAppProjectExtensions { - [AspireExport("publishProjectAsAzureContainerApp", MethodName = "publishAsAzureContainerApp", Description = "Configures the project resource to be published as an Azure Container App")] + [AspireExport("publishProjectAsAzureContainerApp", MethodName = "publishAsAzureContainerApp")] public static ApplicationModel.IResourceBuilder PublishAsAzureContainerApp(this ApplicationModel.IResourceBuilder project, System.Action configure) where T : ApplicationModel.ProjectResource { throw null; } } public static partial class ContainerAppExtensions { - [AspireExport(Description = "Configures the custom domain for the container app")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREACADOMAINS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static void ConfigureCustomDomain(this global::Azure.Provisioning.AppContainers.ContainerApp app, ApplicationModel.IResourceBuilder customDomain, ApplicationModel.IResourceBuilder certificateName) { } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal publishAsAzureContainerAppJob dispatcher export.")] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder PublishAsAzureContainerAppJob(this ApplicationModel.IResourceBuilder resource, System.Action configure) where T : ApplicationModel.IComputeResource { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal publishAsAzureContainerAppJob dispatcher export.")] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder PublishAsAzureContainerAppJob(this ApplicationModel.IResourceBuilder resource) where T : ApplicationModel.IComputeResource { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal publishAsScheduledAzureContainerAppJob dispatcher export.")] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder PublishAsScheduledAzureContainerAppJob(this ApplicationModel.IResourceBuilder resource, string cronExpression, System.Action? configure = null) where T : ApplicationModel.IComputeResource { throw null; } } @@ -86,7 +86,6 @@ public AzureContainerAppCustomizationAnnotation(System.Action Configure { get { throw null; } } } - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public sealed partial class AzureContainerAppJobCustomizationAnnotation : ApplicationModel.IResourceAnnotation { public AzureContainerAppJobCustomizationAnnotation(System.Action configure) { } diff --git a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj index 84d433e5124..1570e890831 100644 --- a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj +++ b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs index ca89a2faac7..b5d3c774560 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebsiteContext.cs @@ -167,6 +167,15 @@ private void ProcessEndpoints() if (value is EndpointReference ep) { + // The referenced endpoint may belong to a resource deployed to a different compute + // environment (for example a Foundry hosted agent). In that case delegate to the owning + // compute environment instead of looking it up in this environment's local endpoint map. + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + ep, [environmentContext.Environment], out var crossExpr)) + { + return ProcessValue(crossExpr, secretType, parent, isSlot); + } + var context = environmentContext.GetAppServiceContext(ep.Resource); return isSlot ? (GetEndpointValue(context._slotEndpointMapping[ep.EndpointName], EndpointProperty.Url), secretType) : @@ -206,6 +215,12 @@ private void ProcessEndpoints() if (value is EndpointReferenceExpression epExpr) { + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + epExpr, [environmentContext.Environment], out var crossExpr)) + { + return ProcessValue(crossExpr, secretType, parent, isSlot); + } + var context = environmentContext.GetAppServiceContext(epExpr.Endpoint.Resource); var mapping = isSlot ? context._slotEndpointMapping[epExpr.Endpoint.EndpointName] : context._endpointMapping[epExpr.Endpoint.EndpointName]; var val = GetEndpointValue(mapping, epExpr.Property); diff --git a/src/Aspire.Hosting.Azure.AppService/api/Aspire.Hosting.Azure.AppService.cs b/src/Aspire.Hosting.Azure.AppService/api/Aspire.Hosting.Azure.AppService.cs index 4887236cfc0..58df7af3523 100644 --- a/src/Aspire.Hosting.Azure.AppService/api/Aspire.Hosting.Azure.AppService.cs +++ b/src/Aspire.Hosting.Azure.AppService/api/Aspire.Hosting.Azure.AppService.cs @@ -10,20 +10,23 @@ namespace Aspire.Hosting { public static partial class AzureAppServiceComputeResourceExtensions { - [AspireExport(Description = "Publishes the compute resource as an Azure App Service website or deployment slot")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsAzureAppServiceWebsite(this ApplicationModel.IResourceBuilder builder, System.Action? configure = null, System.Action? configureSlot = null) where T : ApplicationModel.IComputeResource { throw null; } - [AspireExport(Description = "Skips Azure App Service environment variable name validation for the compute resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder SkipEnvironmentVariableNameChecks(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IComputeResource { throw null; } } public static partial class AzureAppServiceEnvironmentExtensions { - [AspireExport(Description = "Adds an Azure App Service environment resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureAppServiceEnvironment(this IDistributedApplicationBuilder builder, string name) { throw null; } + [AspireExport] + public static ApplicationModel.IResourceBuilder WithAcrPullIdentity(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder identityBuilder) { throw null; } + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withAzureApplicationInsights dispatcher export.")] public static ApplicationModel.IResourceBuilder WithAzureApplicationInsights(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder applicationInsightsLocation) { throw null; } @@ -36,7 +39,7 @@ public static partial class AzureAppServiceEnvironmentExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withAzureApplicationInsights dispatcher export.")] public static ApplicationModel.IResourceBuilder WithAzureApplicationInsights(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Configures whether the Aspire dashboard is included in the Azure App Service environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, bool enable = true) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withDeploymentSlot dispatcher export.")] @@ -45,7 +48,7 @@ public static partial class AzureAppServiceEnvironmentExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withDeploymentSlot dispatcher export.")] public static ApplicationModel.IResourceBuilder WithDeploymentSlot(this ApplicationModel.IResourceBuilder builder, string deploymentSlot) { throw null; } - [AspireExport(Description = "Configures whether HTTP endpoints are automatically upgraded to HTTPS in Azure App Service")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHttpsUpgrade(this ApplicationModel.IResourceBuilder builder, bool upgrade = true) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.ApplicationInsights/api/Aspire.Hosting.Azure.ApplicationInsights.cs b/src/Aspire.Hosting.Azure.ApplicationInsights/api/Aspire.Hosting.Azure.ApplicationInsights.cs index bb7c10c034a..f25c5548e7c 100644 --- a/src/Aspire.Hosting.Azure.ApplicationInsights/api/Aspire.Hosting.Azure.ApplicationInsights.cs +++ b/src/Aspire.Hosting.Azure.ApplicationInsights/api/Aspire.Hosting.Azure.ApplicationInsights.cs @@ -13,10 +13,10 @@ public static partial class AzureApplicationInsightsExtensions [AspireExportIgnore(Reason = "logAnalyticsWorkspace parameter cannot be made optional in ATS. Use the single-parameter overload with WithLogAnalyticsWorkspace instead.")] public static ApplicationModel.IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? logAnalyticsWorkspace) { throw null; } - [AspireExport(Description = "Adds an Azure Application Insights resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureApplicationInsights(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the Application Insights resource to use a Log Analytics Workspace")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithLogAnalyticsWorkspace(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder logAnalyticsWorkspace) { throw null; } [AspireExportIgnore(Reason = "BicepOutputReference is not ATS-compatible. Use the IResourceBuilder overload instead.")] diff --git a/src/Aspire.Hosting.Azure.CognitiveServices/api/Aspire.Hosting.Azure.CognitiveServices.cs b/src/Aspire.Hosting.Azure.CognitiveServices/api/Aspire.Hosting.Azure.CognitiveServices.cs index 76d3034a168..fb30e7c7d01 100644 --- a/src/Aspire.Hosting.Azure.CognitiveServices/api/Aspire.Hosting.Azure.CognitiveServices.cs +++ b/src/Aspire.Hosting.Azure.CognitiveServices/api/Aspire.Hosting.Azure.CognitiveServices.cs @@ -10,17 +10,17 @@ namespace Aspire.Hosting { public static partial class AzureOpenAIExtensions { - [AspireExport(Description = "Adds an Azure OpenAI resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureOpenAI(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Obsolete API that accepts AzureOpenAIDeployment which is not ATS-compatible.")] [System.Obsolete("AddDeployment taking an AzureOpenAIDeployment is deprecated. Please the AddDeployment overload that returns an AzureOpenAIDeploymentResource instead.")] public static ApplicationModel.IResourceBuilder AddDeployment(this ApplicationModel.IResourceBuilder builder, ApplicationModel.AzureOpenAIDeployment deployment) { throw null; } - [AspireExport(Description = "Adds an Azure OpenAI deployment resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDeployment(this ApplicationModel.IResourceBuilder builder, string name, string modelName, string modelVersion) { throw null; } - [AspireExport(Description = "Configures properties of an Azure OpenAI deployment", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } [AspireExportIgnore(Reason = "CognitiveServicesBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the enum-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/api/Aspire.Hosting.Azure.ContainerRegistry.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/api/Aspire.Hosting.Azure.ContainerRegistry.cs index 07f6901c4d4..8275f1daf27 100644 --- a/src/Aspire.Hosting.Azure.ContainerRegistry/api/Aspire.Hosting.Azure.ContainerRegistry.cs +++ b/src/Aspire.Hosting.Azure.ContainerRegistry/api/Aspire.Hosting.Azure.ContainerRegistry.cs @@ -10,18 +10,18 @@ namespace Aspire.Hosting { public static partial class AzureContainerRegistryExtensions { - [AspireExport(Description = "Adds an Azure Container Registry resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureContainerRegistry(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Gets the Azure Container Registry associated with a compute environment resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder GetAzureContainerRegistry(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResource, Azure.IAzureComputeEnvironmentResource { throw null; } - [AspireExport("withContainerRegistryAzureContainerRegistry", MethodName = "withAzureContainerRegistry", Description = "Configures a compute environment resource to use an Azure Container Registry.")] + [AspireExport("withContainerRegistryAzureContainerRegistry", MethodName = "withAzureContainerRegistry")] public static ApplicationModel.IResourceBuilder WithAzureContainerRegistry(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder registryBuilder) where T : ApplicationModel.IResource, ApplicationModel.IComputeEnvironmentResource { throw null; } - [AspireExport(Description = "Configures a purge task for the Azure Container Registry resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPurgeTask(this ApplicationModel.IResourceBuilder builder, string schedule, string? filter = null, System.TimeSpan? ago = null, int keep = 3, string? taskName = null) { throw null; } [AspireExportIgnore(Reason = "ContainerRegistryBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureContainerRegistryRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.CosmosDB/api/Aspire.Hosting.Azure.CosmosDB.cs b/src/Aspire.Hosting.Azure.CosmosDB/api/Aspire.Hosting.Azure.CosmosDB.cs index ea286c206be..fd3ab1614de 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/api/Aspire.Hosting.Azure.CosmosDB.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/api/Aspire.Hosting.Azure.CosmosDB.cs @@ -48,7 +48,7 @@ void Azure.IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(Sy public static partial class AzureCosmosExtensions { - [AspireExport(Description = "Adds an Azure Cosmos DB resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureCosmosDB(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addContainer dispatcher export.")] @@ -57,17 +57,17 @@ public static partial class AzureCosmosExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addContainer dispatcher export.")] public static ApplicationModel.IResourceBuilder AddContainer(this ApplicationModel.IResourceBuilder builder, string name, string partitionKeyPath, string? containerName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Cosmos DB database resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddCosmosDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } [AspireExportIgnore(Reason = "Obsolete API with incorrect return type. Use AddCosmosDatabase instead.")] [System.Obsolete("This method is obsolete because it has the wrong return type and will be removed in a future version. Use AddCosmosDatabase instead to add a Cosmos DB database.")] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string databaseName) { throw null; } - [AspireExport(Description = "Configures the Azure Cosmos DB resource to run using the local emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } - [AspireExport(Description = "Configures the Azure Cosmos DB resource to run using the preview emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECOSMOSDB001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder RunAsPreviewEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } @@ -77,20 +77,20 @@ public static partial class AzureCosmosExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withAccessKeyAuthentication dispatcher export.")] public static ApplicationModel.IResourceBuilder WithAccessKeyAuthentication(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Exposes the Data Explorer endpoint for the preview emulator")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECOSMOSDB001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithDataExplorer(this ApplicationModel.IResourceBuilder builder, int? port = null) { throw null; } - [AspireExport(Description = "Adds a named volume for the data folder to an Azure Cosmos DB emulator resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - [AspireExport(Description = "Configures Azure Cosmos DB to use the default Azure SKU")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDefaultAzureSku(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Sets the host port for the Cosmos DB emulator gateway endpoint")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithGatewayPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Sets the partition count for the Azure Cosmos DB emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPartitionCount(this ApplicationModel.IResourceBuilder builder, int count) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.EventHubs/api/Aspire.Hosting.Azure.EventHubs.cs b/src/Aspire.Hosting.Azure.EventHubs/api/Aspire.Hosting.Azure.EventHubs.cs index 7cdf5fd6d4a..56327bdeea7 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/api/Aspire.Hosting.Azure.EventHubs.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/api/Aspire.Hosting.Azure.EventHubs.cs @@ -10,25 +10,25 @@ namespace Aspire.Hosting { public static partial class AzureEventHubsExtensions { - [AspireExport(Description = "Adds an Azure Event Hubs namespace resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureEventHubs(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Event Hub consumer group resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddConsumerGroup(this ApplicationModel.IResourceBuilder builder, string name, string? groupName = null) { throw null; } [System.Obsolete("This method is obsolete because it has the wrong return type and will be removed in a future version. Use AddHub instead to add an Azure Event Hub.")] public static ApplicationModel.IResourceBuilder AddEventHub(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Event Hub resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddHub(this ApplicationModel.IResourceBuilder builder, string name, string? hubName = null) { throw null; } - [AspireExport(Description = "Configures the Azure Event Hubs resource to run with the local emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } [AspireExportIgnore(Reason = "Action callbacks are not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithConfiguration(this ApplicationModel.IResourceBuilder builder, System.Action configJson) { throw null; } - [AspireExport(Description = "Sets the emulator configuration file path")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithConfigurationFile(this ApplicationModel.IResourceBuilder builder, string path) { throw null; } [System.Obsolete("This method is obsolete because it doesn't work as intended and will be removed in a future version.")] @@ -40,10 +40,10 @@ public static partial class AzureEventHubsExtensions [System.Obsolete("Use WithHostPort instead.")] public static ApplicationModel.IResourceBuilder WithGatewayPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Sets the host port for the Event Hubs emulator endpoint")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Configures properties of an Azure Event Hub", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } [AspireExportIgnore(Reason = "EventHubsBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureEventHubsRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.FrontDoor/Aspire.Hosting.Azure.FrontDoor.csproj b/src/Aspire.Hosting.Azure.FrontDoor/Aspire.Hosting.Azure.FrontDoor.csproj index 0715429ec9b..f5e42917159 100644 --- a/src/Aspire.Hosting.Azure.FrontDoor/Aspire.Hosting.Azure.FrontDoor.csproj +++ b/src/Aspire.Hosting.Azure.FrontDoor/Aspire.Hosting.Azure.FrontDoor.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/Aspire.Hosting.Azure.FrontDoor/AzureFrontDoorExtensions.cs b/src/Aspire.Hosting.Azure.FrontDoor/AzureFrontDoorExtensions.cs index 167c95f44b3..73e557b2926 100644 --- a/src/Aspire.Hosting.Azure.FrontDoor/AzureFrontDoorExtensions.cs +++ b/src/Aspire.Hosting.Azure.FrontDoor/AzureFrontDoorExtensions.cs @@ -201,16 +201,11 @@ public static IResourceBuilder WithOrigin( private static IComputeEnvironmentResource GetEffectiveComputeEnvironment(IResource resource) { - if (resource.GetComputeEnvironment() is { } computeEnvironment) + if (ComputeEnvironmentEndpointResolver.TryGetEffectiveComputeEnvironment(resource, out var computeEnvironment)) { return computeEnvironment; } - if (resource.GetDeploymentTargetAnnotation()?.ComputeEnvironment is { } deploymentComputeEnvironment) - { - return deploymentComputeEnvironment; - } - throw new InvalidOperationException( $"Resource '{resource.Name}' does not have a compute environment. " + "Ensure a compute environment (e.g., Azure Container Apps, Azure App Service) is configured in the application model."); diff --git a/src/Aspire.Hosting.Azure.FrontDoor/api/Aspire.Hosting.Azure.FrontDoor.cs b/src/Aspire.Hosting.Azure.FrontDoor/api/Aspire.Hosting.Azure.FrontDoor.cs index 1a15edd0751..874da433745 100644 --- a/src/Aspire.Hosting.Azure.FrontDoor/api/Aspire.Hosting.Azure.FrontDoor.cs +++ b/src/Aspire.Hosting.Azure.FrontDoor/api/Aspire.Hosting.Azure.FrontDoor.cs @@ -10,10 +10,10 @@ namespace Aspire.Hosting { public static partial class AzureFrontDoorExtensions { - [AspireExport(Description = "Adds an Azure Front Door resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureFrontDoor(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an origin (backend) to the Azure Front Door resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithOrigin(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder resource) where T : ApplicationModel.IComputeResource, ApplicationModel.IResourceWithEndpoints { throw null; } } diff --git a/src/Aspire.Hosting.Azure.Functions/api/Aspire.Hosting.Azure.Functions.cs b/src/Aspire.Hosting.Azure.Functions/api/Aspire.Hosting.Azure.Functions.cs index 5c69bc50633..c46fb91dbd1 100644 --- a/src/Aspire.Hosting.Azure.Functions/api/Aspire.Hosting.Azure.Functions.cs +++ b/src/Aspire.Hosting.Azure.Functions/api/Aspire.Hosting.Azure.Functions.cs @@ -10,14 +10,14 @@ namespace Aspire.Hosting { public static partial class AzureFunctionsProjectResourceExtensions { - [AspireExport(Description = "Adds an Azure Functions project to the distributed application")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureFunctionsProject(this IDistributedApplicationBuilder builder, string name, string projectPath) { throw null; } [AspireExportIgnore(Reason = "TProject : IProjectMetadata is a .NET-specific generic constraint not compatible with ATS. Use the project path overload instead.")] public static ApplicationModel.IResourceBuilder AddAzureFunctionsProject(this IDistributedApplicationBuilder builder, string name) where TProject : IProjectMetadata, new() { throw null; } - [AspireExport(Description = "Configures the Azure Functions project to use specified Azure Storage as host storage")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostStorage(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder storage) { throw null; } [AspireExportIgnore(Reason = "IResourceWithAzureFunctionsConfig is an internal interface constraint not compatible with ATS.")] @@ -27,15 +27,15 @@ public static partial class AzureFunctionsProjectResourceExtensions public static partial class DurableTaskResourceExtensions { - [AspireExport(Description = "Adds a Durable Task scheduler resource to the distributed application.")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDURABLETASK001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder AddDurableTaskScheduler(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a Durable Task hub resource associated with the scheduler.")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDURABLETASK001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder AddTaskHub(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the Durable Task scheduler to run using the local emulator.", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDURABLETASK001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } diff --git a/src/Aspire.Hosting.Azure.KeyVault/api/Aspire.Hosting.Azure.KeyVault.cs b/src/Aspire.Hosting.Azure.KeyVault/api/Aspire.Hosting.Azure.KeyVault.cs index 8b2928d3438..b77693808ef 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/api/Aspire.Hosting.Azure.KeyVault.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/api/Aspire.Hosting.Azure.KeyVault.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting { public static partial class AzureKeyVaultResourceExtensions { - [AspireExport(Description = "Adds an Azure Key Vault resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureKeyVault(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addSecret dispatcher export.")] @@ -31,7 +31,7 @@ public static partial class AzureKeyVaultResourceExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addSecret dispatcher export.")] public static ApplicationModel.IResourceBuilder AddSecret(this ApplicationModel.IResourceBuilder builder, string name, string secretName, ApplicationModel.ReferenceExpression value) { throw null; } - [AspireExport(Description = "Gets a secret reference from the Azure Key Vault")] + [AspireExport] public static Azure.IAzureKeyVaultSecretReference GetSecret(this ApplicationModel.IResourceBuilder builder, string secretName) { throw null; } [AspireExportIgnore(Reason = "KeyVaultBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureKeyVaultRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.Kubernetes/api/Aspire.Hosting.Azure.Kubernetes.cs b/src/Aspire.Hosting.Azure.Kubernetes/api/Aspire.Hosting.Azure.Kubernetes.cs index 09d396f850a..8b91b2ba433 100644 --- a/src/Aspire.Hosting.Azure.Kubernetes/api/Aspire.Hosting.Azure.Kubernetes.cs +++ b/src/Aspire.Hosting.Azure.Kubernetes/api/Aspire.Hosting.Azure.Kubernetes.cs @@ -8,37 +8,55 @@ //------------------------------------------------------------------------------ namespace Aspire.Hosting { + public static partial class AzureCertManagerExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddCertManager(this ApplicationModel.IResourceBuilder builder, string name, string? chartVersion = null) { throw null; } + } + public static partial class AzureKubernetesEnvironmentExtensions { - [AspireExport(Description = "Adds an Azure Kubernetes Service environment resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureKubernetesEnvironment(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a node pool to the AKS cluster")] + [AspireExport] + public static ApplicationModel.IResourceBuilder AddLoadBalancer(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder subnet) { throw null; } + + [AspireExport] public static ApplicationModel.IResourceBuilder AddNodePool(this ApplicationModel.IResourceBuilder builder, string name, string vmSize = "Standard_D2s_v5", int minCount = 1, int maxCount = 3) { throw null; } - [AspireExport(Description = "Configures the AKS environment to use a specific container registry")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerRegistry(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder registry) { throw null; } - [AspireExport("withNodePoolSubnet", MethodName = "withSubnet", Description = "Configures an AKS node pool to use a specific VNet subnet")] + [AspireExport("withNodePoolSubnet", MethodName = "withSubnet")] public static ApplicationModel.IResourceBuilder WithSubnet(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder subnet) { throw null; } - [AspireExport(Description = "Configures the AKS cluster to use a VNet subnet")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithSubnet(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder subnet) { throw null; } - [AspireExport(Description = "Replaces the default system node pool with a customized configuration")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithSystemNodePool(this ApplicationModel.IResourceBuilder builder, string vmSize = "Standard_D2s_v5", int minCount = 1, int maxCount = 3) { throw null; } - [AspireExport(Description = "Enables workload identity on the AKS cluster")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithWorkloadIdentity(this ApplicationModel.IResourceBuilder builder, bool enabled = true) { throw null; } } public static partial class AzureKubernetesIngressExtensions { - [AspireExport(Description = "Adds a Kubernetes Gateway API Gateway to an AKS environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddGateway(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a Kubernetes Ingress resource to an AKS environment")] + [AspireExport] + public static ApplicationModel.IResourceBuilder AddHelmChart(this ApplicationModel.IResourceBuilder builder, string name, string chartReference, string chartVersion) { throw null; } + + [AspireExport] public static ApplicationModel.IResourceBuilder AddIngress(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithLoadBalancer(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder loadBalancer) { throw null; } + + [AspireExport("withLoadBalancerOnIngress", MethodName = "withLoadBalancer")] + public static ApplicationModel.IResourceBuilder WithLoadBalancer(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder loadBalancer) { throw null; } } } @@ -3500,4 +3518,11 @@ public AzureKubernetesEnvironmentResource(string name, System.Action, ApplicationModel.IResourceWithParent, ApplicationModel.IResource + { + internal AzureKubernetesLoadBalancerResource() : base(default!) { } + + public AzureKubernetesEnvironmentResource Parent { get { throw null; } } + } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure.Kusto/api/Aspire.Hosting.Azure.Kusto.cs b/src/Aspire.Hosting.Azure.Kusto/api/Aspire.Hosting.Azure.Kusto.cs index 78f9b62d21d..2198fc50a93 100644 --- a/src/Aspire.Hosting.Azure.Kusto/api/Aspire.Hosting.Azure.Kusto.cs +++ b/src/Aspire.Hosting.Azure.Kusto/api/Aspire.Hosting.Azure.Kusto.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class AzureKustoBuilderExtensions { - [AspireExport(Description = "Adds an Azure Data Explorer (Kusto) cluster resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureKustoCluster(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a Kusto read-write database resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddReadWriteDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Configures the Kusto cluster to run using the local emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } - [AspireExport(Description = "Defines the KQL script used to create the database")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCreationScript(this ApplicationModel.IResourceBuilder builder, string script) { throw null; } - [AspireExport(Description = "Sets the host port for the Kusto emulator endpoint")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int port) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.Network/CompatibilitySuppressions.xml b/src/Aspire.Hosting.Azure.Network/CompatibilitySuppressions.xml index 22fec018e7d..0497d618a92 100644 --- a/src/Aspire.Hosting.Azure.Network/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting.Azure.Network/CompatibilitySuppressions.xml @@ -1,39 +1,3 @@  - - - CP0002 - M:Aspire.Hosting.Azure.AzureNatGatewayResource.get_NameOutput - lib/net8.0/Aspire.Hosting.Azure.Network.dll - lib/net8.0/Aspire.Hosting.Azure.Network.dll - true - - - CP0002 - M:Aspire.Hosting.Azure.AzureNetworkSecurityGroupResource.get_NameOutput - lib/net8.0/Aspire.Hosting.Azure.Network.dll - lib/net8.0/Aspire.Hosting.Azure.Network.dll - true - - - CP0002 - M:Aspire.Hosting.Azure.AzurePrivateEndpointResource.get_NameOutput - lib/net8.0/Aspire.Hosting.Azure.Network.dll - lib/net8.0/Aspire.Hosting.Azure.Network.dll - true - - - CP0002 - M:Aspire.Hosting.Azure.AzurePublicIPAddressResource.get_NameOutput - lib/net8.0/Aspire.Hosting.Azure.Network.dll - lib/net8.0/Aspire.Hosting.Azure.Network.dll - true - - - CP0002 - M:Aspire.Hosting.Azure.AzureVirtualNetworkResource.get_NameOutput - lib/net8.0/Aspire.Hosting.Azure.Network.dll - lib/net8.0/Aspire.Hosting.Azure.Network.dll - true - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs b/src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs index 7ef1291d6b9..93a75a686d5 100644 --- a/src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs +++ b/src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs @@ -10,81 +10,81 @@ namespace Aspire.Hosting { public static partial class AzureNatGatewayExtensions { - [AspireExport(Description = "Adds an Azure NAT Gateway resource to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNatGateway(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Associates an Azure Public IP Address resource with an Azure NAT Gateway resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPublicIPAddress(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder publicIPAddress) { throw null; } } public static partial class AzureNetworkSecurityGroupExtensions { - [AspireExport(Description = "Adds an Azure Network Security Group resource to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNetworkSecurityGroup(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a security rule to an Azure Network Security Group resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithSecurityRule(this ApplicationModel.IResourceBuilder builder, Azure.AzureSecurityRule rule) { throw null; } } public static partial class AzureNetworkSecurityPerimeterExtensions { - [AspireExport(Description = "Adds an Azure Network Security Perimeter resource to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNetworkSecurityPerimeter(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an access rule to an Azure Network Security Perimeter resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithAccessRule(this ApplicationModel.IResourceBuilder builder, Azure.AzureNspAccessRule rule) { throw null; } - [AspireExport("associateWithNetworkSecurityPerimeter", MethodName = "withNetworkSecurityPerimeter", Description = "Associates an Azure PaaS resource with a Network Security Perimeter.")] + [AspireExport("associateWithNetworkSecurityPerimeter", MethodName = "withNetworkSecurityPerimeter")] public static ApplicationModel.IResourceBuilder WithNetworkSecurityPerimeter(this ApplicationModel.IResourceBuilder target, ApplicationModel.IResourceBuilder nsp, global::Azure.Provisioning.Network.NetworkSecurityPerimeterAssociationAccessMode accessMode = global::Azure.Provisioning.Network.NetworkSecurityPerimeterAssociationAccessMode.Enforced, string? associationName = null) where T : ApplicationModel.IResource, Azure.IAzureNspAssociationTarget { throw null; } } public static partial class AzurePrivateEndpointExtensions { - [AspireExport(Description = "Adds an Azure Private Endpoint resource to an Azure subnet resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPrivateEndpoint(this ApplicationModel.IResourceBuilder subnet, ApplicationModel.IResourceBuilder target) { throw null; } } public static partial class AzurePublicIPAddressExtensions { - [AspireExport(Description = "Adds an Azure Public IP Address resource to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPublicIPAddress(this IDistributedApplicationBuilder builder, string name) { throw null; } } public static partial class AzureVirtualNetworkExtensions { - [AspireExport("addAzureVirtualNetworkFromParameter", MethodName = "addAzureVirtualNetwork", Description = "Adds an Azure Virtual Network resource to the application model with a parameterized address prefix.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addAzureVirtualNetwork dispatcher export.")] public static ApplicationModel.IResourceBuilder AddAzureVirtualNetwork(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder addressPrefix) { throw null; } - [AspireExport(Description = "Adds an Azure Virtual Network resource to the application model.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addAzureVirtualNetwork dispatcher export.")] public static ApplicationModel.IResourceBuilder AddAzureVirtualNetwork(this IDistributedApplicationBuilder builder, string name, string? addressPrefix = null) { throw null; } - [AspireExport("addSubnetFromParameter", MethodName = "addSubnet", Description = "Adds an Azure subnet resource with a parameterized address prefix to an Azure Virtual Network resource.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addSubnet dispatcher export.")] public static ApplicationModel.IResourceBuilder AddSubnet(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder addressPrefix, string? subnetName = null) { throw null; } - [AspireExport(Description = "Adds an Azure subnet resource to an Azure Virtual Network resource.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addSubnet dispatcher export.")] public static ApplicationModel.IResourceBuilder AddSubnet(this ApplicationModel.IResourceBuilder builder, string name, string addressPrefix, string? subnetName = null) { throw null; } - [AspireExport(Description = "Adds an inbound allow rule to the Azure subnet resource's Network Security Group.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AllowInbound(this ApplicationModel.IResourceBuilder builder, string? port = null, string? from = null, string? to = null, global::Azure.Provisioning.Network.SecurityRuleProtocol? protocol = null, int? priority = null, string? name = null) { throw null; } - [AspireExport(Description = "Adds an outbound allow rule to the Azure subnet resource's Network Security Group.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AllowOutbound(this ApplicationModel.IResourceBuilder builder, string? port = null, string? from = null, string? to = null, global::Azure.Provisioning.Network.SecurityRuleProtocol? protocol = null, int? priority = null, string? name = null) { throw null; } - [AspireExport(Description = "Adds an inbound deny rule to the Azure subnet resource's Network Security Group.")] + [AspireExport] public static ApplicationModel.IResourceBuilder DenyInbound(this ApplicationModel.IResourceBuilder builder, string? port = null, string? from = null, string? to = null, global::Azure.Provisioning.Network.SecurityRuleProtocol? protocol = null, int? priority = null, string? name = null) { throw null; } - [AspireExport(Description = "Adds an outbound deny rule to the Azure subnet resource's Network Security Group.")] + [AspireExport] public static ApplicationModel.IResourceBuilder DenyOutbound(this ApplicationModel.IResourceBuilder builder, string? port = null, string? from = null, string? to = null, global::Azure.Provisioning.Network.SecurityRuleProtocol? protocol = null, int? priority = null, string? name = null) { throw null; } - [AspireExport("withSubnetDelegatedSubnet", MethodName = "withDelegatedSubnet", Description = "Associates a delegated Azure subnet resource with an Azure resource that supports subnet delegation.")] + [AspireExport("withSubnetDelegatedSubnet", MethodName = "withDelegatedSubnet")] public static ApplicationModel.IResourceBuilder WithDelegatedSubnet(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder subnet) where T : Azure.IAzureDelegatedSubnetResource { throw null; } - [AspireExport(Description = "Associates an Azure NAT Gateway resource with an Azure subnet resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithNatGateway(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder natGateway) { throw null; } - [AspireExport(Description = "Associates an Azure Network Security Group resource with an Azure subnet resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithNetworkSecurityGroup(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder nsg) { throw null; } } } @@ -127,21 +127,21 @@ public AzureNetworkSecurityPerimeterResource(string name, System.Action AddressPrefixes { get { throw null; } } + public System.Collections.Generic.List AddressPrefixes { get { throw null; } init { } } - public System.Collections.Generic.List AddressPrefixReferences { get { throw null; } } + public System.Collections.Generic.List AddressPrefixReferences { get { throw null; } init { } } public required global::Azure.Provisioning.Network.NetworkSecurityPerimeterAccessRuleDirection Direction { get { throw null; } set { } } - public System.Collections.Generic.List FullyQualifiedDomainNameReferences { get { throw null; } } + public System.Collections.Generic.List FullyQualifiedDomainNameReferences { get { throw null; } init { } } - public System.Collections.Generic.List FullyQualifiedDomainNames { get { throw null; } } + public System.Collections.Generic.List FullyQualifiedDomainNames { get { throw null; } init { } } public required string Name { get { throw null; } set { } } - public System.Collections.Generic.List SubscriptionReferences { get { throw null; } } + public System.Collections.Generic.List SubscriptionReferences { get { throw null; } init { } } - public System.Collections.Generic.List Subscriptions { get { throw null; } } + public System.Collections.Generic.List Subscriptions { get { throw null; } init { } } } public partial class AzurePrivateEndpointResource : AzureProvisioningResource diff --git a/src/Aspire.Hosting.Azure.OperationalInsights/api/Aspire.Hosting.Azure.OperationalInsights.cs b/src/Aspire.Hosting.Azure.OperationalInsights/api/Aspire.Hosting.Azure.OperationalInsights.cs index 148525d0cd5..b2df43c02ab 100644 --- a/src/Aspire.Hosting.Azure.OperationalInsights/api/Aspire.Hosting.Azure.OperationalInsights.cs +++ b/src/Aspire.Hosting.Azure.OperationalInsights/api/Aspire.Hosting.Azure.OperationalInsights.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting { public static partial class AzureLogAnalyticsWorkspaceExtensions { - [AspireExport(Description = "Adds an Azure Log Analytics Workspace resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureLogAnalyticsWorkspace(this IDistributedApplicationBuilder builder, string name) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/api/Aspire.Hosting.Azure.PostgreSQL.cs b/src/Aspire.Hosting.Azure.PostgreSQL/api/Aspire.Hosting.Azure.PostgreSQL.cs index 6e8a1830a60..afdd7ecf8a3 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/api/Aspire.Hosting.Azure.PostgreSQL.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/api/Aspire.Hosting.Azure.PostgreSQL.cs @@ -10,10 +10,10 @@ namespace Aspire.Hosting { public static partial class AzurePostgresExtensions { - [AspireExport(Description = "Adds an Azure PostgreSQL Flexible Server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzurePostgresFlexibleServer(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure PostgreSQL database")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use AddAzurePostgresFlexibleServer instead to add an Azure PostgreSQL Flexible Server resource.")] @@ -22,7 +22,7 @@ public static partial class AzurePostgresExtensions [System.Obsolete("This method is obsolete and will be removed in a future version. Use AddAzurePostgresFlexibleServer instead to add an Azure PostgreSQL Flexible Server resource.")] public static ApplicationModel.IResourceBuilder PublishAsAzurePostgresFlexibleServer(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Configures the Azure PostgreSQL Flexible Server resource to run locally in a container", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsContainer(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withPasswordAuthentication dispatcher export.")] @@ -31,7 +31,7 @@ public static partial class AzurePostgresExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withPasswordAuthentication dispatcher export.")] public static ApplicationModel.IResourceBuilder WithPasswordAuthentication(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder keyVaultBuilder, ApplicationModel.IResourceBuilder? userName = null, ApplicationModel.IResourceBuilder? password = null) { throw null; } - [AspireExport(Description = "Adds a Postgres MCP server container", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPOSTGRES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithPostgresMcp(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } } diff --git a/src/Aspire.Hosting.Azure.Redis/api/Aspire.Hosting.Azure.Redis.cs b/src/Aspire.Hosting.Azure.Redis/api/Aspire.Hosting.Azure.Redis.cs index 1ad9efd53c4..af8ae400835 100644 --- a/src/Aspire.Hosting.Azure.Redis/api/Aspire.Hosting.Azure.Redis.cs +++ b/src/Aspire.Hosting.Azure.Redis/api/Aspire.Hosting.Azure.Redis.cs @@ -10,10 +10,10 @@ namespace Aspire.Hosting { public static partial class AzureManagedRedisExtensions { - [AspireExport(Description = "Adds an Azure Managed Redis resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureManagedRedis(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures Azure Managed Redis to run in a local container", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsContainer(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withAccessKeyAuthentication dispatcher export.")] diff --git a/src/Aspire.Hosting.Azure.Search/api/Aspire.Hosting.Azure.Search.cs b/src/Aspire.Hosting.Azure.Search/api/Aspire.Hosting.Azure.Search.cs index 92f283f9b60..d63887cc614 100644 --- a/src/Aspire.Hosting.Azure.Search/api/Aspire.Hosting.Azure.Search.cs +++ b/src/Aspire.Hosting.Azure.Search/api/Aspire.Hosting.Azure.Search.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting { public static partial class AzureSearchExtensions { - [AspireExport(Description = "Adds an Azure AI Search service resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureSearch(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "SearchBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureSearchRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.ServiceBus/api/Aspire.Hosting.Azure.ServiceBus.cs b/src/Aspire.Hosting.Azure.ServiceBus/api/Aspire.Hosting.Azure.ServiceBus.cs index fcc0b60819e..20b89239037 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/api/Aspire.Hosting.Azure.ServiceBus.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/api/Aspire.Hosting.Azure.ServiceBus.cs @@ -10,20 +10,20 @@ namespace Aspire.Hosting { public static partial class AzureServiceBusExtensions { - [AspireExport(Description = "Adds an Azure Service Bus namespace resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureServiceBus(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Obsolete API with incorrect return type. Use AddServiceBusQueue instead.")] [System.Obsolete("This method is obsolete because it has the wrong return type and will be removed in a future version. Use AddServiceBusQueue instead to add an Azure Service Bus Queue.")] public static ApplicationModel.IResourceBuilder AddQueue(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Service Bus queue resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddServiceBusQueue(this ApplicationModel.IResourceBuilder builder, string name, string? queueName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Service Bus subscription resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddServiceBusSubscription(this ApplicationModel.IResourceBuilder builder, string name, string? subscriptionName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Service Bus topic resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddServiceBusTopic(this ApplicationModel.IResourceBuilder builder, string name, string? topicName = null) { throw null; } [AspireExportIgnore(Reason = "Obsolete API. Use AddServiceBusSubscription instead.")] @@ -38,25 +38,25 @@ public static partial class AzureServiceBusExtensions [System.Obsolete("This method is obsolete because it has the wrong return type and will be removed in a future version. Use AddServiceBusTopic instead to add an Azure Service Bus Topic.")] public static ApplicationModel.IResourceBuilder AddTopic(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the Azure Service Bus resource to run with the local emulator", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } [AspireExportIgnore(Reason = "Action callbacks are not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithConfiguration(this ApplicationModel.IResourceBuilder builder, System.Action configJson) { throw null; } - [AspireExport(Description = "Sets the emulator configuration file path")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithConfigurationFile(this ApplicationModel.IResourceBuilder builder, string path) { throw null; } - [AspireExport(Description = "Sets the host port for the Service Bus emulator endpoint")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withQueueProperties", MethodName = "withProperties", Description = "Configures properties of an Azure Service Bus queue", RunSyncOnBackgroundThread = true)] + [AspireExport("withQueueProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } - [AspireExport("withSubscriptionProperties", MethodName = "withProperties", Description = "Configures properties of an Azure Service Bus subscription", RunSyncOnBackgroundThread = true)] + [AspireExport("withSubscriptionProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } - [AspireExport("withTopicProperties", MethodName = "withProperties", Description = "Configures properties of an Azure Service Bus topic", RunSyncOnBackgroundThread = true)] + [AspireExport("withTopicProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } [AspireExportIgnore(Reason = "ServiceBusBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureServiceBusRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.SignalR/api/Aspire.Hosting.Azure.SignalR.cs b/src/Aspire.Hosting.Azure.SignalR/api/Aspire.Hosting.Azure.SignalR.cs index c5d8171dc55..308d9899030 100644 --- a/src/Aspire.Hosting.Azure.SignalR/api/Aspire.Hosting.Azure.SignalR.cs +++ b/src/Aspire.Hosting.Azure.SignalR/api/Aspire.Hosting.Azure.SignalR.cs @@ -16,7 +16,7 @@ public static partial class AzureSignalRExtensions [AspireExportIgnore(Reason = "Use the dedicated polyglot overload instead.")] public static ApplicationModel.IResourceBuilder AddAzureSignalR(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures an Azure SignalR resource to be emulated. This resource requires an Azure SignalR resource to be added to the application model. Please note that the resource will be emulated in Serverless mode.", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } [AspireExportIgnore(Reason = "SignalRBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureSignalRRole-based overload instead.")] diff --git a/src/Aspire.Hosting.Azure.Sql/api/Aspire.Hosting.Azure.Sql.cs b/src/Aspire.Hosting.Azure.Sql/api/Aspire.Hosting.Azure.Sql.cs index d8dd9b40955..e267fe2b194 100644 --- a/src/Aspire.Hosting.Azure.Sql/api/Aspire.Hosting.Azure.Sql.cs +++ b/src/Aspire.Hosting.Azure.Sql/api/Aspire.Hosting.Azure.Sql.cs @@ -10,10 +10,10 @@ namespace Aspire.Hosting { public static partial class AzureSqlExtensions { - [AspireExport(Description = "Adds an Azure SQL Database server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureSqlServer(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure SQL database resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use AddAzureSqlServer instead to add an Azure SQL server resource.")] @@ -22,18 +22,18 @@ public static partial class AzureSqlExtensions [System.Obsolete("This method is obsolete and will be removed in a future version. Use AddAzureSqlServer instead to add an Azure SQL server resource.")] public static ApplicationModel.IResourceBuilder PublishAsAzureSqlDatabase(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Configures the Azure SQL server to run locally in a SQL Server container", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsContainer(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } - [AspireExport(Description = "Configures the Azure SQL server to use a specific storage account for deployment scripts")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] public static ApplicationModel.IResourceBuilder WithAdminDeploymentScriptStorage(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder storage) { throw null; } - [AspireExport(Description = "Configures the Azure SQL server to use a specific subnet for deployment scripts")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] public static ApplicationModel.IResourceBuilder WithAdminDeploymentScriptSubnet(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder subnet) { throw null; } - [AspireExport(Description = "Configures the Azure SQL database to use the default Azure SKU")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDefaultAzureSku(this ApplicationModel.IResourceBuilder builder) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.Storage/api/Aspire.Hosting.Azure.Storage.cs b/src/Aspire.Hosting.Azure.Storage/api/Aspire.Hosting.Azure.Storage.cs index da2d56b6f10..00238d1d6de 100644 --- a/src/Aspire.Hosting.Azure.Storage/api/Aspire.Hosting.Azure.Storage.cs +++ b/src/Aspire.Hosting.Azure.Storage/api/Aspire.Hosting.Azure.Storage.cs @@ -10,56 +10,56 @@ namespace Aspire.Hosting { public static partial class AzureStorageExtensions { - [AspireExport(Description = "Adds an Azure Storage resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureStorage(this IDistributedApplicationBuilder builder, string name) { throw null; } [System.Obsolete("Use AddBlobContainer on IResourceBuilder instead.")] public static ApplicationModel.IResourceBuilder AddBlobContainer(this ApplicationModel.IResourceBuilder builder, string name, string? blobContainerName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Blob Storage container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBlobContainer(this ApplicationModel.IResourceBuilder builder, string name, string? blobContainerName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Blob Storage resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBlobs(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Data Lake Storage resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDataLake(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Data Lake Storage file system resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDataLakeFileSystem(this ApplicationModel.IResourceBuilder builder, string name, string? dataLakeFileSystemName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Storage queue resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddQueue(this ApplicationModel.IResourceBuilder builder, string name, string? queueName = null) { throw null; } - [AspireExport(Description = "Adds an Azure Queue Storage resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddQueues(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds an Azure Table Storage resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddTables(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the Azure Storage resource to be emulated using Azurite", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder RunAsEmulator(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null) { throw null; } - [AspireExport(Description = "Configures whether the emulator checks API version validity")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithApiVersionCheck(this ApplicationModel.IResourceBuilder builder, bool enable = true) { throw null; } - [AspireExport(Description = "Sets the host port for blob requests on the storage emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBlobPort(this ApplicationModel.IResourceBuilder builder, int port) { throw null; } - [AspireExport(Description = "Adds a bind mount for the data folder to an Azure Storage emulator resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string? path = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a named volume for the data folder to an Azure Storage emulator resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Sets the host port for queue requests on the storage emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithQueuePort(this ApplicationModel.IResourceBuilder builder, int port) { throw null; } [AspireExportIgnore(Reason = "StorageBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureStorageRole-based overload instead.")] public static ApplicationModel.IResourceBuilder WithRoleAssignments(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder target, params global::Azure.Provisioning.Storage.StorageBuiltInRole[] roles) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Sets the host port for table requests on the storage emulator")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithTablePort(this ApplicationModel.IResourceBuilder builder, int port) { throw null; } } } diff --git a/src/Aspire.Hosting.Azure.WebPubSub/api/Aspire.Hosting.Azure.WebPubSub.cs b/src/Aspire.Hosting.Azure.WebPubSub/api/Aspire.Hosting.Azure.WebPubSub.cs index 1c9b87a5e55..e83aaf9228b 100644 --- a/src/Aspire.Hosting.Azure.WebPubSub/api/Aspire.Hosting.Azure.WebPubSub.cs +++ b/src/Aspire.Hosting.Azure.WebPubSub/api/Aspire.Hosting.Azure.WebPubSub.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting { public static partial class AzureWebPubSubExtensions { - [AspireExport(Description = "Adds an Azure Web PubSub resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureWebPubSub(this IDistributedApplicationBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "UpstreamAuthSettings is not ATS-compatible. Use the polyglot overload without auth settings instead.")] @@ -19,7 +19,7 @@ public static partial class AzureWebPubSubExtensions [AspireExportIgnore(Reason = "ExpressionInterpolatedStringHandler and UpstreamAuthSettings are not ATS-compatible. Use the polyglot overload without auth settings instead.")] public static ApplicationModel.IResourceBuilder AddEventHandler(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ReferenceExpression.ExpressionInterpolatedStringHandler urlTemplateExpression, string userEventPattern = "*", string[]? systemEvents = null, global::Azure.Provisioning.WebPubSub.UpstreamAuthSettings? authSettings = null) { throw null; } - [AspireExport(Description = "Adds a hub to the Azure Web PubSub resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddHub(this ApplicationModel.IResourceBuilder builder, string name, string? hubName = null) { throw null; } [AspireExportIgnore(Reason = "Use the AddHub overload with the optional hubName parameter instead.")] diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs index ac76fc957dd..02448c02bb5 100644 --- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs +++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs @@ -133,7 +133,8 @@ private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel ap foreach (var resource in resourceSnapshot) { var prerequisiteResources = new HashSet(); - var azureReferences = await GetAzureReferences(resource, cancellationToken).ConfigureAwait(false); + var directDependencies = await resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.DirectOnly, cancellationToken).ConfigureAwait(false); + var azureReferences = new HashSet(directDependencies.OfType()); var azureReferencesWithRoleAssignments = (resource.TryGetAnnotationsOfType(out var annotations) @@ -184,6 +185,42 @@ private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel ap } } + // A direct dependency that is not itself an Azure resource can still "front" one + // (e.g. a Foundry hosted agent's node app fronts its owning Foundry account). Such a + // resource carries ReferenceRoleAssignmentAnnotation(s) declaring that any resource + // referencing it should be granted roles on a transitive Azure target the normal + // IAzureResource-only reference walk above cannot reach. Fold those implied targets + // into the same role-assignment path so the consumer gets an identity + role bicep + // exactly as it would for a direct Azure reference. + foreach (var dependency in directDependencies) + { + if (!dependency.TryGetAnnotationsOfType(out var impliedRoleAssignments)) + { + continue; + } + + foreach (var impliedRoleAssignment in impliedRoleAssignments) + { + var target = impliedRoleAssignment.Target; + if (target.IsContainer() || target.IsEmulator()) + { + continue; + } + + if (executionContext.IsRunMode) + { + AppendGlobalRoleAssignments(globalRoleAssignments, target, impliedRoleAssignment.Roles); + } + else + { + // In PublishMode, materialize as an explicit RoleAssignmentAnnotation so + // GetAllRoleAssignments (which groups by target and unions roles) picks it + // up alongside any roles the consumer already declares for the same target. + resource.Annotations.Add(new RoleAssignmentAnnotation(target, impliedRoleAssignment.Roles)); + } + } + } + // in PublishMode with SupportsTargetedRoleAssignments, we need to create the identity and role assignment resources // if the resource references any Azure resources, or has role assignments to Azure resources if (executionContext.IsPublishMode) @@ -250,7 +287,12 @@ private static Dictionary { foreach (var g in roleAssignments.GroupBy(r => r.Target)) { - result[g.Key] = g.SelectMany(r => r.Roles); + // Deduplicate roles per target. A target can accumulate multiple RoleAssignmentAnnotations + // (e.g. an implied ReferenceRoleAssignmentAnnotation from two hosted agents on the same + // Foundry account, plus a direct reference). Emitting the same RoleDefinition twice would + // produce two RoleAssignment bicep resources with the same identifier ("{prefix}_{roleName}") + // and fail bicep compilation. This mirrors the RunMode path, which unions into a HashSet. + result[g.Key] = g.SelectMany(r => r.Roles).Distinct(); } } return result; @@ -359,19 +401,6 @@ private sealed class AddRoleAssignmentsContext( public DistributedApplicationExecutionContext ExecutionContext => executionContext; } - private async Task> GetAzureReferences(IResource resource, CancellationToken cancellationToken) - { - var dependencies = await resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.DirectOnly, cancellationToken).ConfigureAwait(false); - - HashSet azureReferences = []; - foreach (var azureResource in dependencies.OfType()) - { - azureReferences.Add(azureResource); - } - - return azureReferences; - } - private static void AppendGlobalRoleAssignments(Dictionary> globalRoleAssignments, AzureProvisioningResource azureResource, IEnumerable newRoles) { if (!globalRoleAssignments.TryGetValue(azureResource, out var existingRoles)) diff --git a/src/Aspire.Hosting.Azure/AzureRoleAssignmentResource.cs b/src/Aspire.Hosting.Azure/AzureRoleAssignmentResource.cs index db0da95114b..fa14bc2273e 100644 --- a/src/Aspire.Hosting.Azure/AzureRoleAssignmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureRoleAssignmentResource.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Azure; @@ -19,6 +20,7 @@ namespace Aspire.Hosting.Azure; /// The Aspire resource that owns this set of role assignments, or for global role assignments granted to the deployment principal. /// The user-assigned managed identity whose principal receives the role assignments, or for global role assignments granted to the deployment principal. /// Callback to configure the Azure role assignment resources. +[Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public sealed class AzureRoleAssignmentResource( string name, AzureProvisioningResource targetAzureResource, diff --git a/src/Aspire.Hosting.Azure/CompatibilitySuppressions.xml b/src/Aspire.Hosting.Azure/CompatibilitySuppressions.xml index 78cdfa201ac..0497d618a92 100644 --- a/src/Aspire.Hosting.Azure/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting.Azure/CompatibilitySuppressions.xml @@ -1,18 +1,3 @@  - - - CP0002 - M:Aspire.Hosting.Azure.IAzurePrivateEndpointTarget.GetPrivateDnsZoneName - lib/net8.0/Aspire.Hosting.Azure.dll - lib/net8.0/Aspire.Hosting.Azure.dll - true - - - CP0006 - M:Aspire.Hosting.Azure.IAzurePrivateEndpointTarget.GetPrivateDnsZoneNames - lib/net8.0/Aspire.Hosting.Azure.dll - lib/net8.0/Aspire.Hosting.Azure.dll - true - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/ReferenceRoleAssignmentAnnotation.cs b/src/Aspire.Hosting.Azure/ReferenceRoleAssignmentAnnotation.cs new file mode 100644 index 00000000000..58720eaad6e --- /dev/null +++ b/src/Aspire.Hosting.Azure/ReferenceRoleAssignmentAnnotation.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure; + +/// +/// Declares that any compute resource referencing the annotated resource should be granted +/// on the Azure resource . +/// +/// The Azure resource that referencing resources should be granted roles on. +/// The roles that referencing resources should be assigned on . +/// +/// +/// This annotation is applied to a resource that "fronts" an Azure resource without being an +/// itself. For example, a Foundry hosted agent's node app is a plain +/// compute resource, but invoking the agent requires the caller to hold a role on the owning +/// Foundry account. The account is only a transitive dependency of a consumer, so +/// 's normal reference walk — which only acts on direct +/// dependencies — cannot reach it. +/// +/// +/// When a compute resource takes a direct dependency on a resource carrying this annotation, +/// folds (Target, Roles) into the same role-assignment +/// path used for direct Azure references, so the consumer gets a managed identity and the +/// corresponding role assignment on with no additional wiring. +/// +/// +[Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public sealed class ReferenceRoleAssignmentAnnotation(AzureProvisioningResource target, IReadOnlySet roles) : IResourceAnnotation +{ + /// + /// Gets the Azure resource that resources referencing the annotated resource should be granted roles on. + /// + public AzureProvisioningResource Target { get; } = target; + + /// + /// Gets the set of roles that resources referencing the annotated resource should be assigned on . + /// + public IReadOnlySet Roles { get; } = roles; +} diff --git a/src/Aspire.Hosting.Azure/RoleAssignmentResourceAnnotation.cs b/src/Aspire.Hosting.Azure/RoleAssignmentResourceAnnotation.cs index 932becea274..a1f521c235a 100644 --- a/src/Aspire.Hosting.Azure/RoleAssignmentResourceAnnotation.cs +++ b/src/Aspire.Hosting.Azure/RoleAssignmentResourceAnnotation.cs @@ -8,6 +8,7 @@ namespace Aspire.Hosting.Azure; /// /// An annotation that points to the resource that contains the role assignments for an Azure resource. /// +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. internal sealed class RoleAssignmentResourceAnnotation(AzureRoleAssignmentResource rolesResource) : IResourceAnnotation { /// @@ -15,3 +16,4 @@ internal sealed class RoleAssignmentResourceAnnotation(AzureRoleAssignmentResour /// public AzureRoleAssignmentResource RolesResource { get; } = rolesResource; } +#pragma warning restore ASPIREAZURE003 diff --git a/src/Aspire.Hosting.Azure/api/Aspire.Hosting.Azure.cs b/src/Aspire.Hosting.Azure/api/Aspire.Hosting.Azure.cs index 6040ea3b4fd..426d3ea70b4 100644 --- a/src/Aspire.Hosting.Azure/api/Aspire.Hosting.Azure.cs +++ b/src/Aspire.Hosting.Azure/api/Aspire.Hosting.Azure.cs @@ -10,13 +10,13 @@ namespace Aspire.Hosting { public static partial class AzureBicepResourceExtensions { - [AspireExport(Description = "Adds an Azure Bicep template resource from a file")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBicepTemplate(this IDistributedApplicationBuilder builder, string name, string bicepFile) { throw null; } - [AspireExport(Description = "Adds an Azure Bicep template resource from inline Bicep content")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBicepTemplateString(this IDistributedApplicationBuilder builder, string name, string bicepContent) { throw null; } - [AspireExport(Description = "Gets an output reference from an Azure Bicep template resource")] + [AspireExport] public static Azure.BicepOutputReference GetOutput(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } [System.Obsolete("GetSecretOutput is obsolete. Use IAzureKeyVaultResource.GetSecret instead.")] @@ -81,13 +81,13 @@ public static ApplicationModel.IResourceBuilder WithParameter(this Applica public static partial class AzureProvisionerExtensions { - [AspireExport(Description = "Adds Azure provisioning services to the distributed application builder")] + [AspireExport] public static IDistributedApplicationBuilder AddAzureProvisioning(this IDistributedApplicationBuilder builder) { throw null; } } public static partial class AzureProvisioningResourceExtensions { - [AspireExport(Description = "Adds an Azure provisioning resource to the application model")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureInfrastructure(this IDistributedApplicationBuilder builder, string name, System.Action configureInfrastructure) { throw null; } [AspireExportIgnore(Reason = "KeyVaultSecret is an Azure.Provisioning type not compatible with ATS.")] @@ -111,21 +111,21 @@ public static partial class AzureProvisioningResourceExtensions [AspireExportIgnore(Reason = "ProvisioningParameter is an Azure.Provisioning type not compatible with ATS.")] public static global::Azure.Provisioning.ProvisioningParameter AsProvisioningParameter(this Azure.BicepOutputReference outputReference, Azure.AzureResourceInfrastructure infrastructure, string? parameterName = null) { throw null; } - [AspireExport(Description = "Configures the Azure provisioning infrastructure callback")] + [AspireExport] public static ApplicationModel.IResourceBuilder ConfigureInfrastructure(this ApplicationModel.IResourceBuilder builder, System.Action configure) where T : Azure.AzureProvisioningResource { throw null; } } public static partial class AzureResourceExtensions { - [AspireExport(Description = "Clears the default Azure role assignments from a resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder ClearDefaultRoleAssignments(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IAzureResource { throw null; } - [AspireExport(Description = "Gets the normalized Bicep identifier for an Azure resource")] + [AspireExport] public static string GetBicepIdentifier(this ApplicationModel.IAzureResource resource) { throw null; } - [AspireExport(Description = "Publishes an Azure resource to the manifest as a connection string")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsConnectionString(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IAzureResource, ApplicationModel.IResourceWithConnectionString { throw null; } } @@ -252,15 +252,15 @@ public AzureEnvironmentResource(string name, ApplicationModel.ParameterResource public static partial class AzureEnvironmentResourceExtensions { - [AspireExport(Description = "Adds the shared Azure environment resource to the application model")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder AddAzureEnvironment(this IDistributedApplicationBuilder builder) { throw null; } - [AspireExport(Description = "Sets the Azure location for the shared Azure environment resource")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithLocation(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder location) { throw null; } - [AspireExport(Description = "Sets the Azure resource group for the shared Azure environment resource")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithResourceGroup(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder resourceGroup) { throw null; } } @@ -320,12 +320,24 @@ internal AzureResourceInfrastructure() : base(default!) { } public AzureProvisioningResource AspireResource { get { throw null; } } } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public sealed partial class AzureRoleAssignmentResource : AzureProvisioningResource + { + public AzureRoleAssignmentResource(string name, AzureProvisioningResource targetAzureResource, ApplicationModel.IResource? ownerResource, AzureUserAssignedIdentityResource? identityResource, System.Action configureInfrastructure) : base(default!, default!) { } + + public AzureUserAssignedIdentityResource? IdentityResource { get { throw null; } } + + public ApplicationModel.IResource? OwnerResource { get { throw null; } } + + public AzureProvisioningResource TargetAzureResource { get { throw null; } } + } + public static partial class AzureUserAssignedIdentityExtensions { - [AspireExport(Description = "Adds an Azure user-assigned identity resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureUserAssignedIdentity(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport("withUserAssignedIdentityAzureUserAssignedIdentity", MethodName = "withAzureUserAssignedIdentity", Description = "Associates an Azure user-assigned identity with a compute resource")] + [AspireExport("withUserAssignedIdentityAzureUserAssignedIdentity", MethodName = "withAzureUserAssignedIdentity")] public static ApplicationModel.IResourceBuilder WithAzureUserAssignedIdentity(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder identityResourceBuilder) where T : ApplicationModel.IComputeResource { throw null; } } @@ -520,6 +532,11 @@ public partial interface IResourceWithAzureFunctionsConfig : ApplicationModel.IR void ApplyAzureFunctionsConfiguration(System.Collections.Generic.IDictionary target, string connectionName); } + public partial interface ITokenCredentialProvider + { + global::Azure.Core.TokenCredential TokenCredential { get; } + } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] public sealed partial class PrivateEndpointTargetAnnotation : ApplicationModel.IResourceAnnotation { @@ -528,6 +545,16 @@ public PrivateEndpointTargetAnnotation(AzureProvisioningResource privateEndpoint public AzureProvisioningResource PrivateEndpointResource { get { throw null; } } } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public sealed partial class ReferenceRoleAssignmentAnnotation : ApplicationModel.IResourceAnnotation + { + public ReferenceRoleAssignmentAnnotation(AzureProvisioningResource target, System.Collections.Generic.IReadOnlySet roles) { } + + public System.Collections.Generic.IReadOnlySet Roles { get { throw null; } } + + public AzureProvisioningResource Target { get { throw null; } } + } + public partial class RoleAssignmentAnnotation : ApplicationModel.IResourceAnnotation { public RoleAssignmentAnnotation(AzureProvisioningResource target, System.Collections.Generic.IReadOnlySet roles) { } diff --git a/src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj b/src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj index 757c106895d..233e1b63140 100644 --- a/src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj +++ b/src/Aspire.Hosting.Blazor/Aspire.Hosting.Blazor.csproj @@ -6,6 +6,7 @@ aspire integration hosting blazor webassembly gateway Blazor WebAssembly hosting support for Aspire. $(NoWarn);ASPIREBLAZOR001 + true diff --git a/src/Aspire.Hosting.Blazor/Resources/BlazorWasmAppResource.cs b/src/Aspire.Hosting.Blazor/Resources/BlazorWasmAppResource.cs index a4210b92ba8..3a152f7927c 100644 --- a/src/Aspire.Hosting.Blazor/Resources/BlazorWasmAppResource.cs +++ b/src/Aspire.Hosting.Blazor/Resources/BlazorWasmAppResource.cs @@ -17,7 +17,7 @@ namespace Aspire.Hosting; /// [Experimental("ASPIREBLAZOR001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport] -public class BlazorWasmAppResource(string name, string projectPath) : Resource(name), IResourceWithEnvironment, IResourceWithParent +public sealed class BlazorWasmAppResource(string name, string projectPath) : Resource(name), IResourceWithEnvironment, IResourceWithParent { /// Fully-qualified path to the .csproj file. public string ProjectPath { get; } = projectPath; diff --git a/src/Aspire.Hosting.Blazor/api/Aspire.Hosting.Blazor.cs b/src/Aspire.Hosting.Blazor/api/Aspire.Hosting.Blazor.cs new file mode 100644 index 00000000000..8cf802af91f --- /dev/null +++ b/src/Aspire.Hosting.Blazor/api/Aspire.Hosting.Blazor.cs @@ -0,0 +1,50 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Aspire.Hosting +{ + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREBLAZOR001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public static partial class BlazorGatewayExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddBlazorGateway(this IDistributedApplicationBuilder builder, string name) { throw null; } + + [AspireExport("addBlazorWasmProject")] + public static ApplicationModel.IResourceBuilder AddBlazorWasmApp(this IDistributedApplicationBuilder builder, string name, string projectPath) { throw null; } + + [AspireExportIgnore(Reason = "Open generic type parameter TProject is not ATS-compatible.")] + public static ApplicationModel.IResourceBuilder AddBlazorWasmProject(this IDistributedApplicationBuilder builder, string name) + where TProject : IProjectMetadata, new() { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithBlazorClientApp(this ApplicationModel.IResourceBuilder gateway, ApplicationModel.IResourceBuilder wasmApp, string apiPrefix = "_api", string otlpPrefix = "_otlp", bool proxyTelemetry = true) { throw null; } + } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREBLAZOR001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public static partial class BlazorHostedExtensions + { + [AspireExportIgnore(Reason = "Blazor hosted APIs are not yet stable for ATS export.")] + public static ApplicationModel.IResourceBuilder ProxyBlazorService(this ApplicationModel.IResourceBuilder host, ApplicationModel.IResourceBuilder service, string apiPrefix = "_api") { throw null; } + + [AspireExportIgnore(Reason = "Blazor hosted APIs are not yet stable for ATS export.")] + public static ApplicationModel.IResourceBuilder ProxyBlazorTelemetry(this ApplicationModel.IResourceBuilder host, string otlpPrefix = "_otlp") { throw null; } + } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREBLAZOR001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] + public sealed partial class BlazorWasmAppResource : ApplicationModel.Resource, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResource, ApplicationModel.IResourceWithParent + { + public BlazorWasmAppResource(string name, string projectPath) : base(default!) { } + + public ApplicationModel.IResource Parent { get { throw null; } } + + public string ProjectDirectory { get { throw null; } } + + public string ProjectPath { get { throw null; } } + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting.Browsers/api/Aspire.Hosting.Browsers.cs b/src/Aspire.Hosting.Browsers/api/Aspire.Hosting.Browsers.cs index 1a408ddd69a..898757bf54f 100644 --- a/src/Aspire.Hosting.Browsers/api/Aspire.Hosting.Browsers.cs +++ b/src/Aspire.Hosting.Browsers/api/Aspire.Hosting.Browsers.cs @@ -12,7 +12,7 @@ namespace Aspire.Hosting public static partial class BrowserLogsBuilderExtensions { [System.Diagnostics.CodeAnalysis.Experimental("ASPIREBROWSERLOGS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Adds a child browser logs resource that opens tracked browser sessions, captures browser logs, and captures screenshots.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBrowserLogs(this ApplicationModel.IResourceBuilder builder, string? browser = null, string? profile = null, BrowserUserDataMode? userDataMode = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } } diff --git a/src/Aspire.Hosting.DevTunnels/api/Aspire.Hosting.DevTunnels.cs b/src/Aspire.Hosting.DevTunnels/api/Aspire.Hosting.DevTunnels.cs index e6f16237871..1823b48e113 100644 --- a/src/Aspire.Hosting.DevTunnels/api/Aspire.Hosting.DevTunnels.cs +++ b/src/Aspire.Hosting.DevTunnels/api/Aspire.Hosting.DevTunnels.cs @@ -13,7 +13,7 @@ public static partial class DevTunnelsResourceBuilderExtensions [AspireExportIgnore(Reason = "Use the dedicated polyglot overload instead.")] public static ApplicationModel.IResourceBuilder AddDevTunnel(this IDistributedApplicationBuilder builder, string name, string? tunnelId = null, DevTunnels.DevTunnelOptions? options = null) { throw null; } - [AspireExport("getEndpointByEndpointReference", MethodName = "getTunnelEndpoint", Description = "Gets the public endpoint exposed by the dev tunnel.")] + [AspireExport("getEndpointByEndpointReference", MethodName = "getTunnelEndpoint")] public static ApplicationModel.EndpointReference GetEndpoint(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.EndpointReference targetEndpointReference) { throw null; } [AspireExportIgnore(Reason = "IResource parameter type is not ATS-compatible. Use the EndpointReference-based overload instead.")] @@ -23,16 +23,16 @@ public static partial class DevTunnelsResourceBuilderExtensions public static ApplicationModel.EndpointReference GetEndpoint(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.IResourceBuilder resourceBuilder, string endpointName) where TResource : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Configures the dev tunnel to allow anonymous access.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithAnonymousAccess(this ApplicationModel.IResourceBuilder tunnelBuilder) { throw null; } [AspireExportIgnore(Reason = "DevTunnelPortOptions is not ATS-compatible. Use the overload with EndpointReference or EndpointReference + bool instead.")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.EndpointReference targetEndpoint, DevTunnels.DevTunnelPortOptions? portOptions) { throw null; } - [AspireExport("withReferenceEndpointAnonymous", MethodName = "withTunnelReferenceAnonymous", Description = "Configures the dev tunnel to expose a target endpoint with access control.")] + [AspireExport("withReferenceEndpointAnonymous", MethodName = "withTunnelReferenceAnonymous")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.EndpointReference targetEndpoint, bool allowAnonymous) { throw null; } - [AspireExport("withReferenceEndpoint", MethodName = "withTunnelReference", Description = "Configures the dev tunnel to expose a target endpoint.")] + [AspireExport("withReferenceEndpoint", MethodName = "withTunnelReference")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.EndpointReference targetEndpoint) { throw null; } [AspireExportIgnore(Reason = "This method extends generic IResourceBuilder and injects dev tunnel service discovery. It requires two IResourceBuilder parameters which makes the polyglot API confusing. Use WithReference on the DevTunnelResource builder instead.")] @@ -43,7 +43,7 @@ public static ApplicationModel.IResourceBuilder WithReference WithReference(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.IResourceBuilder resourceBuilder, DevTunnels.DevTunnelPortOptions? portOptions = null) where TResource : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport("withReferenceResourceAnonymous", MethodName = "withTunnelReferenceAll", Description = "Configures the dev tunnel to expose all endpoints on the referenced resource.")] + [AspireExport("withReferenceResourceAnonymous", MethodName = "withTunnelReferenceAll")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tunnelBuilder, ApplicationModel.IResourceBuilder resourceBuilder, bool allowAnonymous) where TResource : ApplicationModel.IResourceWithEndpoints { throw null; } } diff --git a/src/Aspire.Hosting.Docker/CompatibilitySuppressions.xml b/src/Aspire.Hosting.Docker/CompatibilitySuppressions.xml index bfdd2c52258..0497d618a92 100644 --- a/src/Aspire.Hosting.Docker/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting.Docker/CompatibilitySuppressions.xml @@ -1,25 +1,3 @@  - - - CP0002 - M:Aspire.Hosting.Docker.Resources.ServiceNodes.Swarm.UpdateConfig.get_FailOnError - lib/net8.0/Aspire.Hosting.Docker.dll - lib/net8.0/Aspire.Hosting.Docker.dll - true - - - CP0002 - M:Aspire.Hosting.Docker.Resources.ServiceNodes.Swarm.UpdateConfig.get_Parallelism - lib/net8.0/Aspire.Hosting.Docker.dll - lib/net8.0/Aspire.Hosting.Docker.dll - true - - - CP0002 - M:Aspire.Hosting.Docker.Resources.ServiceNodes.Swarm.UpdateConfig.set_FailOnError(System.Nullable{System.Boolean}) - lib/net8.0/Aspire.Hosting.Docker.dll - lib/net8.0/Aspire.Hosting.Docker.dll - true - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs index afb1d90ed6f..ccc4ef07125 100644 --- a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs +++ b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs @@ -10,31 +10,31 @@ namespace Aspire.Hosting { public static partial class DockerComposeAspireDashboardResourceBuilderExtensions { - [AspireExport(Description = "Enables or disables forwarded headers support for the Aspire dashboard")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithForwardedHeaders(this ApplicationModel.IResourceBuilder builder, bool enabled = true) { throw null; } - [AspireExport(Description = "Sets the host port for the Aspire dashboard")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port = null) { throw null; } } public static partial class DockerComposeEnvironmentExtensions { - [AspireExport(Description = "Adds a Docker Compose publishing environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDockerComposeEnvironment(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the generated Docker Compose file before it is written to disk")] + [AspireExport] public static ApplicationModel.IResourceBuilder ConfigureComposeFile(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } - [AspireExport(Description = "Configures the captured environment variables written to the Docker Compose .env file")] + [AspireExport] public static ApplicationModel.IResourceBuilder ConfigureEnvFile(this ApplicationModel.IResourceBuilder builder, System.Action> configure) { throw null; } - [AspireExport("configureDashboard", MethodName = "configureDashboard", Description = "Configures the Aspire dashboard resource for the Docker Compose environment", RunSyncOnBackgroundThread = true)] + [AspireExport("configureDashboard", MethodName = "configureDashboard", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, System.Action> configure) { throw null; } - [AspireExport(Description = "Enables or disables the Aspire dashboard for the Docker Compose environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, bool enabled = true) { throw null; } - [AspireExport(Description = "Configures properties of the Docker Compose environment", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } } @@ -43,13 +43,13 @@ public static partial class DockerComposeServiceExtensions [AspireExportIgnore(Reason = "IManifestExpressionProvider parameters are not ATS-compatible. Use the parameter-builder overload in polyglot app hosts.")] public static string AsEnvironmentPlaceholder(this ApplicationModel.IManifestExpressionProvider manifestExpressionProvider, Docker.DockerComposeServiceResource dockerComposeService) { throw null; } - [AspireExport(Description = "Creates a Docker Compose environment variable placeholder from a parameter builder")] + [AspireExport] public static string AsEnvironmentPlaceholder(this ApplicationModel.IResourceBuilder builder, Docker.DockerComposeServiceResource dockerComposeService) { throw null; } [AspireExportIgnore(Reason = "Prefer the builder or IManifestExpressionProvider overloads in polyglot app hosts to avoid duplicate asEnvironmentPlaceholder projections on ParameterResource.")] public static string AsEnvironmentPlaceholder(this ApplicationModel.ParameterResource parameter, Docker.DockerComposeServiceResource dockerComposeService) { throw null; } - [AspireExport(Description = "Publishes the resource as a Docker Compose service with custom service configuration")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsDockerComposeService(this ApplicationModel.IResourceBuilder builder, System.Action configure) where T : ApplicationModel.IComputeResource { throw null; } } @@ -425,6 +425,7 @@ public sealed partial class Build public string? Target { get { throw null; } set { } } } + [AspireExport(ExposeProperties = true)] [YamlDotNet.Serialization.YamlSerializable] public sealed partial class ConfigReference { @@ -486,6 +487,7 @@ public sealed partial class Logging public System.Collections.Generic.Dictionary Options { get { throw null; } set { } } } + [AspireExport(ExposeProperties = true)] [YamlDotNet.Serialization.YamlSerializable] public sealed partial class SecretReference { @@ -505,6 +507,7 @@ public sealed partial class SecretReference public int? Uid { get { throw null; } set { } } } + [AspireExport(ExposeProperties = true)] [YamlDotNet.Serialization.YamlSerializable] public sealed partial class Ulimit { diff --git a/src/Aspire.Hosting.EntityFrameworkCore/api/Aspire.Hosting.EntityFrameworkCore.cs b/src/Aspire.Hosting.EntityFrameworkCore/api/Aspire.Hosting.EntityFrameworkCore.cs index 2d62bdb47b3..6690fe7a8e2 100644 --- a/src/Aspire.Hosting.EntityFrameworkCore/api/Aspire.Hosting.EntityFrameworkCore.cs +++ b/src/Aspire.Hosting.EntityFrameworkCore/api/Aspire.Hosting.EntityFrameworkCore.cs @@ -25,10 +25,10 @@ public static partial class EFMigrationResourceBuilderExtensions [AspireExport] public static ApplicationModel.IResourceBuilder WithMigrationOutputDirectory(this ApplicationModel.IResourceBuilder builder, string outputDirectory) { throw null; } - [AspireExport("withMigrationsProjectFromPath", MethodName = "withMigrationsProject", Description = "Configures a separate project containing the migrations using a path")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withMigrationsProject dispatcher export.")] public static ApplicationModel.IResourceBuilder WithMigrationsProject(this ApplicationModel.IResourceBuilder builder, string projectPath) { throw null; } - [AspireExport] + [AspireExportIgnore(Reason = "Uses IProjectMetadata generic constraint which is a .NET-specific type. Polyglot app hosts use the internal withMigrationsProject dispatcher export.")] public static ApplicationModel.IResourceBuilder WithMigrationsProject(this ApplicationModel.IResourceBuilder builder) where TProject : IProjectMetadata, new() { throw null; } } @@ -41,10 +41,10 @@ public static partial class EFResourceBuilderExtensions [AspireExportIgnore(Reason = "Action> callbacks are not ATS-compatible.")] public static ApplicationModel.IResourceBuilder AddEFMigrations(this ApplicationModel.IResourceBuilder builder, string name, string dbContextTypeName, System.Action>? configureToolResource) { throw null; } - [AspireExport("addEFMigrationsWithContextType", MethodName = "addEFMigrations", Description = "Adds EF Core migration management for a specific DbContext type identified by name")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addEFMigrations dispatcher export.")] public static ApplicationModel.IResourceBuilder AddEFMigrations(this ApplicationModel.IResourceBuilder builder, string name, string dbContextTypeName) { throw null; } - [AspireExport] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addEFMigrations dispatcher export.")] public static ApplicationModel.IResourceBuilder AddEFMigrations(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } } } @@ -72,8 +72,10 @@ public EFMigrationResource(string name, ApplicationModel.ProjectResource project public ApplicationModel.ProjectResource ProjectResource { get { throw null; } } + [AspireExportIgnore(Reason = "Conflicts with the publishAsMigrationBundle builder method export.")] public bool PublishAsMigrationBundle { get { throw null; } set { } } + [AspireExportIgnore(Reason = "Conflicts with the publishAsMigrationScript builder method export.")] public bool PublishAsMigrationScript { get { throw null; } set { } } public bool PublishBundleContainer { get { throw null; } set { } } diff --git a/src/Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.csproj b/src/Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.csproj index 709af738f3a..d5dd4cae6ad 100644 --- a/src/Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.csproj +++ b/src/Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs b/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs index 0fbcfa32a56..a73b8de2ddb 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/AzureHostedAgentResource.cs @@ -25,7 +25,10 @@ namespace Aspire.Hosting.Foundry; /// public class AzureHostedAgentResource : Resource, IResourceWithEnvironment { - private const string AzureAIUserRoleDefinitionId = "53ca6127-db72-4b80-b1b0-d745d6d5456d"; + // The "Azure AI User" built-in role (data-plane access to Foundry agents/inference). Granted to + // the agent's own instance identity below, and to consumers that reference the agent (see + // HostedAgentResourceBuilderExtensions.GrantHostedAgentConsumerRoles). + internal const string AzureAIUserRoleDefinitionId = "53ca6127-db72-4b80-b1b0-d745d6d5456d"; /// /// Creates a new instance of the class. @@ -161,12 +164,66 @@ private async Task DeployAsync(PipelineStepContext context cancellationToken: context.CancellationToken ).ConfigureAwait(false); + await UpdateAgentEndpointProtocolsAsync(projectClient.AgentAdministrationClient, def, context.CancellationToken).ConfigureAwait(false); + // Foundry should do this automatically in the future. await AssignFoundryRoleToAgentIdentityAsync(context, project, result.Value, provisioningContext).ConfigureAwait(false); return result.Value; } + private async Task UpdateAgentEndpointProtocolsAsync(AgentAdministrationClient agentsClient, HostedAgentConfiguration configuration, CancellationToken cancellationToken) + { + var endpointProtocols = GetAgentEndpointProtocols(configuration.ContainerProtocolVersions); + if (endpointProtocols.Count == 0) + { + return; + } + + var endpoint = new AgentEndpoint(); + foreach (var protocol in endpointProtocols) + { + endpoint.Protocols.Add(protocol); + } + + // Creating a hosted-agent version does not update the endpoint's advertised protocols; + // keep routing in sync so endpoint-scoped invocations can reach the selected version. + await agentsClient.PatchAgentObjectAsync( + Name, + new PatchAgentOptions + { + AgentEndpoint = endpoint + }, + cancellationToken).ConfigureAwait(false); + } + + internal static IReadOnlyList GetAgentEndpointProtocols(IEnumerable protocolVersions) + { + var endpointProtocols = new List(); + + foreach (var protocolVersion in protocolVersions) + { + var endpointProtocol = ToAgentEndpointProtocol(protocolVersion.Protocol); + if (!endpointProtocols.Contains(endpointProtocol)) + { + endpointProtocols.Add(endpointProtocol); + } + } + + return endpointProtocols; + } + + private static AgentEndpointProtocol ToAgentEndpointProtocol(ProjectsAgentProtocol protocol) + { + return protocol.ToString() switch + { + "activity_protocol" => AgentEndpointProtocol.Activity, + "invocations" => AgentEndpointProtocol.Invocations, + "responses" => AgentEndpointProtocol.Responses, + var value => new AgentEndpointProtocol(value) + }; + } + private async Task AssignFoundryRoleToAgentIdentityAsync( PipelineStepContext context, AzureCognitiveServicesProjectResource project, @@ -253,6 +310,15 @@ internal static async Task> GetResolvedEnvironmentVar var resolvedEnvVars = new Dictionary(); foreach (var (key, value) in collectedEnvVars) { + if (HostedAgentConfiguration.IsReservedEnvironmentVariableName(key)) + { + // Foundry injects platform-owned variables such as PORT itself. Some Aspire resource + // types use these variables to model local/container startup, but forwarding them in + // the hosted-agent definition causes Foundry to reject the version payload. + logger.LogDebug("Environment variable '{Key}' for resource '{Name}' is reserved by Foundry Hosted Agents and will be skipped.", key, resource.Name); + continue; + } + switch (value) { case null: @@ -366,8 +432,7 @@ internal static async Task> GetResolvedEnvironmentVar throw CreateEndpointResolutionException(hostedAgent, resource, environmentVariableName, endpointReference, $"Endpoint '{endpoint.Name}' is internal. Foundry hosted agents can only reference externally exposed endpoints during publish."); } - var deploymentTarget = endpointReference.Resource.GetDeploymentTargetAnnotation(); - if (deploymentTarget?.ComputeEnvironment is not { } computeEnvironment) + if (!ComputeEnvironmentEndpointResolver.TryGetEffectiveComputeEnvironment(endpointReference.Resource, out var computeEnvironment)) { var reason = $"Resource '{endpointReference.Resource.Name}' does not have a compute environment deployment target."; throw CreateEndpointResolutionException(hostedAgent, resource, environmentVariableName, endpointReference, reason); diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs index c2d0161a82d..5e21a6d0e2d 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs @@ -3,8 +3,8 @@ using System.Net.Http.Json; using System.Text.Json; -using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; using Aspire.Hosting.Foundry; using Microsoft.Extensions.DependencyInjection; @@ -16,6 +16,8 @@ namespace Aspire.Hosting; public static class HostedAgentResourceBuilderExtensions { private static readonly JsonSerializerOptions s_indentedJsonOptions = new() { WriteIndented = true }; + private const string ResponsesProtocol = "responses"; + private const string InvocationsProtocol = "invocations"; /// /// Configures the resource to run locally as a Microsoft Foundry hosted agent. @@ -46,8 +48,8 @@ public static IResourceBuilder AsHostedAgent(this IResourceBuilder buil // The internal AsHostedAgentForExport overload below is the polyglot-exported version of AsHostedAgent. // The CLR method name differs from AsHostedAgent to avoid C# overload ambiguity with the Action-based // overload, but the ATS capability name must stay "asHostedAgent" for compatibility. - // .NET callers should keep using the Action overload above, which exposes - // the full HostedAgentConfiguration surface (tools, content filters, container protocol versions, etc.). + // .NET callers should keep using the Action overload, which exposes the + // full HostedAgentConfiguration surface (tools, content filters, container protocol versions, etc.). /// /// Configures the resource to run and publish as a hosted agent in Microsoft Foundry, targeting the specified Foundry project. @@ -55,7 +57,7 @@ public static IResourceBuilder AsHostedAgent(this IResourceBuilder buil /// The type of resource being configured. /// The resource builder for the compute resource. /// The Microsoft Foundry project the hosted agent is deployed into. - /// Optional hosted agent deployment options (description, CPU, memory, metadata, environment variables) applied in publish mode. + /// Optional hosted agent deployment options. Protocols apply in run and publish mode; other options apply in publish mode. /// A reference to the for chaining. /// The resource builder. /// Thrown when or is . @@ -69,7 +71,7 @@ internal static IResourceBuilder AsHostedAgentForExport( ArgumentNullException.ThrowIfNull(project); Action? configure = options is null ? null : options.ApplyTo; - return AsHostedAgent(builder, project: project, configure: configure); + return ConfigureAsHostedAgent(builder, project: project, configure: configure); } /// @@ -80,20 +82,33 @@ internal static IResourceBuilder AsHostedAgentForExport( /// The type of resource being configured. /// The resource builder for the compute resource. /// Optional Microsoft Foundry project resource used for both run and publish mode configuration. When , an existing Foundry project in the model is reused or a new project is created in publish mode. - /// A callback to configure hosted agent deployment options in publish mode. + /// A callback to configure hosted agent deployment options. /// A reference to the for chaining. - [AspireExportIgnore(Reason = "Action callback shape is awkward for polyglot hosts; the HostedAgentOptions overload is exported instead.")] + /// + /// The setting affects both run and publish mode. + /// Other settings are used in publish mode. + /// + [AspireExportIgnore(Reason = "Action callback shape is awkward for polyglot hosts; the HostedAgentOptions DTO shape is exported instead.")] public static IResourceBuilder AsHostedAgent( this IResourceBuilder builder, IResourceBuilder? project, Action? configure = null) where T : IResourceWithEndpoints, IResourceWithEnvironment, IComputeResource + { + return ConfigureAsHostedAgent(builder, project, configure); + } + + private static IResourceBuilder ConfigureAsHostedAgent( + this IResourceBuilder builder, + IResourceBuilder? project, + Action? configure) + where T : IResourceWithEndpoints, IResourceWithEnvironment, IComputeResource { ArgumentNullException.ThrowIfNull(builder); if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - ConfigureRunMode(builder); + ConfigureRunMode(builder, configure); if (project is not null) { @@ -116,7 +131,7 @@ public static IResourceBuilder AsHostedAgent( /// /// The type of resource being configured. /// The resource builder for the compute resource. - /// A callback to configure hosted agent deployment options in publish mode. + /// A callback to configure hosted agent deployment options. /// A reference to the for chaining. [AspireExportIgnore(Reason = "Subset of the full AsHostedAgent overload.")] public static IResourceBuilder AsHostedAgent( @@ -157,9 +172,11 @@ private static IResourceBuilder ResolvePr .AddProject($"{builder.Resource.Name}-proj"); } - private static void ConfigureRunMode(IResourceBuilder builder) + private static void ConfigureRunMode(IResourceBuilder builder, Action? configure) where T : IResourceWithEndpoints, IResourceWithEnvironment, IComputeResource { + var protocol = GetRunProtocol(configure); + // Preserve any target port already configured on an existing "http" endpoint; // fall back to the default MAF agent port (8088) when none is set. var existingHttpEndpoint = builder.Resource.Annotations.OfType().FirstOrDefault(e => e.Name == "http"); @@ -175,14 +192,14 @@ private static void ConfigureRunMode(IResourceBuilder builder) { return; } - http.DisplayText = "Responses Endpoint"; + http.DisplayText = protocol.EndpointDisplayText; http.Url = new UriBuilder(http.Url) { - Path = "/responses" + Path = protocol.Path }.ToString(); }) .WithHttpCommand( - path: "/responses", + path: protocol.Path, displayName: "Send Message", endpointName: "http", commandOptions: new() @@ -195,7 +212,7 @@ private static void ConfigureRunMode(IResourceBuilder builder) { var interactionService = ctx.ServiceProvider.GetRequiredService(); var result = await interactionService.PromptInputAsync( - title: "Responses API", + title: protocol.PromptTitle, message: "Enter a message to send to the agent.", inputLabel: "Message", placeHolder: "I would like to know the weather today.", @@ -208,7 +225,7 @@ private static void ConfigureRunMode(IResourceBuilder builder) } var request = ctx.Request; var input = result.Data.Value; - request.Content = new StringContent(new JsonObject() { ["input"] = input }.ToString(), System.Text.Encoding.UTF8, "application/json"); + request.Content = protocol.CreateRequestContent(input); }, GetCommandResult = async ctx => { @@ -224,17 +241,32 @@ private static void ConfigureRunMode(IResourceBuilder builder) CommandResultFormat.Text); } - var responseJson = await response.Content.ReadFromJsonAsync(cancellationToken: ctx.CancellationToken).ConfigureAwait(true); - if (responseJson is null) + if (protocol.ExpectsJsonResponse) + { + var responseJson = await response.Content.ReadFromJsonAsync(cancellationToken: ctx.CancellationToken).ConfigureAwait(true); + if (responseJson.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return CommandResults.Failure("Agent returned an empty response."); + } + + var formattedResponse = JsonSerializer.Serialize(responseJson, s_indentedJsonOptions); + return CommandResults.Success( + message: "Agent response received.", + result: formattedResponse, + resultFormat: CommandResultFormat.Json, + displayImmediately: true); + } + + var responseText = await response.Content.ReadAsStringAsync(ctx.CancellationToken).ConfigureAwait(true); + if (string.IsNullOrEmpty(responseText)) { return CommandResults.Failure("Agent returned an empty response."); } - var formattedResponse = JsonSerializer.Serialize(responseJson, s_indentedJsonOptions); return CommandResults.Success( message: "Agent response received.", - result: formattedResponse, - resultFormat: CommandResultFormat.Json, + result: responseText, + resultFormat: CommandResultFormat.Text, displayImmediately: true); }, } @@ -261,6 +293,47 @@ private static void ConfigureRunMode(IResourceBuilder builder) }); } + private static HostedAgentRunProtocol GetRunProtocol(Action? configure) + { + var protocol = GetConfiguredRunProtocol(configure); + if (string.IsNullOrWhiteSpace(protocol) || string.Equals(protocol, ResponsesProtocol, StringComparison.OrdinalIgnoreCase)) + { + return HostedAgentRunProtocol.Responses; + } + + if (string.Equals(protocol, InvocationsProtocol, StringComparison.OrdinalIgnoreCase)) + { + return HostedAgentRunProtocol.Invocations; + } + + throw new NotSupportedException($"Foundry hosted agent protocol '{protocol}' is not supported in run mode. Supported protocols: '{ResponsesProtocol}', '{InvocationsProtocol}'."); + } + + private static string? GetConfiguredRunProtocol(Action? configure) + { + if (configure is null) + { + return null; + } + + // Run mode does not need the deployment image, but the same configuration callback is also used in + // publish mode where the image is known. Use a scratch configuration here so protocol selection has + // one C# API surface across run and publish mode. + var configuration = new HostedAgentConfiguration(image: string.Empty); + try + { + configure(configuration); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to apply the hosted agent configuration callback while determining the Foundry hosted agent protocol for run mode. In run mode, only {nameof(HostedAgentConfiguration.ContainerProtocolVersions)} is used; other options can still be validated by the callback.", + ex); + } + + return configuration.ContainerProtocolVersions.FirstOrDefault()?.Protocol.ToString(); + } + private static void ConfigurePublishMode( IResourceBuilder builder, IResourceBuilder project, @@ -347,5 +420,67 @@ private static void ConfigurePublishMode( builder.ApplicationBuilder.AddResource(hostedAgent) .WithIconName("Agents") .WithReferenceRelationship(target); + + // Referencing a hosted agent (its node app) only injects the agent's service-discovery URL. + // Unlike referencing a first-class Azure resource, it does not give the consumer a managed + // identity or any RBAC on the Foundry account, so calls to the agent's invocation endpoint + // fail with 401/403 at runtime. Stamp a ReferenceRoleAssignmentAnnotation on the agent's + // target so AzureResourcePreparer grants the "Azure AI User" role on the owning Foundry + // account to every consumer that references this agent, and provisions the identity that + // makes ACA inject AZURE_CLIENT_ID. + StampHostedAgentConsumerRoleAnnotation(target, projectResource.Parent); + } + + private static void StampHostedAgentConsumerRoleAnnotation(IResourceWithEnvironment target, FoundryResource account) + { + // Grant only the "Azure AI User" role required to invoke the hosted agent. We deliberately do + // not union the account's default data-plane roles here: + // - A consumer that also references the account directly still receives those defaults through + // AzureResourcePreparer's normal reference walk (they are preserved when GetAllRoleAssignments + // unions per target). + // - A consumer that declares explicit role assignments on the account intentionally suppresses + // the account defaults; folding them back in here would defeat that suppression. + // So the minimal, least-privilege grant for a pure agent consumer is "Azure AI User" alone. + var roles = new HashSet + { + new(AzureHostedAgentResource.AzureAIUserRoleDefinitionId, "Azure AI User") + }; + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. + target.Annotations.Add(new ReferenceRoleAssignmentAnnotation(account, roles)); +#pragma warning restore ASPIREAZURE003 + } + + private sealed class HostedAgentRunProtocol + { + public static HostedAgentRunProtocol Responses { get; } = new() + { + Path = "/responses", + EndpointDisplayText = "Responses Endpoint", + PromptTitle = "Responses API", + ExpectsJsonResponse = true, + CreateRequestContent = input => JsonContent.Create(new { input }) + }; + + public static HostedAgentRunProtocol Invocations { get; } = new() + { + Path = "/invocations", + EndpointDisplayText = "Invocations Endpoint", + PromptTitle = "Invocations API", + ExpectsJsonResponse = false, + // Agent Framework's invocations host expects a JSON body with a "message" field: + // https://github.com/microsoft/agent-framework/blob/main/python/packages/foundry_hosting/agent_framework_foundry_hosting/_invocations.py + CreateRequestContent = input => JsonContent.Create(new { message = input }) + }; + + public required string Path { get; init; } + + public required string EndpointDisplayText { get; init; } + + public required string PromptTitle { get; init; } + + public required bool ExpectsJsonResponse { get; init; } + + public required Func CreateRequestContent { get; init; } } } diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentConfiguration.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentConfiguration.cs index 289dbc636b7..206e1f77326 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentConfiguration.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentConfiguration.cs @@ -116,6 +116,7 @@ public decimal Memory internal ProjectsAgentVersionCreationOptions ToProjectsAgentVersionCreationOptions(string targetResourceName) { ValidateEnvironmentVariableNames(EnvironmentVariables.Keys, targetResourceName); + ValidateEnvironmentVariableNamesAreNotReserved(EnvironmentVariables.Keys, targetResourceName); var def = new HostedAgentDefinition( ContainerProtocolVersions, @@ -165,6 +166,30 @@ private static void ValidateEnvironmentVariableNames(IEnumerable environ $"Invalid name(s): '{string.Join("', '", invalidNames)}'"); } + private static void ValidateEnvironmentVariableNamesAreNotReserved(IEnumerable environmentVariableNames, string? targetResourceName) + { + var reservedNames = environmentVariableNames + .Where(IsReservedEnvironmentVariableName) + .Order(StringComparer.Ordinal) + .ToArray(); + + if (reservedNames.Length == 0) + { + return; + } + + throw new DistributedApplicationException( + $"Foundry hosted agent for target resource '{targetResourceName}' contains environment variable names that are reserved by Foundry Hosted Agents. " + + $"Reserved name(s): '{string.Join("', '", reservedNames)}'"); + } + + internal static bool IsReservedEnvironmentVariableName(string name) + { + return string.Equals(name, "PORT", StringComparison.OrdinalIgnoreCase) || + name.StartsWith("FOUNDRY_", StringComparison.OrdinalIgnoreCase) || + name.StartsWith("AGENT_", StringComparison.OrdinalIgnoreCase); + } + // hosted agent environment variables must contain only letters, digits, or underscores. [GeneratedRegex("^[A-Za-z0-9_]+$")] private static partial Regex EnvironmentVariableNameRegex(); diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentOptions.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentOptions.cs index 7cf11d3c78c..0cb543793db 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentOptions.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentOptions.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Azure.AI.Projects.Agents; + namespace Aspire.Hosting.Foundry; -// HostedAgentOptions exposes the subset of HostedAgentConfiguration that is meaningful to non-.NET -// app hosts. .NET callers should use the AsHostedAgent overload that takes Action -// to access the full configuration surface (tools, content filters, container protocol versions, etc.). +// HostedAgentOptions exposes the subset of HostedAgentConfiguration that can be shared by .NET and +// polyglot app hosts. .NET callers can use the AsHostedAgent overload that takes +// Action when they need the full Azure SDK-specific configuration surface. /// /// Options that control how a compute resource is deployed as a Microsoft Foundry hosted agent. @@ -45,6 +47,16 @@ internal sealed class HostedAgentOptions /// public IDictionary EnvironmentVariables { get; init; } = new Dictionary(); + /// + /// Protocol versions that the hosted agent container supports for ingress communication. + /// When not set, the hosted agent default responses protocol is used. + /// + /// + /// In run mode, the first protocol entry selects the dashboard URL and HTTP command protocol. + /// In publish mode, all entries are emitted to the Foundry hosted agent definition. + /// + public IList Protocols { get; init; } = []; + internal void ApplyTo(HostedAgentConfiguration configuration) { if (Description is not null) @@ -73,5 +85,77 @@ internal void ApplyTo(HostedAgentConfiguration configuration) { configuration.EnvironmentVariables[kvp.Key] = kvp.Value; } + + var protocols = ValidateProtocols(); + if (protocols.Count > 0) + { + var protocolVersionRecords = protocols.Select(ToProtocolVersionRecord).ToArray(); + + configuration.ContainerProtocolVersions.Clear(); + foreach (var record in protocolVersionRecords) + { + configuration.ContainerProtocolVersions.Add(record); + } + } } + + private IList ValidateProtocols() + { + if (Protocols is null) + { + throw new ArgumentNullException(nameof(Protocols), "Hosted agent protocols cannot be null."); + } + + foreach (var protocol in Protocols) + { + ValidateProtocol(protocol); + } + + return Protocols; + } + + private static void ValidateProtocol(HostedAgentProtocolVersion protocolVersion) + { + if (protocolVersion is null) + { + throw new ArgumentNullException(nameof(protocolVersion), "Hosted agent protocols cannot contain null entries."); + } + + if (string.IsNullOrWhiteSpace(protocolVersion.Protocol)) + { + ThrowInvalidProtocolProperty(nameof(HostedAgentProtocolVersion.Protocol), "Hosted agent protocol cannot be null, empty, or whitespace."); + } + + if (string.IsNullOrWhiteSpace(protocolVersion.Version)) + { + ThrowInvalidProtocolProperty(nameof(HostedAgentProtocolVersion.Version), "Hosted agent protocol version cannot be null, empty, or whitespace."); + } + } + + private static void ThrowInvalidProtocolProperty(string propertyName, string message) + { + throw new ArgumentException(message, propertyName); + } + + private static ProtocolVersionRecord ToProtocolVersionRecord(HostedAgentProtocolVersion protocolVersion) + { + return new ProtocolVersionRecord(new ProjectsAgentProtocol(protocolVersion.Protocol), protocolVersion.Version); + } +} + +/// +/// A protocol and version supported by a Microsoft Foundry hosted agent container. +/// +[AspireDto] +internal sealed class HostedAgentProtocolVersion +{ + /// + /// The protocol name, such as responses or invocations. + /// + public required string Protocol { get; init; } + + /// + /// The protocol version, such as 1.0.0. + /// + public required string Version { get; init; } } diff --git a/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs b/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs index 8fcabc67d52..ac90f16f99e 100644 --- a/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs +++ b/src/Aspire.Hosting.Foundry/Project/ProjectResource.cs @@ -212,9 +212,48 @@ public AzureContainerRegistryResource? ContainerRegistry /// Get the address for the particular agent's endpoint. /// ReferenceExpression IComputeEnvironmentResource.GetHostAddressExpression(EndpointReference endpointReference) + => GetAgentAddressExpression(endpointReference); + + /// + /// Produces the endpoint property expression for a hosted agent endpoint owned by this Foundry project. + /// + /// + /// The agent address is already a fully-qualified https URL + /// (for example https://{account}.services.ai.azure.com/.../agents/{name}), so it is + /// returned directly for URL-shaped properties. The default + /// implementation composes + /// {scheme}://{host}, which would produce a malformed double-scheme value + /// (for example http://https://...). Only the properties that WithReference emits for a + /// hosted agent endpoint are supported. + /// + ReferenceExpression IComputeEnvironmentResource.GetEndpointPropertyExpression(EndpointReferenceExpression endpointReferenceExpression) + { + ArgumentNullException.ThrowIfNull(endpointReferenceExpression); + + var property = endpointReferenceExpression.Property; + return property switch + { + EndpointProperty.Url => GetAgentAddressExpression(endpointReferenceExpression.Endpoint), + EndpointProperty.Scheme => ReferenceExpression.Create($"https"), + _ => throw new InvalidOperationException( + $"The endpoint property '{property}' is not supported for Foundry hosted agent endpoints. Only 'Url' and 'Scheme' are supported.") + }; + } + + private ReferenceExpression GetAgentAddressExpression(EndpointReference endpointReference) { var resource = endpointReference.Resource; - return ReferenceExpression.Create($"{Endpoint}/agents/{resource.Name}"); + + // For hosted agents, deployment creates the Foundry agent version using the wrapper + // AzureHostedAgentResource.Name (e.g. "agent-ha" for a target named "agent"), not the + // target resource name. The published cross-environment URL must point at that deployed + // agent name, so prefer the hosted-agent deployment target's name when one exists and fall + // back to the resource name for plain (non-hosted) agents. + var agentName = resource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget is AzureHostedAgentResource hostedAgent + ? hostedAgent.Name + : resource.Name; + + return ReferenceExpression.Create($"{Endpoint}/agents/{agentName}"); } /// diff --git a/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.ats.txt b/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.ats.txt index d203fa14a53..bff2fdcab1a 100644 --- a/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.ats.txt +++ b/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.ats.txt @@ -268,7 +268,7 @@ Aspire.Hosting.Foundry/addSearchConnection(search: Aspire.Hosting.Azure.Search/A Aspire.Hosting.Foundry/addSharePointTool(name: string, projectConnectionIds: string[]) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.SharePointToolResource Aspire.Hosting.Foundry/addStorageConnection(storage: Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.AzureStorageResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectConnectionResource Aspire.Hosting.Foundry/addWebSearchTool(name: string) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.WebSearchToolResource -Aspire.Hosting.Foundry/asHostedAgent(project: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, options?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints +Aspire.Hosting.Foundry/asHostedAgentExecutable(project: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, options?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.HostedAgentOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting.Foundry/AzurePromptAgentResource.connectionStringExpression(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression Aspire.Hosting.Foundry/AzurePromptAgentResource.description(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> string Aspire.Hosting.Foundry/AzurePromptAgentResource.instructions(context: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzurePromptAgentResource) -> string @@ -304,6 +304,7 @@ Aspire.Hosting.Foundry/runAsFoundryLocal() -> Aspire.Hosting.Foundry/Aspire.Host Aspire.Hosting.Foundry/withAppInsights(appInsights: Aspire.Hosting.Azure.ApplicationInsights/Aspire.Hosting.Azure.AzureApplicationInsightsResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource Aspire.Hosting.Foundry/withBingReference(bingReference: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.BingGroundingConnectionResource|string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.BingGroundingToolResource Aspire.Hosting.Foundry/withCapabilityHost(resource: Aspire.Hosting.Azure.CosmosDB/Aspire.Hosting.AzureCosmosDBResource|Aspire.Hosting.Azure.Storage/Aspire.Hosting.Azure.AzureStorageResource|Aspire.Hosting.Azure.Search/Aspire.Hosting.Azure.AzureSearchResource|Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource +Aspire.Hosting.Foundry/withComputeEnvironmentExecutable(project?: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource, configure?: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting.Foundry/withFoundryDeploymentProperties(configure: callback) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryDeploymentResource Aspire.Hosting.Foundry/withFoundryRoleAssignments(target: Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.FoundryResource, roles: enum:Aspire.Hosting.FoundryRole[]) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting.Foundry/withKeyVault(keyVault: Aspire.Hosting.Azure.KeyVault/Aspire.Hosting.Azure.AzureKeyVaultResource) -> Aspire.Hosting.Foundry/Aspire.Hosting.Foundry.AzureCognitiveServicesProjectResource diff --git a/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.cs b/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.cs index 2889e15e269..5c410ca70c3 100644 --- a/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.cs +++ b/src/Aspire.Hosting.Foundry/api/Aspire.Hosting.Foundry.cs @@ -10,31 +10,31 @@ namespace Aspire.Hosting { public static partial class AzureCognitiveServicesProjectConnectionsBuilderExtensions { - [AspireExport("addBingGroundingConnectionFromParameter", Description = "Adds a Grounding with Bing Search connection to a Microsoft Foundry project using a parameter.")] + [AspireExport("addBingGroundingConnectionFromParameter")] public static ApplicationModel.IResourceBuilder AddBingGroundingConnection(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder bingResourceId) { throw null; } - [AspireExport(Description = "Adds a Grounding with Bing Search connection to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBingGroundingConnection(this ApplicationModel.IResourceBuilder builder, string name, string bingResourceId) { throw null; } - [AspireExport("addContainerRegistryConnection", Description = "Adds an Azure Container Registry connection to a Microsoft Foundry project.")] + [AspireExport("addContainerRegistryConnection")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder registry) { throw null; } - [AspireExport("addKeyVaultConnection", Description = "Adds an Azure Key Vault connection to a Microsoft Foundry project.")] + [AspireExport("addKeyVaultConnection")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder keyVault) { throw null; } - [AspireExport("addSearchConnection", Description = "Adds an Azure AI Search connection to a Microsoft Foundry project.")] + [AspireExport("addSearchConnection")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder search) { throw null; } - [AspireExport("addStorageConnection", Description = "Adds an Azure Storage connection to a Microsoft Foundry project.")] + [AspireExport("addStorageConnection")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder storage) { throw null; } - [AspireExport("addCosmosConnection", Description = "Adds an Azure Cosmos DB connection to a Microsoft Foundry project.")] + [AspireExport("addCosmosConnection")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder db) { throw null; } [AspireExportIgnore(Reason = "Raw AzureContainerRegistryResource parameters are not ATS-compatible. Use the resource-builder overload instead.")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, Azure.AzureContainerRegistryResource registry) { throw null; } - [AspireExport("addSearchConnectionFromResource", Description = "Adds an Azure AI Search connection to a Microsoft Foundry project.")] + [AspireExportIgnore(Reason = "Raw AzureSearchResource parameters are not ATS-compatible. Use the resource-builder overload instead.")] public static ApplicationModel.IResourceBuilder AddConnection(this ApplicationModel.IResourceBuilder builder, Azure.AzureSearchResource search) { throw null; } [AspireExportIgnore(Reason = "Raw AzureStorageResource parameters are not ATS-compatible. Use the resource-builder overload instead.")] @@ -58,13 +58,13 @@ public static partial class AzureCognitiveServicesProjectExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addModelDeployment dispatcher export.")] public static ApplicationModel.IResourceBuilder AddModelDeployment(this ApplicationModel.IResourceBuilder builder, string name, string modelName, string modelVersion, string format) { throw null; } - [AspireExport(Description = "Adds a Microsoft Foundry project resource to a Microsoft Foundry resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddProject(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Associates an Azure Application Insights resource with a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithAppInsights(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder appInsights) { throw null; } - [AspireExport(Description = "Associates an Azure Key Vault resource with a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithKeyVault(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder keyVault) { throw null; } [AspireExportIgnore(Reason = "The standard WithReference export already covers this polyglot scenario.")] @@ -80,13 +80,13 @@ public static partial class FoundryExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addDeployment dispatcher export.")] public static ApplicationModel.IResourceBuilder AddDeployment(this ApplicationModel.IResourceBuilder builder, string name, string modelName, string modelVersion, string format) { throw null; } - [AspireExport(Description = "Adds a Microsoft Foundry resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddFoundry(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the Microsoft Foundry resource to run by using Foundry Local.")] + [AspireExport] public static ApplicationModel.IResourceBuilder RunAsFoundryLocal(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport("withFoundryDeploymentProperties", MethodName = "withProperties", Description = "Configures properties of a Microsoft Foundry deployment resource.", RunSyncOnBackgroundThread = true)] + [AspireExport("withFoundryDeploymentProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } [AspireExportIgnore(Reason = "CognitiveServicesBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the FoundryRole-based overload instead.")] @@ -96,64 +96,64 @@ public static ApplicationModel.IResourceBuilder WithRoleAssignments(this A public static partial class HostedAgentResourceBuilderExtensions { - [AspireExport] - public static ApplicationModel.IResourceBuilder AsHostedAgent(this ApplicationModel.IResourceBuilder builder) + [AspireExportIgnore(Reason = "Action callback shape is awkward for polyglot hosts; the HostedAgentOptions DTO shape is exported instead.")] + public static ApplicationModel.IResourceBuilder AsHostedAgent(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder? project, System.Action? configure = null) where T : ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IComputeResource { throw null; } - [AspireExport("withComputeEnvironmentExecutable", MethodName = "withComputeEnvironment")] - public static ApplicationModel.IResourceBuilder WithComputeEnvironment(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder? project = null, System.Action? configure = null) + [AspireExportIgnore(Reason = "Subset of the full AsHostedAgent overload.")] + public static ApplicationModel.IResourceBuilder AsHostedAgent(this ApplicationModel.IResourceBuilder builder, System.Action configure) where T : ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IComputeResource { throw null; } - [AspireExportIgnore(Reason = "Subset of the full WithComputeEnvironment overload which is exported.")] - public static ApplicationModel.IResourceBuilder WithComputeEnvironment(this ApplicationModel.IResourceBuilder builder, System.Action configure) + [AspireExportIgnore(Reason = "Subset of the full AsHostedAgent(project) overload which is exported.")] + public static ApplicationModel.IResourceBuilder AsHostedAgent(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResourceWithEnvironment, ApplicationModel.IComputeResource { throw null; } } public static partial class PromptAgentBuilderExtensions { - [AspireExport(Description = "Adds an Azure AI Search tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAISearchTool(this ApplicationModel.IResourceBuilder project, string name, string? indexName = null) { throw null; } [AspireExportIgnore(Reason = "BinaryData parameter is not ATS-compatible. Use the string overload instead.")] public static ApplicationModel.IResourceBuilder AddAzureFunctionTool(this ApplicationModel.IResourceBuilder project, string name, string functionName, string description, System.BinaryData parameters, string inputQueueEndpoint, string inputQueueName, string outputQueueEndpoint, string outputQueueName) { throw null; } - [AspireExport(Description = "Adds an Azure Function tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAzureFunctionTool(this ApplicationModel.IResourceBuilder project, string name, string functionName, string description, string parametersJson, string inputQueueEndpoint, string inputQueueName, string outputQueueEndpoint, string outputQueueName) { throw null; } - [AspireExport(Description = "Adds a Bing Grounding tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddBingGroundingTool(this ApplicationModel.IResourceBuilder project, string name) { throw null; } - [AspireExport(Description = "Adds a Code Interpreter tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddCodeInterpreterTool(this ApplicationModel.IResourceBuilder project, string name) { throw null; } - [AspireExport(Description = "Adds a Computer Use tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddComputerUseTool(this ApplicationModel.IResourceBuilder project, string name, int displayWidth = 1024, int displayHeight = 768, string environment = "browser") { throw null; } - [AspireExport(Description = "Adds a Microsoft Fabric data agent tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddFabricTool(this ApplicationModel.IResourceBuilder project, string name, params string[] projectConnectionIds) { throw null; } - [AspireExport(Description = "Adds a File Search tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddFileSearchTool(this ApplicationModel.IResourceBuilder project, string name, params string[] vectorStoreIds) { throw null; } [AspireExportIgnore(Reason = "BinaryData parameter is not ATS-compatible. Use the string overload instead.")] public static ApplicationModel.IResourceBuilder AddFunctionTool(this ApplicationModel.IResourceBuilder project, string name, string functionName, System.BinaryData parameters, string? description = null, bool? strictModeEnabled = null) { throw null; } - [AspireExport(Description = "Adds an Image Generation tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddImageGenerationTool(this ApplicationModel.IResourceBuilder project, string name) { throw null; } - [AspireExport(Description = "Adds a prompt agent to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPromptAgent(this ApplicationModel.IResourceBuilder project, string name, ApplicationModel.IResourceBuilder model, string? instructions = null) { throw null; } - [AspireExport(Description = "Adds a SharePoint grounding tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddSharePointTool(this ApplicationModel.IResourceBuilder project, string name, params string[] projectConnectionIds) { throw null; } - [AspireExport(Description = "Adds a Web Search tool to a Microsoft Foundry project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddWebSearchTool(this ApplicationModel.IResourceBuilder project, string name) { throw null; } [AspireExportIgnore(Reason = "IFoundryTool is not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithCustomTool(this ApplicationModel.IResourceBuilder builder, Foundry.IFoundryTool tool) { throw null; } - [AspireExport(Description = "Links an Azure AI Search tool to a backing search resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tool, ApplicationModel.IResourceBuilder search) { throw null; } [AspireExportIgnore(Reason = "Covered by the internal AspireUnion overload.")] @@ -165,7 +165,7 @@ public static partial class PromptAgentBuilderExtensions [AspireExportIgnore(Reason = "Covered by the internal AspireUnion overload.")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder tool, string bingResourceId) { throw null; } - [AspireExport(Description = "Adds a tool to a prompt agent.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithTool(this ApplicationModel.IResourceBuilder agent, ApplicationModel.IResourceBuilder tool) { throw null; } } } @@ -217,6 +217,8 @@ public AzureCognitiveServicesProjectResource(string name, System.Action> ApplicationModel.IResourceWithConnectionString.GetConnectionProperties() { throw null; } @@ -439,14 +441,6 @@ public partial class FoundryModel public required string Version { get { throw null; } init { } } - public static partial class AI21Labs - { - [AspireValue("FoundryModels")] - public static readonly FoundryModel AI21Jamba15Large; - [AspireValue("FoundryModels")] - public static readonly FoundryModel AI21Jamba15Mini; - } - public static partial class Anthropic { [AspireValue("FoundryModels")] @@ -484,12 +478,8 @@ public static partial class Cohere [AspireValue("FoundryModels")] public static readonly FoundryModel CohereCommandA; [AspireValue("FoundryModels")] - public static readonly FoundryModel CohereCommandR; - [AspireValue("FoundryModels")] public static readonly FoundryModel CohereCommandR082024; [AspireValue("FoundryModels")] - public static readonly FoundryModel CohereCommandRPlus; - [AspireValue("FoundryModels")] public static readonly FoundryModel CohereCommandRPlus082024; [AspireValue("FoundryModels")] public static readonly FoundryModel CohereEmbedV3English; @@ -521,11 +511,13 @@ public static partial class DeepSeek [AspireValue("FoundryModels")] public static readonly FoundryModel DeepSeekV30324; [AspireValue("FoundryModels")] - public static readonly FoundryModel DeepSeekV31; - [AspireValue("FoundryModels")] public static readonly FoundryModel DeepSeekV32; [AspireValue("FoundryModels")] public static readonly FoundryModel DeepSeekV32Speciale; + [AspireValue("FoundryModels")] + public static readonly FoundryModel DeepSeekV4Flash; + [AspireValue("FoundryModels")] + public static readonly FoundryModel DeepSeekV4Pro; } public static partial class Local @@ -541,8 +533,14 @@ public static partial class Local [AspireValue("FoundryModels")] public static readonly FoundryModel Mistral7bV02; [AspireValue("FoundryModels")] + public static readonly FoundryModel MistralNemo12bInstruct; + [AspireValue("FoundryModels")] public static readonly FoundryModel NemotronSpeechStreamingEn06b; [AspireValue("FoundryModels")] + public static readonly FoundryModel NemotronSpeechStreamingEs06b; + [AspireValue("FoundryModels")] + public static readonly FoundryModel Olmo37bInstruct; + [AspireValue("FoundryModels")] public static readonly FoundryModel Phi35Mini; [AspireValue("FoundryModels")] public static readonly FoundryModel Phi3Mini128k; @@ -586,6 +584,8 @@ public static partial class Local [AspireValue("FoundryModels")] public static readonly FoundryModel Qwen352b; [AspireValue("FoundryModels")] + public static readonly FoundryModel Qwen352bText; + [AspireValue("FoundryModels")] public static readonly FoundryModel Qwen354b; [AspireValue("FoundryModels")] public static readonly FoundryModel Qwen359b; @@ -601,6 +601,8 @@ public static partial class Local public static readonly FoundryModel Qwen3Vl4bInstruct; [AspireValue("FoundryModels")] public static readonly FoundryModel Qwen3Vl8bInstruct; + [AspireValue("FoundryModels")] + public static readonly FoundryModel Smollm33b; } public static partial class Meta @@ -610,21 +612,13 @@ public static partial class Meta [AspireValue("FoundryModels")] public static readonly FoundryModel Llama3290BVisionInstruct; [AspireValue("FoundryModels")] - public static readonly FoundryModel Llama3370BInstruct; - [AspireValue("FoundryModels")] public static readonly FoundryModel Llama4Maverick17B128EInstructFP8; [AspireValue("FoundryModels")] public static readonly FoundryModel Llama4Scout17B16EInstruct; [AspireValue("FoundryModels")] public static readonly FoundryModel MetaLlama31405BInstruct; [AspireValue("FoundryModels")] - public static readonly FoundryModel MetaLlama3170BInstruct; - [AspireValue("FoundryModels")] public static readonly FoundryModel MetaLlama318BInstruct; - [AspireValue("FoundryModels")] - public static readonly FoundryModel MetaLlama370BInstruct; - [AspireValue("FoundryModels")] - public static readonly FoundryModel MetaLlama38BInstruct; } public static partial class Microsoft @@ -650,8 +644,12 @@ public static partial class Microsoft [AspireValue("FoundryModels")] public static readonly FoundryModel AzureLanguageConversationalPiiRedaction; [AspireValue("FoundryModels")] + public static readonly FoundryModel AzureLanguageDocumentPiiRedaction; + [AspireValue("FoundryModels")] public static readonly FoundryModel AzureLanguageLanguageDetection; [AspireValue("FoundryModels")] + public static readonly FoundryModel AzureLanguageTextAnalyticsForHealth; + [AspireValue("FoundryModels")] public static readonly FoundryModel AzureLanguageTextPiiRedaction; [AspireValue("FoundryModels")] public static readonly FoundryModel AzureSpeechSpeechToText; @@ -697,6 +695,8 @@ public static partial class Microsoft [AspireValue("FoundryModels")] public static readonly FoundryModel Phi3Small8kInstruct; [AspireValue("FoundryModels")] + public static readonly FoundryModel Phi3Vision128kInstruct; + [AspireValue("FoundryModels")] public static readonly FoundryModel Phi4; [AspireValue("FoundryModels")] public static readonly FoundryModel Phi4MiniInstruct; @@ -722,18 +722,10 @@ public static partial class MistralAI [AspireValue("FoundryModels")] public static readonly FoundryModel MistralDocumentAi2512; [AspireValue("FoundryModels")] - public static readonly FoundryModel MistralLarge2407; - [AspireValue("FoundryModels")] - public static readonly FoundryModel MistralLarge2411; - [AspireValue("FoundryModels")] public static readonly FoundryModel MistralLarge3; [AspireValue("FoundryModels")] public static readonly FoundryModel MistralMedium2505; [AspireValue("FoundryModels")] - public static readonly FoundryModel MistralNemo; - [AspireValue("FoundryModels")] - public static readonly FoundryModel MistralSmall; - [AspireValue("FoundryModels")] public static readonly FoundryModel MistralSmall2503; } @@ -834,6 +826,8 @@ public static partial class OpenAI [AspireValue("FoundryModels")] public static readonly FoundryModel GptAudioMini; [AspireValue("FoundryModels")] + public static readonly FoundryModel GptChatLatest; + [AspireValue("FoundryModels")] public static readonly FoundryModel GptImage1; [AspireValue("FoundryModels")] public static readonly FoundryModel GptImage15; @@ -844,14 +838,18 @@ public static partial class OpenAI [AspireValue("FoundryModels")] public static readonly FoundryModel GptOss120b; [AspireValue("FoundryModels")] - public static readonly FoundryModel GptOss20b; - [AspireValue("FoundryModels")] public static readonly FoundryModel GptRealtime; [AspireValue("FoundryModels")] public static readonly FoundryModel GptRealtime15; [AspireValue("FoundryModels")] + public static readonly FoundryModel GptRealtime2; + [AspireValue("FoundryModels")] public static readonly FoundryModel GptRealtimeMini; [AspireValue("FoundryModels")] + public static readonly FoundryModel GptRealtimeTranslate; + [AspireValue("FoundryModels")] + public static readonly FoundryModel GptRealtimeWhisper; + [AspireValue("FoundryModels")] public static readonly FoundryModel O1; [AspireValue("FoundryModels")] public static readonly FoundryModel O1Mini; @@ -895,10 +893,6 @@ public static partial class StabilityAI public static partial class XAI { - [AspireValue("FoundryModels")] - public static readonly FoundryModel Grok3; - [AspireValue("FoundryModels")] - public static readonly FoundryModel Grok3Mini; [AspireValue("FoundryModels")] public static readonly FoundryModel Grok4; [AspireValue("FoundryModels")] @@ -910,6 +904,8 @@ public static partial class XAI [AspireValue("FoundryModels")] public static readonly FoundryModel Grok420Reasoning; [AspireValue("FoundryModels")] + public static readonly FoundryModel Grok43; + [AspireValue("FoundryModels")] public static readonly FoundryModel Grok4FastNonReasoning; [AspireValue("FoundryModels")] public static readonly FoundryModel Grok4FastReasoning; @@ -1011,8 +1007,6 @@ public HostedAgentConfiguration(string image) { } [AspireExportIgnore(Reason = "Azure SDK-specific type not usable from polyglot hosts.")] public System.Collections.Generic.IList Tools { get { throw null; } init { } } - - public global::Azure.AI.Projects.Agents.ProjectsAgentVersionCreationOptions ToProjectsAgentVersionCreationOptions() { throw null; } } public partial interface IFoundryTool diff --git a/src/Aspire.Hosting.Garnet/api/Aspire.Hosting.Garnet.cs b/src/Aspire.Hosting.Garnet/api/Aspire.Hosting.Garnet.cs index e3c252d060a..589cdf1e6f2 100644 --- a/src/Aspire.Hosting.Garnet/api/Aspire.Hosting.Garnet.cs +++ b/src/Aspire.Hosting.Garnet/api/Aspire.Hosting.Garnet.cs @@ -16,16 +16,16 @@ public static partial class GarnetBuilderExtensions [AspireExportIgnore(Reason = "Use the dedicated polyglot overload instead.")] public static ApplicationModel.IResourceBuilder AddGarnet(this IDistributedApplicationBuilder builder, string name, int? port) { throw null; } - [AspireExport(Description = "Mounts a host directory as the Garnet data directory.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a persistent data volume to the Garnet resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use the overload without the keysChangedThreshold parameter.")] public static ApplicationModel.IResourceBuilder WithPersistence(this ApplicationModel.IResourceBuilder builder, System.TimeSpan? interval, long keysChangedThreshold) { throw null; } - [AspireExport(Description = "Configures snapshot persistence for the Garnet resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPersistence(this ApplicationModel.IResourceBuilder builder, System.TimeSpan? interval = null) { throw null; } } } diff --git a/src/Aspire.Hosting.GitHub.Models/api/Aspire.Hosting.GitHub.Models.cs b/src/Aspire.Hosting.GitHub.Models/api/Aspire.Hosting.GitHub.Models.cs index 8cf0abde572..b0719d64bf2 100644 --- a/src/Aspire.Hosting.GitHub.Models/api/Aspire.Hosting.GitHub.Models.cs +++ b/src/Aspire.Hosting.GitHub.Models/api/Aspire.Hosting.GitHub.Models.cs @@ -16,10 +16,10 @@ public static partial class GitHubModelsExtensions [AspireExportIgnore(Reason = "The polyglot overload uses the GitHubModelName enum instead. See the internal AddGitHubModel(GitHubModelName) overload.")] public static ApplicationModel.IResourceBuilder AddGitHubModel(this IDistributedApplicationBuilder builder, string name, string model, ApplicationModel.IResourceBuilder? organization = null) { throw null; } - [AspireExport(Description = "Configures the API key for the GitHub Model resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithApiKey(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder apiKey) { throw null; } - [AspireExport("enableHealthCheck", MethodName = "withHealthCheck", Description = "Adds a health check for the GitHub Model resource.")] + [AspireExport("enableHealthCheck")] public static ApplicationModel.IResourceBuilder WithHealthCheck(this ApplicationModel.IResourceBuilder builder) { throw null; } } } diff --git a/src/Aspire.Hosting.Go/api/Aspire.Hosting.Go.cs b/src/Aspire.Hosting.Go/api/Aspire.Hosting.Go.cs new file mode 100644 index 00000000000..3866b33624f --- /dev/null +++ b/src/Aspire.Hosting.Go/api/Aspire.Hosting.Go.cs @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Aspire.Hosting +{ + public static partial class GoHostingExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddGoApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string packagePath = ".", string[]? buildTags = null, string? ldFlags = null, string? gcFlags = null, bool raceDetector = false) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithAppArgs(this ApplicationModel.IResourceBuilder builder, params object[] args) + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithDelveServer(this ApplicationModel.IResourceBuilder builder, int port = 2345) + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithGoPrivate(this ApplicationModel.IResourceBuilder builder, string[] privatePatterns, string authHost, string usernameArgName = "GIT_USER", string tokenSecretId = "gittoken") + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithModDownload(this ApplicationModel.IResourceBuilder builder) + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithModTidy(this ApplicationModel.IResourceBuilder builder) + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithModVendor(this ApplicationModel.IResourceBuilder builder) + where T : Go.GoAppResource { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithVetTool(this ApplicationModel.IResourceBuilder builder) + where T : Go.GoAppResource { throw null; } + } +} + +namespace Aspire.Hosting.Go +{ + [AspireExport(ExposeProperties = true)] + public partial class GoAppResource : ApplicationModel.ExecutableResource, IResourceWithServiceDiscovery, ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResource, ApplicationModel.IContainerFilesDestinationResource + { + public GoAppResource(string name, string workingDirectory) : base(default!, default!, default!) { } + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting.JavaScript/BunAppResource.cs b/src/Aspire.Hosting.JavaScript/BunAppResource.cs index 1c553fea982..866a751efb8 100644 --- a/src/Aspire.Hosting.JavaScript/BunAppResource.cs +++ b/src/Aspire.Hosting.JavaScript/BunAppResource.cs @@ -12,5 +12,5 @@ namespace Aspire.Hosting.JavaScript; /// The command to execute. /// The working directory to use for the command. [AspireExport(ExposeProperties = true)] -public class BunAppResource(string name, string command, string workingDirectory) +public sealed class BunAppResource(string name, string command, string workingDirectory) : JavaScriptAppResource(name, command, workingDirectory), IResourceWithServiceDiscovery, IContainerFilesDestinationResource; diff --git a/src/Aspire.Hosting.JavaScript/CompatibilitySuppressions.xml b/src/Aspire.Hosting.JavaScript/CompatibilitySuppressions.xml new file mode 100644 index 00000000000..bfacb9bb9b6 --- /dev/null +++ b/src/Aspire.Hosting.JavaScript/CompatibilitySuppressions.xml @@ -0,0 +1,11 @@ + + + + + CP0002 + M:Aspire.Hosting.JavaScriptHostingExtensions.PublishAsNpmScript``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.String,System.String) + lib/net8.0/Aspire.Hosting.JavaScript.dll + lib/net8.0/Aspire.Hosting.JavaScript.dll + true + + \ No newline at end of file diff --git a/src/Aspire.Hosting.JavaScript/api/Aspire.Hosting.JavaScript.cs b/src/Aspire.Hosting.JavaScript/api/Aspire.Hosting.JavaScript.cs index b966c771f94..62369ff88ca 100644 --- a/src/Aspire.Hosting.JavaScript/api/Aspire.Hosting.JavaScript.cs +++ b/src/Aspire.Hosting.JavaScript/api/Aspire.Hosting.JavaScript.cs @@ -10,31 +10,34 @@ namespace Aspire.Hosting { public static partial class JavaScriptHostingExtensions { - [AspireExport(Description = "Adds a JavaScript application resource")] + [AspireExport] + public static ApplicationModel.IResourceBuilder AddBunApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath) { throw null; } + + [AspireExport] public static ApplicationModel.IResourceBuilder AddJavaScriptApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string runScriptName = "dev") { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Adds a Next.js application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNextJsApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string runScriptName = "dev") { throw null; } - [AspireExport(Description = "Adds a Node.js application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNodeApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath) { throw null; } - [AspireExport(Description = "Adds a Vite application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddViteApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string runScriptName = "dev") { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Disables deploy-time build validation checks for the Next.js application.")] + [AspireExport] public static ApplicationModel.IResourceBuilder DisableBuildValidation(this ApplicationModel.IResourceBuilder builder) { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Publishes the JavaScript application as a standalone Node.js server that runs a built artifact directly.")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsNodeServer(this ApplicationModel.IResourceBuilder builder, string entryPoint, string outputPath = ".") where TResource : JavaScript.JavaScriptAppResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Publishes the JavaScript application as a Node.js server that uses a package manager script at runtime.")] - public static ApplicationModel.IResourceBuilder PublishAsNpmScript(this ApplicationModel.IResourceBuilder builder, string startScriptName = "start", string? runScriptArguments = null) + [AspireExport] + public static ApplicationModel.IResourceBuilder PublishAsPackageScript(this ApplicationModel.IResourceBuilder builder, string scriptName = "start", string? runScriptArguments = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREJAVASCRIPT001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] @@ -48,34 +51,34 @@ public static ApplicationModel.IResourceBuilder PublishAsStaticWebsit where TResource : JavaScript.JavaScriptAppResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Configures a browser debugger for the JavaScript application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBrowserDebugger(this ApplicationModel.IResourceBuilder builder, string browser = "msedge") where T : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Specifies an npm script to run before starting the application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBuildScript(this ApplicationModel.IResourceBuilder resource, string scriptName, string[]? args = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Configures Bun as the package manager")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBun(this ApplicationModel.IResourceBuilder resource, bool install = true, string[]? installArgs = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Configures npm as the package manager")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithNpm(this ApplicationModel.IResourceBuilder resource, bool install = true, string? installCommand = null, string[]? installArgs = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Configures pnpm as the package manager")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPnpm(this ApplicationModel.IResourceBuilder resource, bool install = true, string[]? installArgs = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Specifies an npm script to run during development")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithRunScript(this ApplicationModel.IResourceBuilder resource, string scriptName, string[]? args = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } - [AspireExport(Description = "Configures a custom Vite configuration file")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithViteConfig(this ApplicationModel.IResourceBuilder builder, string configPath) { throw null; } - [AspireExport(Description = "Configures yarn as the package manager")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithYarn(this ApplicationModel.IResourceBuilder resource, bool install = true, string[]? installArgs = null) where TResource : JavaScript.JavaScriptAppResource { throw null; } } @@ -83,6 +86,12 @@ public static ApplicationModel.IResourceBuilder WithYarn(t namespace Aspire.Hosting.JavaScript { + [AspireExport(ExposeProperties = true)] + public sealed partial class BunAppResource : JavaScriptAppResource, IResourceWithServiceDiscovery, ApplicationModel.IResourceWithEndpoints, ApplicationModel.IResource, ApplicationModel.IContainerFilesDestinationResource + { + public BunAppResource(string name, string command, string workingDirectory) : base(default!, default!, default!) { } + } + public sealed partial record CopyFilePattern(string Source, string Destination) { } diff --git a/src/Aspire.Hosting.Kafka/api/Aspire.Hosting.Kafka.cs b/src/Aspire.Hosting.Kafka/api/Aspire.Hosting.Kafka.cs index cc304a35ae2..cb56ab59ee3 100644 --- a/src/Aspire.Hosting.Kafka/api/Aspire.Hosting.Kafka.cs +++ b/src/Aspire.Hosting.Kafka/api/Aspire.Hosting.Kafka.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class KafkaBuilderExtensions { - [AspireExport(Description = "Adds a Kafka container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddKafka(this IDistributedApplicationBuilder builder, string name, int? port = null) { throw null; } - [AspireExport(Description = "Adds a data bind mount to the Kafka container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume to the Kafka container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Sets the host port for the Kafka UI container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Adds a Kafka UI container to manage the Kafka resource", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithKafkaUI(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } } diff --git a/src/Aspire.Hosting.Keycloak/api/Aspire.Hosting.Keycloak.cs b/src/Aspire.Hosting.Keycloak/api/Aspire.Hosting.Keycloak.cs index 47a8b8e7fd4..ff5e58de580 100644 --- a/src/Aspire.Hosting.Keycloak/api/Aspire.Hosting.Keycloak.cs +++ b/src/Aspire.Hosting.Keycloak/api/Aspire.Hosting.Keycloak.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class KeycloakResourceBuilderExtensions { - [AspireExport(Description = "Adds a Keycloak container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddKeycloak(this IDistributedApplicationBuilder builder, string name, int? port = null, ApplicationModel.IResourceBuilder? adminUsername = null, ApplicationModel.IResourceBuilder? adminPassword = null) { throw null; } - [AspireExport(Description = "Adds a data bind mount for Keycloak")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Adds a data volume for Keycloak")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - [AspireExport(Description = "Disables Keycloak features")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDisabledFeatures(this ApplicationModel.IResourceBuilder builder, params string[] features) { throw null; } - [AspireExport(Description = "Enables Keycloak features")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithEnabledFeatures(this ApplicationModel.IResourceBuilder builder, params string[] features) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withOtlpExporter dispatcher export.")] diff --git a/src/Aspire.Hosting.Kubernetes/Aspire.Hosting.Kubernetes.csproj b/src/Aspire.Hosting.Kubernetes/Aspire.Hosting.Kubernetes.csproj index 04e87db475a..a7992c280ff 100644 --- a/src/Aspire.Hosting.Kubernetes/Aspire.Hosting.Kubernetes.csproj +++ b/src/Aspire.Hosting.Kubernetes/Aspire.Hosting.Kubernetes.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesHelmChartResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesHelmChartResource.cs index 3580a361116..2c31a784a40 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesHelmChartResource.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesHelmChartResource.cs @@ -41,7 +41,7 @@ namespace Aspire.Hosting.Kubernetes; /// /// [AspireExport] -public class KubernetesHelmChartResource : Resource, IResourceWithParent +public sealed class KubernetesHelmChartResource : Resource, IResourceWithParent { /// /// Initializes a new instance of . diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs b/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs index 66921ab26ad..6f75bf8acfb 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesResource.cs @@ -486,6 +486,16 @@ private async Task ProcessValueAsync(KubernetesEnvironmentContext contex if (value is EndpointReference ep) { + // The referenced endpoint may belong to a resource deployed to a different compute + // environment (for example a Foundry hosted agent). In that case delegate to the owning + // compute environment instead of looking it up in this environment's local endpoint map. + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + ep, [kubernetesEnvironmentResource, kubernetesEnvironmentResource.OwningComputeEnvironment], out var crossExpr)) + { + value = crossExpr; + continue; + } + var referencedResource = ep.Resource == this ? this : await context.CreateKubernetesResourceAsync(ep.Resource, executionContext, default).ConfigureAwait(false); @@ -516,6 +526,13 @@ private async Task ProcessValueAsync(KubernetesEnvironmentContext contex if (value is EndpointReferenceExpression epExpr) { + if (ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + epExpr, [kubernetesEnvironmentResource, kubernetesEnvironmentResource.OwningComputeEnvironment], out var crossExpr)) + { + value = crossExpr; + continue; + } + var referencedResource = epExpr.Endpoint.Resource == this ? this : await context.CreateKubernetesResourceAsync(epExpr.Endpoint.Resource, executionContext, default).ConfigureAwait(false); diff --git a/src/Aspire.Hosting.Kubernetes/api/Aspire.Hosting.Kubernetes.cs b/src/Aspire.Hosting.Kubernetes/api/Aspire.Hosting.Kubernetes.cs index 6390ad8e59d..47c4e43d0ca 100644 --- a/src/Aspire.Hosting.Kubernetes/api/Aspire.Hosting.Kubernetes.cs +++ b/src/Aspire.Hosting.Kubernetes/api/Aspire.Hosting.Kubernetes.cs @@ -8,127 +8,181 @@ //------------------------------------------------------------------------------ namespace Aspire.Hosting { + public static partial class CertManagerExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddCertManager(this ApplicationModel.IResourceBuilder builder, string name, string? chartVersion = null) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder AddIssuer(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } + + [AspireExport("withAcmeServerParam")] + public static ApplicationModel.IResourceBuilder WithAcmeServer(this ApplicationModel.IResourceBuilder builder, string serverUrl, ApplicationModel.IResourceBuilder email) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithAcmeServer(this ApplicationModel.IResourceBuilder builder, string serverUrl, string email) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithHttp01Solver(this ApplicationModel.IResourceBuilder builder) { throw null; } + + [AspireExport("withLetsEncryptProductionParam")] + public static ApplicationModel.IResourceBuilder WithLetsEncryptProduction(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder email) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithLetsEncryptProduction(this ApplicationModel.IResourceBuilder builder, string email) { throw null; } + + [AspireExport("withLetsEncryptStagingParam")] + public static ApplicationModel.IResourceBuilder WithLetsEncryptStaging(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder email) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithLetsEncryptStaging(this ApplicationModel.IResourceBuilder builder, string email) { throw null; } + + [AspireExport("withGatewayTlsIssuer")] + public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder issuer) { throw null; } + } + public static partial class KubernetesAspireDashboardResourceBuilderExtensions { - [AspireExport(Description = "Enables or disables forwarded headers support for the Aspire dashboard")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithForwardedHeaders(this ApplicationModel.IResourceBuilder builder, bool enabled = true) { throw null; } - [AspireExport(Description = "Sets the Kubernetes Service ports for the OTLP endpoints")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithOtlpServicePort(this ApplicationModel.IResourceBuilder builder, int? grpcPort = null, int? httpPort = null) { throw null; } - [AspireExport(Description = "Sets the Kubernetes Service port for the Aspire dashboard")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithServicePort(this ApplicationModel.IResourceBuilder builder, int? port = null) { throw null; } } public static partial class KubernetesEnvironmentExtensions { - [AspireExport(Description = "Adds a Kubernetes publishing environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddKubernetesEnvironment(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Adds a named node pool to a Kubernetes environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddNodePool(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport("configureDashboard", MethodName = "configureDashboard", Description = "Configures the Aspire dashboard resource for the Kubernetes environment", RunSyncOnBackgroundThread = true)] + [AspireExport("configureDashboard", MethodName = "configureDashboard", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, System.Action> configure) { throw null; } - [AspireExport(Description = "Enables or disables the Aspire dashboard for the Kubernetes environment")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDashboard(this ApplicationModel.IResourceBuilder builder, bool enabled = true) { throw null; } - [AspireExport(Description = "Configures Helm chart deployment settings", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithHelm(this ApplicationModel.IResourceBuilder builder, System.Action? configure = null) { throw null; } - [AspireExport("withKubernetesNodePool", MethodName = "withNodePool", Description = "Schedules a workload on a specific Kubernetes node pool")] + [AspireExport("withKubernetesNodePool", MethodName = "withNodePool")] public static ApplicationModel.IResourceBuilder WithNodePool(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder nodePool) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Configures properties of a Kubernetes environment", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithProperties(this ApplicationModel.IResourceBuilder builder, System.Action configure) { throw null; } } public static partial class KubernetesGatewayExtensions { - [AspireExport(Description = "Adds a Kubernetes Gateway API Gateway resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddGateway(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport("withGatewayAnnotationParam", Description = "Adds a parameterized Kubernetes metadata annotation to a Gateway")] + [AspireExport("withGatewayAnnotationParam")] public static ApplicationModel.IResourceBuilder WithGatewayAnnotation(this ApplicationModel.IResourceBuilder builder, string key, ApplicationModel.IResourceBuilder value) { throw null; } - [AspireExport(Description = "Adds a Kubernetes metadata annotation to a Gateway")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithGatewayAnnotation(this ApplicationModel.IResourceBuilder builder, string key, string value) { throw null; } - [AspireExport("withGatewayClassParam", Description = "Sets a parameterized GatewayClass for a Kubernetes Gateway")] + [AspireExport("withGatewayClassParam")] public static ApplicationModel.IResourceBuilder WithGatewayClass(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder className) { throw null; } - [AspireExport(Description = "Sets the GatewayClass for a Kubernetes Gateway")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithGatewayClass(this ApplicationModel.IResourceBuilder builder, string className) { throw null; } - [AspireExport("withGatewayHostnameParam", Description = "Adds a parameterized hostname to a Kubernetes Gateway")] + [AspireExport("withGatewayHostnameParam")] public static ApplicationModel.IResourceBuilder WithHostname(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder hostname) { throw null; } - [AspireExport("withGatewayHostname", MethodName = "withHostname", Description = "Adds a hostname to a Kubernetes Gateway")] + [AspireExport("withGatewayHostname", MethodName = "withHostname")] public static ApplicationModel.IResourceBuilder WithHostname(this ApplicationModel.IResourceBuilder builder, string hostname) { throw null; } - [AspireExport("withGatewayPathRoute", Description = "Adds a path-based route to a Kubernetes Gateway")] - public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } + [AspireExport("withGatewayPathRoute")] + public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.GatewayPathMatchType pathType = Kubernetes.GatewayPathMatchType.PathPrefix) { throw null; } - [AspireExport("withGatewayHostRoute", Description = "Adds a host-and-path route to a Kubernetes Gateway")] - public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string host, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } + [AspireExport("withGatewayHostRoute")] + public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string host, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.GatewayPathMatchType pathType = Kubernetes.GatewayPathMatchType.PathPrefix) { throw null; } - [AspireExport("withGatewayTlsParam", Description = "Configures TLS on a Kubernetes Gateway with a parameterized secret")] + [AspireExport("withGatewayTlsParam")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder secretName) { throw null; } - [AspireExport("withGatewayTls", MethodName = "withTls", Description = "Configures TLS on a Kubernetes Gateway listener")] + [AspireExport("withGatewayTls", MethodName = "withTls")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder, string secretName) { throw null; } - [AspireExport("withGatewayTlsAuto", Description = "Configures TLS on a Kubernetes Gateway with an auto-generated secret")] + [AspireExport("withGatewayTlsAuto")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder) { throw null; } } + public static partial class KubernetesHelmChartExtensions + { + [AspireExport] + public static ApplicationModel.IResourceBuilder AddHelmChart(this ApplicationModel.IResourceBuilder builder, string name, string chartReference, string chartVersion) { throw null; } + + [AspireExport("withHelmChartDestroy")] + public static ApplicationModel.IResourceBuilder WithDestroy(this ApplicationModel.IResourceBuilder builder) { throw null; } + + [AspireExport("withHelmChartForceConflicts")] + public static ApplicationModel.IResourceBuilder WithForceConflicts(this ApplicationModel.IResourceBuilder builder) { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithHelmValue(this ApplicationModel.IResourceBuilder builder, string key, string value) { throw null; } + + [AspireExport("withHelmChartNamespace")] + public static ApplicationModel.IResourceBuilder WithNamespace(this ApplicationModel.IResourceBuilder builder, string @namespace) { throw null; } + + [AspireExport("withHelmChartReleaseName")] + public static ApplicationModel.IResourceBuilder WithReleaseName(this ApplicationModel.IResourceBuilder builder, string releaseName) { throw null; } + } + public static partial class KubernetesIngressExtensions { - [AspireExport(Description = "Adds a Kubernetes Ingress resource for HTTP routing")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddIngress(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } - [AspireExport(Description = "Sets the default backend for a Kubernetes Ingress")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDefaultBackend(this ApplicationModel.IResourceBuilder builder, ApplicationModel.EndpointReference endpoint) { throw null; } - [AspireExport("withIngressHostnameParam", Description = "Adds a parameterized hostname to a Kubernetes Ingress")] + [AspireExport("withIngressHostnameParam")] public static ApplicationModel.IResourceBuilder WithHostname(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder hostname) { throw null; } - [AspireExport("withIngressHostname", MethodName = "withHostname", Description = "Adds a hostname to a Kubernetes Ingress")] + [AspireExport("withIngressHostname", MethodName = "withHostname")] public static ApplicationModel.IResourceBuilder WithHostname(this ApplicationModel.IResourceBuilder builder, string hostname) { throw null; } - [AspireExport("withIngressAnnotationParam", Description = "Adds a parameterized Kubernetes metadata annotation to an Ingress")] + [AspireExport("withIngressAnnotationParam")] public static ApplicationModel.IResourceBuilder WithIngressAnnotation(this ApplicationModel.IResourceBuilder builder, string key, ApplicationModel.IResourceBuilder value) { throw null; } - [AspireExport(Description = "Adds a Kubernetes metadata annotation to a Kubernetes Ingress")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithIngressAnnotation(this ApplicationModel.IResourceBuilder builder, string key, string value) { throw null; } - [AspireExport("withIngressClassParam", Description = "Sets a parameterized ingress class for a Kubernetes Ingress")] + [AspireExport("withIngressClassParam")] public static ApplicationModel.IResourceBuilder WithIngressClass(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder className) { throw null; } - [AspireExport(Description = "Sets the ingress class for a Kubernetes Ingress")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithIngressClass(this ApplicationModel.IResourceBuilder builder, string className) { throw null; } - [AspireExport("withIngressPathRoute", Description = "Adds a path-based route to a Kubernetes Ingress")] - public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } + [AspireExport("withIngressPath")] + public static ApplicationModel.IResourceBuilder WithPath(this ApplicationModel.IResourceBuilder builder, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } - [AspireExport("withIngressHostRoute", Description = "Adds a host-and-path route to a Kubernetes Ingress")] - public static ApplicationModel.IResourceBuilder WithRoute(this ApplicationModel.IResourceBuilder builder, string host, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } + [AspireExport("withIngressHostAndPath")] + public static ApplicationModel.IResourceBuilder WithPath(this ApplicationModel.IResourceBuilder builder, string host, string path, ApplicationModel.EndpointReference endpoint, Kubernetes.IngressPathType pathType = Kubernetes.IngressPathType.Prefix) { throw null; } - [AspireExport("withIngressTlsParam", Description = "Configures TLS for a Kubernetes Ingress with a parameterized secret")] + [AspireExport("withIngressTlsParam")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder secretName) { throw null; } - [AspireExport("withIngressTls", MethodName = "withTls", Description = "Configures TLS for a Kubernetes Ingress using a K8S secret")] + [AspireExport("withIngressTls", MethodName = "withTls")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder, string secretName) { throw null; } - [AspireExport("withIngressTlsAuto", Description = "Configures TLS for a Kubernetes Ingress with an auto-generated secret")] + [AspireExport("withIngressTlsAuto")] public static ApplicationModel.IResourceBuilder WithTls(this ApplicationModel.IResourceBuilder builder) { throw null; } } public static partial class KubernetesServiceExtensions { - [AspireExport(Description = "Publishes the resource as a Kubernetes service")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsKubernetesService(this ApplicationModel.IResourceBuilder builder, System.Action configure) where T : ApplicationModel.IComputeResource { throw null; } } @@ -136,6 +190,31 @@ public static ApplicationModel.IResourceBuilder PublishAsKubernetesService namespace Aspire.Hosting.Kubernetes { + [AspireExport] + public sealed partial class CertManagerIssuerResource : ApplicationModel.Resource, ApplicationModel.IResourceWithParent, ApplicationModel.IResourceWithParent, ApplicationModel.IResource + { + public CertManagerIssuerResource(string name, CertManagerResource parent) : base(default!) { } + + public CertManagerResource Parent { get { throw null; } } + } + + [AspireExport] + public sealed partial class CertManagerResource : ApplicationModel.Resource, ApplicationModel.IResourceWithParent, ApplicationModel.IResourceWithParent, ApplicationModel.IResource + { + public CertManagerResource(string name, KubernetesEnvironmentResource environment, KubernetesHelmChartResource helmChart) : base(default!) { } + + public KubernetesHelmChartResource HelmChart { get { throw null; } } + + public KubernetesEnvironmentResource Parent { get { throw null; } } + } + + public enum GatewayPathMatchType + { + PathPrefix = 0, + Exact = 1, + RegularExpression = 2 + } + public sealed partial class HelmChartDescriptionAnnotation : ApplicationModel.IResourceAnnotation { public HelmChartDescriptionAnnotation(ApplicationModel.ReferenceExpression description) { } @@ -258,6 +337,22 @@ public KubernetesGatewayResource(string name, KubernetesEnvironmentResource envi public KubernetesEnvironmentResource Parent { get { throw null; } } } + [AspireExport] + public sealed partial class KubernetesHelmChartResource : ApplicationModel.Resource, ApplicationModel.IResourceWithParent, ApplicationModel.IResourceWithParent, ApplicationModel.IResource + { + public KubernetesHelmChartResource(string name, KubernetesEnvironmentResource environment, string chartReference, string chartVersion) : base(default!) { } + + public string ChartReference { get { throw null; } } + + public string ChartVersion { get { throw null; } } + + public string? Namespace { get { throw null; } set { } } + + public KubernetesEnvironmentResource Parent { get { throw null; } } + + public string? ReleaseName { get { throw null; } set { } } + } + [AspireExport] public partial class KubernetesIngressResource : ApplicationModel.Resource, ApplicationModel.IResourceWithParent, ApplicationModel.IResourceWithParent, ApplicationModel.IResource { @@ -290,6 +385,7 @@ public partial class KubernetesResource : ApplicationModel.Resource, Application { public KubernetesResource(string name, ApplicationModel.IResource resource, KubernetesEnvironmentResource kubernetesEnvironmentResource) : base(default!) { } + [AspireExportIgnore(Reason = "Kubernetes manifest resource types are C#-only customization objects and are not part of the polyglot SDK surface.")] public System.Collections.Generic.List AdditionalResources { get { throw null; } } public Resources.ConfigMap? ConfigMap { get { throw null; } set { } } diff --git a/src/Aspire.Hosting.Maui/api/Aspire.Hosting.Maui.cs b/src/Aspire.Hosting.Maui/api/Aspire.Hosting.Maui.cs index 4cd29b2cbb1..589b6a572fb 100644 --- a/src/Aspire.Hosting.Maui/api/Aspire.Hosting.Maui.cs +++ b/src/Aspire.Hosting.Maui/api/Aspire.Hosting.Maui.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting { public static partial class MauiAndroidExtensions { - [AspireExport(Description = "Adds an Android device resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAndroidDevice(this ApplicationModel.IResourceBuilder builder, string name, string? deviceId = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with the optional device ID parameter instead.")] @@ -19,7 +19,7 @@ public static partial class MauiAndroidExtensions [AspireExportIgnore(Reason = "Convenience overload. Use the overload with name and optional device ID instead.")] public static ApplicationModel.IResourceBuilder AddAndroidDevice(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Adds an Android emulator resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddAndroidEmulator(this ApplicationModel.IResourceBuilder builder, string name, string? emulatorId = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with the optional emulator ID parameter instead.")] @@ -31,7 +31,7 @@ public static partial class MauiAndroidExtensions public static partial class MauiiOSExtensions { - [AspireExport(Description = "Adds an iOS device resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddiOSDevice(this ApplicationModel.IResourceBuilder builder, string name, string? deviceId = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with the optional device UDID parameter instead.")] @@ -40,7 +40,7 @@ public static partial class MauiiOSExtensions [AspireExportIgnore(Reason = "Convenience overload. Use the overload with name and optional device UDID instead.")] public static ApplicationModel.IResourceBuilder AddiOSDevice(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport(Description = "Adds an iOS simulator resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddiOSSimulator(this ApplicationModel.IResourceBuilder builder, string name, string? simulatorId = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with the optional simulator UDID parameter instead.")] @@ -52,7 +52,7 @@ public static partial class MauiiOSExtensions public static partial class MauiMacCatalystExtensions { - [AspireExport(Description = "Adds a Mac Catalyst platform resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddMacCatalystDevice(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the named overload instead.")] @@ -61,20 +61,20 @@ public static partial class MauiMacCatalystExtensions public static partial class MauiOtlpExtensions { - [AspireExport(Description = "Configures a .NET MAUI platform resource to send OpenTelemetry data through a development tunnel.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithOtlpDevTunnel(this ApplicationModel.IResourceBuilder builder) where T : Maui.IMauiPlatformResource, ApplicationModel.IResourceWithEnvironment { throw null; } } public static partial class MauiProjectExtensions { - [AspireExport(Description = "Adds a .NET MAUI project to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddMauiProject(this IDistributedApplicationBuilder builder, string name, string projectPath) { throw null; } } public static partial class MauiWindowsExtensions { - [AspireExport(Description = "Adds a Windows platform resource for a .NET MAUI project.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddWindowsDevice(this ApplicationModel.IResourceBuilder builder, string name) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the named overload instead.")] diff --git a/src/Aspire.Hosting.Milvus/api/Aspire.Hosting.Milvus.cs b/src/Aspire.Hosting.Milvus/api/Aspire.Hosting.Milvus.cs index 0e7d095df08..87c907eb063 100644 --- a/src/Aspire.Hosting.Milvus/api/Aspire.Hosting.Milvus.cs +++ b/src/Aspire.Hosting.Milvus/api/Aspire.Hosting.Milvus.cs @@ -10,26 +10,26 @@ namespace Aspire.Hosting { public static partial class MilvusBuilderExtensions { - [AspireExport(Description = "Adds a Milvus database resource to a Milvus server resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds a Milvus server resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddMilvus(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? apiKey = null, int? grpcPort = null) { throw null; } - [AspireExport(Description = "Adds the Attu administration tool for Milvus.", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithAttu(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) where T : Milvus.MilvusServerResource { throw null; } [System.Obsolete("Use WithConfigurationFile instead.")] public static ApplicationModel.IResourceBuilder WithConfigurationBindMount(this ApplicationModel.IResourceBuilder builder, string configurationFilePath) { throw null; } - [AspireExport(Description = "Copies a Milvus configuration file into the container.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithConfigurationFile(this ApplicationModel.IResourceBuilder builder, string configurationFilePath) { throw null; } - [AspireExport(Description = "Mounts a host directory as the Milvus data directory.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a persistent data volume to the Milvus server resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } } } diff --git a/src/Aspire.Hosting.MongoDB/api/Aspire.Hosting.MongoDB.cs b/src/Aspire.Hosting.MongoDB/api/Aspire.Hosting.MongoDB.cs index 57e9a02960c..52a48d92ee6 100644 --- a/src/Aspire.Hosting.MongoDB/api/Aspire.Hosting.MongoDB.cs +++ b/src/Aspire.Hosting.MongoDB/api/Aspire.Hosting.MongoDB.cs @@ -10,32 +10,32 @@ namespace Aspire.Hosting { public static partial class MongoDBBuilderExtensions { - [AspireExport(Description = "Adds a MongoDB database resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds a MongoDB container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddMongoDB(this IDistributedApplicationBuilder builder, string name, int? port = null, ApplicationModel.IResourceBuilder? userName = null, ApplicationModel.IResourceBuilder? password = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with optional userName and password parameters instead.")] public static ApplicationModel.IResourceBuilder AddMongoDB(this IDistributedApplicationBuilder builder, string name, int? port) { throw null; } - [AspireExport(Description = "Adds a bind mount for the MongoDB data folder")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a named volume for the MongoDB data folder")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Sets the host port for the Mongo Express resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } [System.Obsolete("Use WithInitFiles instead.")] [AspireExportIgnore(Reason = "Obsolete API. Use WithInitFiles instead.")] public static ApplicationModel.IResourceBuilder WithInitBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = true) { throw null; } - [AspireExport(Description = "Copies init files into a MongoDB container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithInitFiles(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Adds a MongoExpress administration platform for MongoDB", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithMongoExpress(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) where T : ApplicationModel.MongoDBServerResource { throw null; } } diff --git a/src/Aspire.Hosting.MySql/api/Aspire.Hosting.MySql.cs b/src/Aspire.Hosting.MySql/api/Aspire.Hosting.MySql.cs index a1ab0d06cfb..79c6c2162b7 100644 --- a/src/Aspire.Hosting.MySql/api/Aspire.Hosting.MySql.cs +++ b/src/Aspire.Hosting.MySql/api/Aspire.Hosting.MySql.cs @@ -10,34 +10,34 @@ namespace Aspire.Hosting { public static partial class MySqlBuilderExtensions { - [AspireExport(Description = "Adds a MySQL database")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds a MySQL server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddMySql(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; } - [AspireExport(Description = "Defines the SQL script for database creation")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCreationScript(this ApplicationModel.IResourceBuilder builder, string script) { throw null; } - [AspireExport(Description = "Adds a data bind mount for MySQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume for MySQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Sets the host port for phpMyAdmin")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } [System.Obsolete("Use WithInitFiles instead.")] public static ApplicationModel.IResourceBuilder WithInitBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = true) { throw null; } - [AspireExport(Description = "Copies init files to MySQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithInitFiles(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Configures the MySQL password")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPassword(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder password) { throw null; } - [AspireExport(Description = "Adds phpMyAdmin management UI", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithPhpMyAdmin(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) where T : ApplicationModel.MySqlServerResource { throw null; } } diff --git a/src/Aspire.Hosting.Nats/api/Aspire.Hosting.Nats.cs b/src/Aspire.Hosting.Nats/api/Aspire.Hosting.Nats.cs index bc0098d1974..98e36d6b124 100644 --- a/src/Aspire.Hosting.Nats/api/Aspire.Hosting.Nats.cs +++ b/src/Aspire.Hosting.Nats/api/Aspire.Hosting.Nats.cs @@ -16,16 +16,16 @@ public static partial class NatsBuilderExtensions [AspireExportIgnore(Reason = "Use the dedicated polyglot overload instead.")] public static ApplicationModel.IResourceBuilder AddNats(this IDistributedApplicationBuilder builder, string name, int? port) { throw null; } - [AspireExport(Description = "Mounts a host directory as the NATS data directory.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a persistent data volume to the NATS resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use the overload without the srcMountPath parameter and WithDataBindMount extension instead if you want to keep data locally.")] public static ApplicationModel.IResourceBuilder WithJetStream(this ApplicationModel.IResourceBuilder builder, string? srcMountPath = null) { throw null; } - [AspireExport(Description = "Configures the NATS resource to enable JetStream.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithJetStream(this ApplicationModel.IResourceBuilder builder) { throw null; } } } diff --git a/src/Aspire.Hosting.OpenAI/api/Aspire.Hosting.OpenAI.cs b/src/Aspire.Hosting.OpenAI/api/Aspire.Hosting.OpenAI.cs index 45c3b60adbf..d2da6df11dd 100644 --- a/src/Aspire.Hosting.OpenAI/api/Aspire.Hosting.OpenAI.cs +++ b/src/Aspire.Hosting.OpenAI/api/Aspire.Hosting.OpenAI.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class OpenAIExtensions { - [AspireExport(Description = "Adds an OpenAI model resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddModel(this ApplicationModel.IResourceBuilder builder, string name, string model) { throw null; } - [AspireExport(Description = "Adds an OpenAI resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddOpenAI(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Configures the API key for the OpenAI resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithApiKey(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder apiKey) { throw null; } - [AspireExport(Description = "Configures the endpoint URI for the OpenAI resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, string endpoint) { throw null; } - [AspireExport(Description = "Adds a health check for the OpenAI model resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHealthCheck(this ApplicationModel.IResourceBuilder builder) { throw null; } } } diff --git a/src/Aspire.Hosting.Oracle/api/Aspire.Hosting.Oracle.cs b/src/Aspire.Hosting.Oracle/api/Aspire.Hosting.Oracle.cs index 82f1367c8bb..29109aa0b82 100644 --- a/src/Aspire.Hosting.Oracle/api/Aspire.Hosting.Oracle.cs +++ b/src/Aspire.Hosting.Oracle/api/Aspire.Hosting.Oracle.cs @@ -10,25 +10,25 @@ namespace Aspire.Hosting { public static partial class OracleDatabaseBuilderExtensions { - [AspireExport(Description = "Adds an Oracle database resource to an Oracle server resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds an Oracle server resource to the distributed application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddOracle(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; } - [AspireExport(Description = "Mounts a host directory as the Oracle data directory.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Adds a persistent data volume to the Oracle server resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - [AspireExport(Description = "Mounts a host directory as the Oracle DB setup directory.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDbSetupBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } [System.Obsolete("Use WithInitFiles instead.")] public static ApplicationModel.IResourceBuilder WithInitBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Copies initialization files into the Oracle container.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithInitFiles(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } } } diff --git a/src/Aspire.Hosting.Orleans/api/Aspire.Hosting.Orleans.cs b/src/Aspire.Hosting.Orleans/api/Aspire.Hosting.Orleans.cs index 4dbd4957c3f..7dabd5c2867 100644 --- a/src/Aspire.Hosting.Orleans/api/Aspire.Hosting.Orleans.cs +++ b/src/Aspire.Hosting.Orleans/api/Aspire.Hosting.Orleans.cs @@ -10,44 +10,44 @@ namespace Aspire.Hosting { public static partial class OrleansServiceClientExtensions { - [AspireExport("withOrleansClientReference", MethodName = "withReference", Description = "Adds an Orleans client reference to a resource")] + [AspireExport("withOrleansClientReference")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder builder, Orleans.OrleansServiceClient orleansServiceClient) where T : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithEndpoints { throw null; } } public static partial class OrleansServiceExtensions { - [AspireExport(Description = "Adds an Orleans service configuration")] + [AspireExport] public static Orleans.OrleansService AddOrleans(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "Creates an Orleans client view for the service")] + [AspireExport] public static Orleans.OrleansServiceClient AsClient(this Orleans.OrleansService orleansService) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the default provider overload instead.")] public static Orleans.OrleansService WithBroadcastChannel(this Orleans.OrleansService orleansServiceBuilder, string name, Orleans.IProviderConfiguration provider) { throw null; } - [AspireExport(Description = "Adds an Orleans broadcast channel provider")] + [AspireExport] public static Orleans.OrleansService WithBroadcastChannel(this Orleans.OrleansService orleansServiceBuilder, string name) { throw null; } [AspireExportIgnore(Reason = "ParameterResource handle overload is not needed in polyglot. Use the string overload instead.")] public static Orleans.OrleansService WithClusterId(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder clusterId) { throw null; } - [AspireExport(Description = "Sets the Orleans cluster ID")] + [AspireExport] public static Orleans.OrleansService WithClusterId(this Orleans.OrleansService orleansServiceBuilder, string clusterId) { throw null; } - [AspireExport(Description = "Configures Orleans clustering using a resource connection")] + [AspireExport] public static Orleans.OrleansService WithClustering(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder provider) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the resource-based overload instead.")] public static Orleans.OrleansService WithClustering(this Orleans.OrleansService orleansServiceBuilder, Orleans.IProviderConfiguration provider) { throw null; } - [AspireExport(Description = "Configures Orleans development clustering")] + [AspireExport] public static Orleans.OrleansService WithDevelopmentClustering(this Orleans.OrleansService orleansServiceBuilder) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with explicit provider name instead.")] public static Orleans.OrleansService WithGrainDirectory(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder provider) { throw null; } - [AspireExport(Description = "Adds an Orleans grain directory provider")] + [AspireExport] public static Orleans.OrleansService WithGrainDirectory(this Orleans.OrleansService orleansServiceBuilder, string name, ApplicationModel.IResourceBuilder provider) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the resource-based overload instead.")] @@ -56,26 +56,26 @@ public static partial class OrleansServiceExtensions [AspireExportIgnore(Reason = "Convenience overload. Use the overload with explicit provider name instead.")] public static Orleans.OrleansService WithGrainStorage(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder provider) { throw null; } - [AspireExport(Description = "Adds an Orleans grain storage provider")] + [AspireExport] public static Orleans.OrleansService WithGrainStorage(this Orleans.OrleansService orleansServiceBuilder, string name, ApplicationModel.IResourceBuilder provider) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the resource-based overload instead.")] public static Orleans.OrleansService WithGrainStorage(this Orleans.OrleansService orleansServiceBuilder, string name, Orleans.IProviderConfiguration provider) { throw null; } - [AspireExport(Description = "Adds in-memory Orleans grain storage")] + [AspireExport] public static Orleans.OrleansService WithMemoryGrainStorage(this Orleans.OrleansService orleansServiceBuilder, string name) { throw null; } - [AspireExport(Description = "Configures in-memory Orleans reminders")] + [AspireExport] public static Orleans.OrleansService WithMemoryReminders(this Orleans.OrleansService orleansServiceBuilder) { throw null; } - [AspireExport(Description = "Adds in-memory Orleans streaming")] + [AspireExport] public static Orleans.OrleansService WithMemoryStreaming(this Orleans.OrleansService orleansServiceBuilder, string name) { throw null; } - [AspireExport("withOrleansReference", MethodName = "withReference", Description = "Adds an Orleans silo reference to a resource")] + [AspireExport("withOrleansReference")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder builder, Orleans.OrleansService orleansService) where T : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Configures Orleans reminder storage")] + [AspireExport] public static Orleans.OrleansService WithReminders(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder provider) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the resource-based overload instead.")] @@ -84,13 +84,13 @@ public static ApplicationModel.IResourceBuilder WithReference(this Applica [AspireExportIgnore(Reason = "ParameterResource handle overload is not needed in polyglot. Use the string overload instead.")] public static Orleans.OrleansService WithServiceId(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder serviceId) { throw null; } - [AspireExport(Description = "Sets the Orleans service ID")] + [AspireExport] public static Orleans.OrleansService WithServiceId(this Orleans.OrleansService orleansServiceBuilder, string serviceId) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with explicit provider name instead.")] public static Orleans.OrleansService WithStreaming(this Orleans.OrleansService orleansServiceBuilder, ApplicationModel.IResourceBuilder provider) { throw null; } - [AspireExport(Description = "Adds an Orleans stream provider")] + [AspireExport] public static Orleans.OrleansService WithStreaming(this Orleans.OrleansService orleansServiceBuilder, string name, ApplicationModel.IResourceBuilder provider) { throw null; } [AspireExportIgnore(Reason = "IProviderConfiguration cannot be created directly by polyglot callers. Use the resource-based overload instead.")] diff --git a/src/Aspire.Hosting.PostgreSQL/api/Aspire.Hosting.PostgreSQL.cs b/src/Aspire.Hosting.PostgreSQL/api/Aspire.Hosting.PostgreSQL.cs index 5e903cfa614..03f25781d5d 100644 --- a/src/Aspire.Hosting.PostgreSQL/api/Aspire.Hosting.PostgreSQL.cs +++ b/src/Aspire.Hosting.PostgreSQL/api/Aspire.Hosting.PostgreSQL.cs @@ -10,52 +10,52 @@ namespace Aspire.Hosting { public static partial class PostgresBuilderExtensions { - [AspireExport(Description = "Adds a PostgreSQL database")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds a PostgreSQL server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPostgres(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? userName = null, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; } - [AspireExport(Description = "Defines the SQL script for database creation")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCreationScript(this ApplicationModel.IResourceBuilder builder, string script) { throw null; } - [AspireExport(Description = "Adds a data bind mount for PostgreSQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume for PostgreSQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport("withPostgresHostPort", MethodName = "withHostPort", Description = "Sets the host port for PostgreSQL")] + [AspireExport("withPostgresHostPort", MethodName = "withHostPort")] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withPgAdminHostPort", MethodName = "withHostPort", Description = "Sets the host port for pgAdmin")] + [AspireExport("withPgAdminHostPort", MethodName = "withHostPort")] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withPgWebHostPort", MethodName = "withHostPort", Description = "Sets the host port for pgweb")] + [AspireExport("withPgWebHostPort", MethodName = "withHostPort")] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } [AspireExportIgnore(Reason = "Obsolete. Use WithInitFiles instead.")] [System.Obsolete("Use WithInitFiles instead.")] public static ApplicationModel.IResourceBuilder WithInitBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = true) { throw null; } - [AspireExport(Description = "Copies init files to PostgreSQL")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithInitFiles(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Configures the PostgreSQL password")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPassword(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder password) { throw null; } - [AspireExport(Description = "Adds pgAdmin 4 management UI", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithPgAdmin(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) where T : ApplicationModel.PostgresServerResource { throw null; } - [AspireExport(Description = "Adds pgweb management UI", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithPgWeb(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } - [AspireExport(Description = "Adds Postgres MCP server", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPOSTGRES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithPostgresMcp(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } - [AspireExport(Description = "Configures the PostgreSQL user name")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithUserName(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder userName) { throw null; } } } diff --git a/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs b/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs index e3cf4f4bccc..767271d0ce4 100644 --- a/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs +++ b/src/Aspire.Hosting.Python/api/Aspire.Hosting.Python.cs @@ -18,35 +18,35 @@ public static partial class PythonAppResourceBuilderExtensions [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static ApplicationModel.IResourceBuilder AddPythonApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath, params string[] scriptArgs) { throw null; } - [AspireExport(Description = "Adds a Python script application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPythonApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string scriptPath) { throw null; } - [AspireExport(Description = "Adds a Python executable application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPythonExecutable(this IDistributedApplicationBuilder builder, string name, string appDirectory, string executableName) { throw null; } - [AspireExport(Description = "Adds a Python module application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddPythonModule(this IDistributedApplicationBuilder builder, string name, string appDirectory, string moduleName) { throw null; } - [AspireExport(Description = "Adds a Uvicorn-based Python application resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddUvicornApp(this IDistributedApplicationBuilder builder, string name, string appDirectory, string app) { throw null; } - [AspireExport(Description = "Enables debugging support for a Python application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDebugging(this ApplicationModel.IResourceBuilder builder) where T : Python.PythonAppResource { throw null; } - [AspireExport(Description = "Configures the entrypoint for a Python application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithEntrypoint(this ApplicationModel.IResourceBuilder builder, Python.EntrypointType entrypointType, string entrypoint) where T : Python.PythonAppResource { throw null; } - [AspireExport(Description = "Configures pip package installation for a Python application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPip(this ApplicationModel.IResourceBuilder builder, bool install = true, string[]? installArgs = null) where T : Python.PythonAppResource { throw null; } - [AspireExport(Description = "Configures uv package management for a Python application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithUv(this ApplicationModel.IResourceBuilder builder, bool install = true, string[]? args = null) where T : Python.PythonAppResource { throw null; } - [AspireExport(Description = "Configures the virtual environment for a Python application")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithVirtualEnvironment(this ApplicationModel.IResourceBuilder builder, string virtualEnvironmentPath, bool createIfNotExists = true) where T : Python.PythonAppResource { throw null; } } diff --git a/src/Aspire.Hosting.Qdrant/api/Aspire.Hosting.Qdrant.cs b/src/Aspire.Hosting.Qdrant/api/Aspire.Hosting.Qdrant.cs index bd8a78239f6..f57abb9435c 100644 --- a/src/Aspire.Hosting.Qdrant/api/Aspire.Hosting.Qdrant.cs +++ b/src/Aspire.Hosting.Qdrant/api/Aspire.Hosting.Qdrant.cs @@ -10,13 +10,13 @@ namespace Aspire.Hosting { public static partial class QdrantBuilderExtensions { - [AspireExport(Description = "Adds a Qdrant resource to the application. A container is used for local development.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddQdrant(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? apiKey = null, int? grpcPort = null, int? httpPort = null) { throw null; } - [AspireExport(Description = "Adds a bind mount for the data folder to a Qdrant container resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a named volume for the data folder to a Qdrant container resource.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the generic withReference export.")] diff --git a/src/Aspire.Hosting.RabbitMQ/api/Aspire.Hosting.RabbitMQ.cs b/src/Aspire.Hosting.RabbitMQ/api/Aspire.Hosting.RabbitMQ.cs index bb749cc0a68..b3584f2490c 100644 --- a/src/Aspire.Hosting.RabbitMQ/api/Aspire.Hosting.RabbitMQ.cs +++ b/src/Aspire.Hosting.RabbitMQ/api/Aspire.Hosting.RabbitMQ.cs @@ -10,13 +10,13 @@ namespace Aspire.Hosting { public static partial class RabbitMQBuilderExtensions { - [AspireExport(Description = "Adds a RabbitMQ container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddRabbitMQ(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? userName = null, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; } - [AspireExport(Description = "Adds a data bind mount to the RabbitMQ container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume to the RabbitMQ container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withManagementPlugin dispatcher export.")] diff --git a/src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs b/src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs index ecd75e18c67..169a64d270b 100644 --- a/src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs +++ b/src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs @@ -10,43 +10,43 @@ namespace Aspire.Hosting { public static partial class RedisBuilderExtensions { - [AspireExport(Description = "Adds a Redis container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddRedis(this IDistributedApplicationBuilder builder, string name, int? port = null, ApplicationModel.IResourceBuilder? password = null) { throw null; } [AspireExportIgnore(Reason = "Polyglot app hosts use the canonical addRedis export with options.")] public static ApplicationModel.IResourceBuilder AddRedis(this IDistributedApplicationBuilder builder, string name, int? port) { throw null; } - [AspireExport(Description = "Adds a data bind mount with persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport("withRedisInsightDataBindMount", MethodName = "withDataBindMount", Description = "Adds a data bind mount for Redis Insight")] + [AspireExport("withRedisInsightDataBindMount", MethodName = "withDataBindMount")] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; } - [AspireExport(Description = "Adds a data volume with persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport("withRedisInsightDataVolume", Description = "Adds a data volume for Redis Insight", MethodName = "withDataVolume")] + [AspireExport("withRedisInsightDataVolume", MethodName = "withDataVolume")] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; } - [AspireExport(Description = "Sets the host port for Redis")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withRedisCommanderHostPort", MethodName = "withHostPort", Description = "Sets the host port for Redis Commander")] + [AspireExport("withRedisCommanderHostPort", MethodName = "withHostPort")] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withRedisInsightHostPort", MethodName = "withHostPort", Description = "Sets the host port for Redis Insight")] + [AspireExport("withRedisInsightHostPort", MethodName = "withHostPort")] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Configures the password for Redis")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPassword(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder? password) { throw null; } - [AspireExport(Description = "Configures Redis persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPersistence(this ApplicationModel.IResourceBuilder builder, System.TimeSpan? interval = null, long keysChangedThreshold = 1) { throw null; } - [AspireExport(Description = "Adds Redis Commander management UI", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithRedisCommander(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } - [AspireExport(Description = "Adds Redis Insight management UI", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithRedisInsight(this ApplicationModel.IResourceBuilder builder, System.Action>? configureContainer = null, string? containerName = null) { throw null; } } } diff --git a/src/Aspire.Hosting.Seq/api/Aspire.Hosting.Seq.cs b/src/Aspire.Hosting.Seq/api/Aspire.Hosting.Seq.cs index 42fbb99955f..79bb56bac94 100644 --- a/src/Aspire.Hosting.Seq/api/Aspire.Hosting.Seq.cs +++ b/src/Aspire.Hosting.Seq/api/Aspire.Hosting.Seq.cs @@ -10,16 +10,16 @@ namespace Aspire.Hosting { public static partial class SeqBuilderExtensions { - [AspireExport(Description = "Adds a Seq server container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddSeq(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? adminPassword, int? port = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with optional adminPassword parameter instead.")] public static ApplicationModel.IResourceBuilder AddSeq(this IDistributedApplicationBuilder builder, string name, int? port = null) { throw null; } - [AspireExport(Description = "Adds a data bind mount for Seq")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume for Seq")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } } } diff --git a/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.cs b/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.cs index 6c40c49ce50..f553a7178ff 100644 --- a/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.cs +++ b/src/Aspire.Hosting.SqlServer/api/Aspire.Hosting.SqlServer.cs @@ -10,25 +10,25 @@ namespace Aspire.Hosting { public static partial class SqlServerBuilderExtensions { - [AspireExport(Description = "Adds a SQL Server database resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; } - [AspireExport(Description = "Adds a SQL Server container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddSqlServer(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; } - [AspireExport(Description = "Defines the SQL script used to create the database")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCreationScript(this ApplicationModel.IResourceBuilder builder, string script) { throw null; } - [AspireExport(Description = "Adds a bind mount for the SQL Server data folder")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a named volume for the SQL Server data folder")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Sets the host port for the SQL Server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Configures the password for the SQL Server resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPassword(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder password) { throw null; } } } diff --git a/src/Aspire.Hosting.Testing/api/Aspire.Hosting.Testing.cs b/src/Aspire.Hosting.Testing/api/Aspire.Hosting.Testing.cs index 946eb31a85e..9e25da9ed56 100644 --- a/src/Aspire.Hosting.Testing/api/Aspire.Hosting.Testing.cs +++ b/src/Aspire.Hosting.Testing/api/Aspire.Hosting.Testing.cs @@ -43,7 +43,7 @@ public static partial class DistributedApplicationHostingTestingExtensions [AspireExportIgnore(Reason = "Use the exported getConnectionString overload without a cancellation token.")] public static System.Threading.Tasks.ValueTask GetConnectionStringAsync(this DistributedApplication app, string resourceName, System.Threading.CancellationToken cancellationToken = default) { throw null; } - [AspireExport(Description = "Gets the endpoint for the specified resource.")] + [AspireExport] public static System.Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = null) { throw null; } [AspireExportIgnore(Reason = "Use the ATS-friendly overload that accepts a network identifier string.")] diff --git a/src/Aspire.Hosting.Valkey/api/Aspire.Hosting.Valkey.cs b/src/Aspire.Hosting.Valkey/api/Aspire.Hosting.Valkey.cs index aa83256845c..eff0f849a14 100644 --- a/src/Aspire.Hosting.Valkey/api/Aspire.Hosting.Valkey.cs +++ b/src/Aspire.Hosting.Valkey/api/Aspire.Hosting.Valkey.cs @@ -10,19 +10,19 @@ namespace Aspire.Hosting { public static partial class ValkeyBuilderExtensions { - [AspireExport(Description = "Adds a Valkey container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddValkey(this IDistributedApplicationBuilder builder, string name, int? port = null, ApplicationModel.IResourceBuilder? password = null) { throw null; } [AspireExportIgnore(Reason = "Convenience overload. Use the overload with optional password parameter instead.")] public static ApplicationModel.IResourceBuilder AddValkey(this IDistributedApplicationBuilder builder, string name, int? port) { throw null; } - [AspireExport(Description = "Adds a data bind mount for Valkey and enables persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Adds a data volume for Valkey and enables persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null, bool isReadOnly = false) { throw null; } - [AspireExport(Description = "Configures Valkey persistence")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPersistence(this ApplicationModel.IResourceBuilder builder, System.TimeSpan? interval = null, long keysChangedThreshold = 1) { throw null; } } } diff --git a/src/Aspire.Hosting.Yarp/api/Aspire.Hosting.Yarp.cs b/src/Aspire.Hosting.Yarp/api/Aspire.Hosting.Yarp.cs index 62fdbbd281a..358d07e94db 100644 --- a/src/Aspire.Hosting.Yarp/api/Aspire.Hosting.Yarp.cs +++ b/src/Aspire.Hosting.Yarp/api/Aspire.Hosting.Yarp.cs @@ -51,25 +51,25 @@ public static partial class YarpConfigurationBuilderExtensions public static partial class YarpResourceExtensions { - [AspireExport(Description = "Adds a YARP container to the application model.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddYarp(this IDistributedApplicationBuilder builder, string name) { throw null; } - [AspireExport(Description = "In publish mode, generates a Dockerfile that copies static files from the specified resource into /app/wwwroot.")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishWithStaticFiles(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder resourceWithFiles) { throw null; } - [AspireExport(Description = "Configure the YARP resource.", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder WithConfiguration(this ApplicationModel.IResourceBuilder builder, System.Action configurationBuilder) { throw null; } - [AspireExport(Description = "Configures the host HTTPS port that the YARP resource is exposed on instead of using randomly assigned port.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostHttpsPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport(Description = "Configures the host port that the YARP resource is exposed on instead of using randomly assigned port.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHostPort(this ApplicationModel.IResourceBuilder builder, int? port) { throw null; } - [AspireExport("withStaticFiles2", MethodName = "withStaticFiles", Description = "Enables static file serving. In run mode: bind mounts to /wwwroot.")] + [AspireExportIgnore(Reason = "A single internal export with an optional sourcePath parameter provides the polyglot API without changing the public C# overloads.")] public static ApplicationModel.IResourceBuilder WithStaticFiles(this ApplicationModel.IResourceBuilder builder, string sourcePath) { throw null; } - [AspireExport("withStaticFiles1", MethodName = "withStaticFiles", Description = "Enables static file serving in the YARP resource. Static files are served from the wwwroot folder.")] + [AspireExportIgnore(Reason = "A single internal export with an optional sourcePath parameter provides the polyglot API without changing the public C# overloads.")] public static ApplicationModel.IResourceBuilder WithStaticFiles(this ApplicationModel.IResourceBuilder builder) { throw null; } } } @@ -100,10 +100,10 @@ public static partial class YarpClusterExtensions [AspireExportIgnore(Reason = "HttpClientConfig is not ATS-compatible. Use the DTO-based overload instead.")] public static YarpCluster WithHttpClientConfig(this YarpCluster cluster, global::Yarp.ReverseProxy.Configuration.HttpClientConfig config) { throw null; } - [AspireExport(Description = "Sets the load balancing policy for the cluster.")] + [AspireExport] public static YarpCluster WithLoadBalancingPolicy(this YarpCluster cluster, string policy) { throw null; } - [AspireExport("withClusterMetadata", MethodName = "withMetadata", Description = "Sets metadata for the cluster.")] + [AspireExport("withClusterMetadata", MethodName = "withMetadata")] public static YarpCluster WithMetadata(this YarpCluster cluster, System.Collections.Generic.IReadOnlyDictionary metadata) { throw null; } [AspireExportIgnore(Reason = "SessionAffinityConfig is not ATS-compatible. Use the DTO-based overload instead.")] @@ -129,31 +129,31 @@ public static partial class YarpRouteExtensions [AspireExportIgnore(Reason = "RouteHeader is not ATS-compatible. Use the DTO-based overload instead.")] public static YarpRoute WithMatchHeaders(this YarpRoute route, params global::Yarp.ReverseProxy.Configuration.RouteHeader[] headers) { throw null; } - [AspireExport(Description = "Matches requests that contain the specified host headers.")] + [AspireExport] public static YarpRoute WithMatchHosts(this YarpRoute route, params string[] hosts) { throw null; } - [AspireExport(Description = "Matches requests that use the specified HTTP methods.")] + [AspireExport] public static YarpRoute WithMatchMethods(this YarpRoute route, params string[] methods) { throw null; } - [AspireExport(Description = "Matches requests with the specified path pattern.")] + [AspireExport] public static YarpRoute WithMatchPath(this YarpRoute route, string path) { throw null; } [AspireExportIgnore(Reason = "RouteQueryParameter is not ATS-compatible. Use the DTO-based overload instead.")] public static YarpRoute WithMatchRouteQueryParameter(this YarpRoute route, params global::Yarp.ReverseProxy.Configuration.RouteQueryParameter[] queryParameters) { throw null; } - [AspireExport(Description = "Sets the maximum request body size for the route.")] + [AspireExport] public static YarpRoute WithMaxRequestBodySize(this YarpRoute route, long maxRequestBodySize) { throw null; } - [AspireExport("withRouteMetadata", MethodName = "withMetadata", Description = "Sets metadata for the route.")] + [AspireExport("withRouteMetadata", MethodName = "withMetadata")] public static YarpRoute WithMetadata(this YarpRoute route, System.Collections.Generic.IReadOnlyDictionary? metadata) { throw null; } - [AspireExport(Description = "Sets the route order.")] + [AspireExport] public static YarpRoute WithOrder(this YarpRoute route, int? order) { throw null; } [AspireExportIgnore(Reason = "Action> callbacks are not ATS-compatible.")] public static YarpRoute WithTransform(this YarpRoute route, System.Action> createTransform) { throw null; } - [AspireExport(Description = "Sets the transforms for the route.")] + [AspireExport] public static YarpRoute WithTransforms(this YarpRoute route, System.Collections.Generic.IReadOnlyList>? transforms) { throw null; } } } @@ -162,19 +162,19 @@ namespace Aspire.Hosting.Yarp.Transforms { public static partial class ForwardedTransformExtensions { - [AspireExport(Description = "Adds the transform which will set the given header with the Base64 encoded client certificate.")] + [AspireExport] public static YarpRoute WithTransformClientCertHeader(this YarpRoute route, string headerName) { throw null; } - [AspireExport(Description = "Adds the transform which will add the Forwarded header as defined by [RFC 7239](https://tools.ietf.org/html/rfc7239).")] + [AspireExport] public static YarpRoute WithTransformForwarded(this YarpRoute route, bool useHost = true, bool useProto = true, global::Yarp.ReverseProxy.Transforms.NodeFormat forFormat = global::Yarp.ReverseProxy.Transforms.NodeFormat.Random, global::Yarp.ReverseProxy.Transforms.NodeFormat byFormat = global::Yarp.ReverseProxy.Transforms.NodeFormat.Random, global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions action = global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions.Set) { throw null; } - [AspireExport(Description = "Adds the transform which will add X-Forwarded-* headers.")] + [AspireExport] public static YarpRoute WithTransformXForwarded(this YarpRoute route, string headerPrefix = "X-Forwarded-", global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions xDefault = global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions.Set, global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions? xFor = null, global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions? xHost = null, global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions? xProto = null, global::Yarp.ReverseProxy.Transforms.ForwardedTransformActions? xPrefix = null) { throw null; } } public static partial class HttpMethodTransformExtensions { - [AspireExport(Description = "Adds the transform that will replace the HTTP method if it matches.")] + [AspireExport] public static YarpRoute WithTransformHttpMethodChange(this YarpRoute route, string fromHttpMethod, string toHttpMethod) { throw null; } } @@ -195,61 +195,61 @@ public static partial class PathTransformExtensions public static partial class QueryTransformExtensions { - [AspireExport(Description = "Adds the transform that will remove the given query key.")] + [AspireExport] public static YarpRoute WithTransformQueryRemoveKey(this YarpRoute route, string queryKey) { throw null; } - [AspireExport(Description = "Adds the transform that will append or set the query parameter from a route value.")] + [AspireExport] public static YarpRoute WithTransformQueryRouteValue(this YarpRoute route, string queryKey, string routeValueKey, bool append = true) { throw null; } - [AspireExport(Description = "Adds the transform that will append or set the query parameter from the given value.")] + [AspireExport] public static YarpRoute WithTransformQueryValue(this YarpRoute route, string queryKey, string value, bool append = true) { throw null; } } public static partial class RequestHeadersTransformExtensions { - [AspireExport(Description = "Adds the transform which will enable or suppress copying request headers to the proxy request.")] + [AspireExport] public static YarpRoute WithTransformCopyRequestHeaders(this YarpRoute route, bool copy = true) { throw null; } - [AspireExport(Description = "Adds the transform which will append or set the request header.")] + [AspireExport] public static YarpRoute WithTransformRequestHeader(this YarpRoute route, string headerName, string value, bool append = true) { throw null; } - [AspireExport(Description = "Adds the transform which will remove the request header.")] + [AspireExport] public static YarpRoute WithTransformRequestHeaderRemove(this YarpRoute route, string headerName) { throw null; } - [AspireExport(Description = "Adds the transform which will append or set the request header from a route value.")] + [AspireExport] public static YarpRoute WithTransformRequestHeaderRouteValue(this YarpRoute route, string headerName, string routeValueKey, bool append = true) { throw null; } - [AspireExport(Description = "Adds the transform which will only copy the allowed request headers. Other transforms")] + [AspireExport] public static YarpRoute WithTransformRequestHeadersAllowed(this YarpRoute route, params string[] allowedHeaders) { throw null; } - [AspireExport(Description = "Adds the transform which will copy the incoming request Host header to the proxy request.")] + [AspireExport] public static YarpRoute WithTransformUseOriginalHostHeader(this YarpRoute route, bool useOriginal = true) { throw null; } } public static partial class ResponseTransformExtensions { - [AspireExport(Description = "Adds the transform which will enable or suppress copying response headers to the client response.")] + [AspireExport] public static YarpRoute WithTransformCopyResponseHeaders(this YarpRoute route, bool copy = true) { throw null; } - [AspireExport(Description = "Adds the transform which will enable or suppress copying response trailers to the client response.")] + [AspireExport] public static YarpRoute WithTransformCopyResponseTrailers(this YarpRoute route, bool copy = true) { throw null; } - [AspireExport(Description = "Adds the transform which will append or set the response header.")] + [AspireExport] public static YarpRoute WithTransformResponseHeader(this YarpRoute route, string headerName, string value, bool append = true, global::Yarp.ReverseProxy.Transforms.ResponseCondition condition = global::Yarp.ReverseProxy.Transforms.ResponseCondition.Success) { throw null; } - [AspireExport(Description = "Adds the transform which will remove the response header.")] + [AspireExport] public static YarpRoute WithTransformResponseHeaderRemove(this YarpRoute route, string headerName, global::Yarp.ReverseProxy.Transforms.ResponseCondition condition = global::Yarp.ReverseProxy.Transforms.ResponseCondition.Success) { throw null; } - [AspireExport(Description = "Adds the transform which will only copy the allowed response headers. Other transforms")] + [AspireExport] public static YarpRoute WithTransformResponseHeadersAllowed(this YarpRoute route, params string[] allowedHeaders) { throw null; } - [AspireExport(Description = "Adds the transform which will append or set the response trailer.")] + [AspireExport] public static YarpRoute WithTransformResponseTrailer(this YarpRoute route, string headerName, string value, bool append = true, global::Yarp.ReverseProxy.Transforms.ResponseCondition condition = global::Yarp.ReverseProxy.Transforms.ResponseCondition.Success) { throw null; } - [AspireExport(Description = "Adds the transform which will remove the response trailer.")] + [AspireExport] public static YarpRoute WithTransformResponseTrailerRemove(this YarpRoute route, string headerName, global::Yarp.ReverseProxy.Transforms.ResponseCondition condition = global::Yarp.ReverseProxy.Transforms.ResponseCondition.Success) { throw null; } - [AspireExport(Description = "Adds the transform which will only copy the allowed response trailers. Other transforms")] + [AspireExport] public static YarpRoute WithTransformResponseTrailersAllowed(this YarpRoute route, params string[] allowedHeaders) { throw null; } } } \ No newline at end of file diff --git a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs index 8761dd98dab..40e6c181cac 100644 --- a/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs +++ b/src/Aspire.Hosting/ApplicationModel/AllocatedEndpoint.cs @@ -45,13 +45,13 @@ public class AllocatedEndpoint /// The port number of the endpoint. /// A string representing how to retrieve the target port of the instance. /// The binding mode of the endpoint. - /// The network identifier for the network associated with the endpoint. + /// The network identifier for the network associated with the endpoint. public AllocatedEndpoint( EndpointAnnotation endpoint, string address, int port, EndpointBindingMode bindingMode, string? targetPortExpression = null, - NetworkIdentifier? networkID = null + NetworkIdentifier? networkId = null ) { ArgumentNullException.ThrowIfNull(endpoint); @@ -63,7 +63,7 @@ public AllocatedEndpoint( BindingMode = bindingMode; Port = port; TargetPortExpression = targetPortExpression; - NetworkID = networkID ?? endpoint.DefaultNetworkID; + NetworkID = networkId ?? endpoint.DefaultNetworkID; } /// diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 3d1cbc9db7c..354499973c1 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -25,7 +25,7 @@ public sealed class EndpointAnnotation : IResourceAnnotation private bool? _tlsEnabled; private bool _isProxied = true; private bool? _isExplicitlyProxied; - private readonly NetworkIdentifier _networkID; + private readonly NetworkIdentifier _networkId; /// /// Initializes a new instance of . @@ -97,7 +97,7 @@ bool isProxied /// Initializes a new instance of . /// /// Network protocol: TCP or UDP are supported today, others possibly in future. - /// The ID of the network that is the "default" network for the Endpoint. + /// The ID of the network that is the "default" network for the Endpoint. /// Clients connected to the same network can reach the endpoint without any routing or network address translation. /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. /// Transport that is being used (e.g. http, http2, http3 etc). @@ -108,7 +108,7 @@ bool isProxied /// Specifies if the endpoint will be proxied by DCP. Defaults to . public EndpointAnnotation( ProtocolType protocol, - NetworkIdentifier? networkID, + NetworkIdentifier? networkId, string? uriScheme = null, string? transport = null, [EndpointName] string? name = null, @@ -136,9 +136,9 @@ public EndpointAnnotation( _targetPort = targetPort; IsExternal = isExternal ?? false; IsExplicitlyProxied = isProxied; - _networkID = networkID ?? KnownNetworkIdentifiers.LocalhostNetwork; + _networkId = networkId ?? KnownNetworkIdentifiers.LocalhostNetwork; #pragma warning disable CS0618 // Type or member is obsolete - AllAllocatedEndpoints.TryAdd(_networkID, AllocatedEndpointSnapshot); + AllAllocatedEndpoints.TryAdd(_networkId, AllocatedEndpointSnapshot); #pragma warning restore CS0618 // Type or member is obsolete } @@ -146,7 +146,7 @@ public EndpointAnnotation( /// Initializes a new instance of . /// /// Network protocol: TCP or UDP are supported today, others possibly in future. - /// The ID of the network that is the "default" network for the Endpoint. + /// The ID of the network that is the "default" network for the Endpoint. /// Clients connected to the same network can reach the endpoint without any routing or network address translation. /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. /// Transport that is being used (e.g. http, http2, http3 etc). @@ -157,7 +157,7 @@ public EndpointAnnotation( /// Specifies if the endpoint will be proxied by DCP. public EndpointAnnotation( ProtocolType protocol, - NetworkIdentifier? networkID, + NetworkIdentifier? networkId, string? uriScheme, string? transport, [EndpointName] string? name, @@ -167,7 +167,7 @@ public EndpointAnnotation( bool isProxied ) : this( protocol, - networkID, + networkId, uriScheme, transport, name, @@ -334,7 +334,7 @@ public bool TlsEnabled /// /// Gets the ID of the network that is the "default" network for the Endpoint (the one the Endpoint is associated with and can be reached without routing or network address translation). /// - public NetworkIdentifier DefaultNetworkID => _networkID; + public NetworkIdentifier DefaultNetworkID => _networkId; /// /// Gets or sets the default for this Endpoint. @@ -364,9 +364,9 @@ public AllocatedEndpoint? AllocatedEndpoint } else { - if (_networkID != value.NetworkID) + if (_networkId != value.NetworkID) { - throw new InvalidOperationException($"The default AllocatedEndpoint's network ID must match the EndpointAnnotation network ID ('{_networkID}'). The attempted AllocatedEndpoint belongs to '{value.NetworkID}'."); + throw new InvalidOperationException($"The default AllocatedEndpoint's network ID must match the EndpointAnnotation network ID ('{_networkId}'). The attempted AllocatedEndpoint belongs to '{value.NetworkID}'."); } AllocatedEndpointSnapshot.SetValue(value); } @@ -419,15 +419,15 @@ IEnumerator IEnumerable.GetEnumerator() /// Adds an AllocatedEndpoint snapshot for a specific network if one does not already exist. /// [Obsolete("This method is for internal use only and will be marked internal in a future Aspire release. Use AddOrUpdateAllocatedEndpoint instead.")] - public bool TryAdd(NetworkIdentifier networkID, ValueSnapshot snapshot) + public bool TryAdd(NetworkIdentifier networkId, ValueSnapshot snapshot) { lock (_snapshots) { - if (_snapshots.Any(s => s.NetworkID.Equals(networkID))) + if (_snapshots.Any(s => s.NetworkID.Equals(networkId))) { return false; } - _snapshots.Add(new NetworkEndpointSnapshot(snapshot, networkID)); + _snapshots.Add(new NetworkEndpointSnapshot(snapshot, networkId)); return true; } } @@ -435,33 +435,33 @@ public bool TryAdd(NetworkIdentifier networkID, ValueSnapshot /// /// Adds or updates an AllocatedEndpoint value associated with a specific network in the snapshot list. /// - public void AddOrUpdateAllocatedEndpoint(NetworkIdentifier networkID, AllocatedEndpoint endpoint) + public void AddOrUpdateAllocatedEndpoint(NetworkIdentifier networkId, AllocatedEndpoint endpoint) { - if (endpoint.NetworkID != networkID) + if (endpoint.NetworkID != networkId) { - throw new ArgumentException($"AllocatedEndpoint must use the same network as the {nameof(networkID)} parameter", nameof(endpoint)); + throw new ArgumentException($"AllocatedEndpoint must use the same network as the {nameof(networkId)} parameter", nameof(endpoint)); } - var nes = GetSnapshotFor(networkID); + var nes = GetSnapshotFor(networkId); nes.Snapshot.SetValue(endpoint); } /// /// Gets an AllocatedEndpoint for a given network ID, waiting for it to appear if it is not already present. /// - public Task GetAllocatedEndpointAsync(NetworkIdentifier networkID, CancellationToken cancellationToken = default) + public Task GetAllocatedEndpointAsync(NetworkIdentifier networkId, CancellationToken cancellationToken = default) { - var nes = GetSnapshotFor(networkID); + var nes = GetSnapshotFor(networkId); return nes.Snapshot.GetValueAsync(cancellationToken); } - private NetworkEndpointSnapshot GetSnapshotFor(NetworkIdentifier networkID) + private NetworkEndpointSnapshot GetSnapshotFor(NetworkIdentifier networkId) { lock (_snapshots) { - var nes = _snapshots.FirstOrDefault(s => s.NetworkID.Equals(networkID)); + var nes = _snapshots.FirstOrDefault(s => s.NetworkID.Equals(networkId)); if (nes is null) { - nes = new NetworkEndpointSnapshot(new ValueSnapshot(), networkID); + nes = new NetworkEndpointSnapshot(new ValueSnapshot(), networkId); _snapshots.Add(nes); } return nes; diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 6ae84bf893d..38bf5581644 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -16,7 +16,7 @@ public sealed class EndpointReference : IExpressionValue, IManifestExpressionPro // A reference to the endpoint annotation if it exists. private EndpointAnnotation? _endpointAnnotation; private bool? _isAllocated; - private readonly NetworkIdentifier? _contextNetworkID; + private readonly NetworkIdentifier? _contextNetworkId; /// /// Gets the endpoint annotation associated with the endpoint reference. @@ -129,7 +129,7 @@ private string BuildMissingEndpointMessage() /// The reference will be resolved in the context of this network, which may be different /// from the network associated with the default network of the referenced Endpoint. /// - public NetworkIdentifier? ContextNetworkID => _contextNetworkID; + public NetworkIdentifier? ContextNetworkID => _contextNetworkId; /// /// Gets the specified property expression of the endpoint. @@ -244,7 +244,7 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen foreach (var nes in endpointAnnotation.AllAllocatedEndpoints) { - if (string.Equals(nes.NetworkID.Value, (_contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork).Value, StringComparisons.NetworkID)) + if (string.Equals(nes.NetworkID.Value, (_contextNetworkId ?? KnownNetworkIdentifiers.LocalhostNetwork).Value, StringComparisons.NetworkId)) { if (!nes.Snapshot.IsValueSet) { @@ -263,14 +263,14 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen /// /// The resource with endpoints that owns the referenced endpoint. /// The endpoint annotation. - /// The ID of the network that serves as the context for the EndpointReference. + /// The ID of the network that serves as the context for the EndpointReference. /// /// Most Aspire resources are accessed in the context of the "localhost" network (host processes calling other host processes, /// or host processes calling container via mapped ports). If a is specified, the /// will always resolve in the context of that network. If the is null, the reference will attempt to resolve itself /// based on the context of the requesting resource. /// - public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID) + public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkId) { ArgumentNullException.ThrowIfNull(owner); ArgumentNullException.ThrowIfNull(endpoint); @@ -278,7 +278,7 @@ public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoi Resource = owner; EndpointName = endpoint.Name; _endpointAnnotation = endpoint; - _contextNetworkID = contextNetworkID; + _contextNetworkId = contextNetworkId; } /// @@ -295,21 +295,21 @@ public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoi /// /// The resource with endpoints that owns the referenced endpoint. /// The name of the endpoint. - /// The ID of the network that serves as the context for the EndpointReference. + /// The ID of the network that serves as the context for the EndpointReference. /// /// Most Aspire resources are accessed in the context of the "localhost" network (host proceses calling other host processes, /// or host processes calling container via mapped ports). This is why EndpointReference assumes this /// context unless specified otherwise. However, for container-to-container, or container-to-host communication, /// you must specify a container network context for the EndpointReference to be resolved correctly. /// - public EndpointReference(IResourceWithEndpoints owner, string endpointName, NetworkIdentifier? contextNetworkID = null) + public EndpointReference(IResourceWithEndpoints owner, string endpointName, NetworkIdentifier? contextNetworkId = null) { ArgumentNullException.ThrowIfNull(owner); ArgumentNullException.ThrowIfNull(endpointName); Resource = owner; EndpointName = endpointName; - _contextNetworkID = contextNetworkID; + _contextNetworkId = contextNetworkId; } /// diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs index beb2c2fc1d3..ef3eae38416 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReferenceAnnotation.cs @@ -24,10 +24,10 @@ public sealed class EndpointReferenceAnnotation(IResourceWithEndpoints resource) /// /// Gets the set of specific endpoint names that are referenced. When is , this set is ignored. /// - public HashSet EndpointNames { get; } = new(StringComparers.EndpointAnnotationName); + public ISet EndpointNames { get; } = new HashSet(StringComparers.EndpointAnnotationName); /// /// Gets or sets the network identifier used as context for resolving endpoint addresses. /// - public NetworkIdentifier ContextNetworkID { get; set; } = KnownNetworkIdentifiers.LocalhostNetwork; + public NetworkIdentifier ContextNetworkId { get; set; } = KnownNetworkIdentifiers.LocalhostNetwork; } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index ee946810e99..8533253d008 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -721,14 +721,14 @@ public static IEnumerable GetEndpoints(this IResourceWithEndp /// Gets references to all endpoints for the specified resource. /// /// The which contains annotations. - /// The ID of the network that serves as the context context for the endpoint references. + /// The ID of the network that serves as the context context for the endpoint references. /// An enumeration of based on the annotations from the resources' collection. [AspireExportIgnore(Reason = "Network-specific endpoint enumeration is not part of the ATS surface.")] - public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkID) + public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkId) { if (TryGetAnnotationsOfType(resource, out var endpoints)) { - return endpoints.Select(e => new EndpointReference(resource, e, contextNetworkID)); + return endpoints.Select(e => new EndpointReference(resource, e, contextNetworkId)); } return []; @@ -761,10 +761,10 @@ public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource /// /// The which contains annotations. /// The name of the endpoint. - /// The network ID of the network that provides the context for the returned + /// The network ID of the network that provides the context for the returned /// An object providing resolvable reference for the specified endpoint. [AspireExportIgnore(Reason = "Network-specific endpoint lookup is not part of the ATS surface.")] - public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkID) + public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkId) { var endpoint = resource.TryGetEndpoints(out var endpoints) ? @@ -772,11 +772,11 @@ public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource null; if (endpoint is null) { - return new EndpointReference(resource, endpointName, contextNetworkID); + return new EndpointReference(resource, endpointName, contextNetworkId); } else { - return new EndpointReference(resource, endpoint, contextNetworkID); + return new EndpointReference(resource, endpoint, contextNetworkId); } } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceUrlsCallbackContext.cs b/src/Aspire.Hosting/ApplicationModel/ResourceUrlsCallbackContext.cs index 56c699365e3..b36abae189b 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceUrlsCallbackContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceUrlsCallbackContext.cs @@ -41,11 +41,11 @@ public class ResourceUrlsCallbackContext(DistributedApplicationExecutionContext /// If does not implement then returns null. /// /// The name of the endpoint. - /// The identifier of the network that serves as the context for the endpoint reference. - public EndpointReference? GetEndpoint(string name, NetworkIdentifier contextNetworkID) => + /// The identifier of the network that serves as the context for the endpoint reference. + public EndpointReference? GetEndpoint(string name, NetworkIdentifier contextNetworkId) => Resource switch { - IResourceWithEndpoints resourceWithEndpoints => resourceWithEndpoints.GetEndpoint(name, contextNetworkID), + IResourceWithEndpoints resourceWithEndpoints => resourceWithEndpoints.GetEndpoint(name, contextNetworkId), _ => null }; diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 8ecd3607985..0497d618a92 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -1,32 +1,3 @@  - - - CP0006 - M:Aspire.Hosting.Pipelines.IDeploymentStateManager.ClearAllStateAsync(System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0006 - M:Aspire.Hosting.Publishing.IContainerRuntime.ComposeDownAsync(Aspire.Hosting.Publishing.ComposeOperationContext,System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0006 - M:Aspire.Hosting.Publishing.IContainerRuntime.ComposeListServicesAsync(Aspire.Hosting.Publishing.ComposeOperationContext,System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0006 - M:Aspire.Hosting.Publishing.IContainerRuntime.ComposeUpAsync(Aspire.Hosting.Publishing.ComposeOperationContext,System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - + \ No newline at end of file diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index 6801c2b6768..f0f8171ffa0 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -312,7 +312,7 @@ ts.Service is not null && throw new InvalidDataException($"The '{endpoint.Name}' on resource '{ts.ResourceName}' should have an associated DCP Service resource already set up"); } - var networkID = new NetworkIdentifier(ts.ContainerNetworkName!); + var networkId = new NetworkIdentifier(ts.ContainerNetworkName!); var address = string.IsNullOrEmpty(ts.TunnelInstanceName) ? containerHostName : KnownHostNames.DefaultContainerTunnelHostName; var port = (int)ts.Service!.AllocatedPort!; @@ -322,9 +322,9 @@ ts.Service is not null && port, EndpointBindingMode.SingleAddress, targetPortExpression: $$$"""{{- portForServing "{{{ts.Service.Metadata.Name}}}" -}}""", - networkID + networkId ); - endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(networkID, tunnelAllocatedEndpoint); + endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(networkId, tunnelAllocatedEndpoint); } } } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 7eafe2ee5a0..d583bc09143 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -812,7 +812,7 @@ private static Action CreateEndpointReferenceEnviron // Track per-scheme index for service discovery keys to handle multiple endpoints with the same scheme. var schemeIndexTracker = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var endpoint in annotation.Resource.GetEndpoints(annotation.ContextNetworkID)) + foreach (var endpoint in annotation.Resource.GetEndpoints(annotation.ContextNetworkId)) { if (specificEndpointName != null && !string.Equals(endpoint.EndpointName, specificEndpointName, StringComparison.OrdinalIgnoreCase)) { @@ -1407,7 +1407,7 @@ private static void ApplyEndpoints(this IResourceBuilder builder, IResourc endpointReferenceAnnotation = new EndpointReferenceAnnotation(resourceWithEndpoints); if (builder.Resource.IsContainer()) { - endpointReferenceAnnotation.ContextNetworkID = KnownNetworkIdentifiers.DefaultAspireContainerNetwork; + endpointReferenceAnnotation.ContextNetworkId = KnownNetworkIdentifiers.DefaultAspireContainerNetwork; } builder.WithAnnotation(endpointReferenceAnnotation); @@ -1482,7 +1482,7 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build // can also be resolved in the context of container-to-container communication by using the target port // and the container name as the host. This is why we only set the context network to localhost, // for both container and non-container resources. - endpoint = new EndpointAnnotation(ProtocolType.Tcp, name: endpointName, networkID: KnownNetworkIdentifiers.LocalhostNetwork); + endpoint = new EndpointAnnotation(ProtocolType.Tcp, name: endpointName, networkId: KnownNetworkIdentifiers.LocalhostNetwork); callback(endpoint); builder.Resource.Annotations.Add(endpoint); } @@ -1612,7 +1612,7 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build targetPort: targetPort, isExternal: isExternal, isProxied: isProxied, - networkID: KnownNetworkIdentifiers.LocalhostNetwork); + networkId: KnownNetworkIdentifiers.LocalhostNetwork); ConfigureEndpointEnvironmentVariable(builder, annotation, env); @@ -1885,15 +1885,15 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui /// The resource type. /// The the resource builder. /// The name of the endpoint. - /// The network context in which to resolve the endpoint. If null, localhost (loopback) network context will be used. + /// The network context in which to resolve the endpoint. If null, localhost (loopback) network context will be used. /// An that can be used to resolve the address of the endpoint after resource allocation has occurred. /// This method is not available in polyglot app hosts. Use the overload without NetworkIdentifier instead. [AspireExportIgnore(Reason = "NetworkIdentifier is not ATS-compatible.")] - public static EndpointReference GetEndpoint(this IResourceBuilder builder, [EndpointName] string name, NetworkIdentifier contextNetworkID) where T : IResourceWithEndpoints + public static EndpointReference GetEndpoint(this IResourceBuilder builder, [EndpointName] string name, NetworkIdentifier contextNetworkId) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); - return builder.Resource.GetEndpoint(name, contextNetworkID); + return builder.Resource.GetEndpoint(name, contextNetworkId); } /// @@ -4888,7 +4888,7 @@ public static IResourceBuilder WithHidden(this IResourceBuilder builder /// /// The resource type. /// The resource builder. - /// The completion exit code to treat as successful. Defaults to 0. + /// The completion exit code to treat as successful. /// The . /// /// This method is useful for one-off resources such as setup scripts, migrations, or build steps that should remain visible while running @@ -4896,7 +4896,7 @@ public static IResourceBuilder WithHidden(this IResourceBuilder builder /// Hidden resources can still be accessed directly by their name, by using Show hidden resources toggle in the dashboard or by using aspire describe --include-hidden from the CLI. /// [AspireExportIgnore(Reason = "Use ATS-friendly overload that supports a single exit code or multiple exit codes.")] - public static IResourceBuilder WithHiddenOnCompletion(this IResourceBuilder builder, int exitCode = 0) where T : IResource + public static IResourceBuilder WithHiddenOnCompletion(this IResourceBuilder builder, int exitCode) where T : IResource { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.cs b/src/Aspire.Hosting/api/Aspire.Hosting.cs index 60c26a7b764..0f37302c3c9 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.cs +++ b/src/Aspire.Hosting/api/Aspire.Hosting.cs @@ -9,14 +9,12 @@ namespace Aspire.Hosting { [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public sealed partial class AspireDtoAttribute : System.Attribute { public string? DtoTypeId { get { throw null; } set { } } } [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.Class | System.AttributeTargets.Method | System.AttributeTargets.Property | System.AttributeTargets.Interface, Inherited = false, AllowMultiple = true)] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public sealed partial class AspireExportAttribute : System.Attribute { public AspireExportAttribute() { } @@ -41,14 +39,12 @@ public AspireExportAttribute(System.Type type) { } } [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method | System.AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public sealed partial class AspireExportIgnoreAttribute : System.Attribute { public string? Reason { get { throw null; } set { } } } [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Parameter, AllowMultiple = false)] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public sealed partial class AspireUnionAttribute : System.Attribute { public AspireUnionAttribute(params System.Type[] types) { } @@ -57,7 +53,6 @@ public AspireUnionAttribute(params System.Type[] types) { } } [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public sealed partial class AspireValueAttribute : System.Attribute { public AspireValueAttribute(string catalogName) { } @@ -100,7 +95,7 @@ public static partial class ContainerRegistryResourceBuilderExtensions public static ApplicationModel.IResourceBuilder AddContainerRegistry(this IDistributedApplicationBuilder builder, string name, string endpoint, string? repository = null) { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECOMPUTE003", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Configures a resource to use a container registry")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerRegistry(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder registry) where TDestination : ApplicationModel.IResource where TContainerRegistry : ApplicationModel.IResource, ApplicationModel.IContainerRegistry { throw null; } } @@ -113,28 +108,28 @@ public static partial class ContainerResourceBuilderExtensions [AspireExportIgnore(Reason = "Use the polyglot addContainer overload that accepts a string or AddContainerOptions value.")] public static ApplicationModel.IResourceBuilder AddContainer(this IDistributedApplicationBuilder builder, string name, string image) { throw null; } - [AspireExport(Description = "Adds a container resource built from a Dockerfile")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDockerfile(this IDistributedApplicationBuilder builder, string name, string contextPath, string? dockerfilePath = null, string? stage = null) { throw null; } [AspireExportIgnore(Reason = "This synchronous overload is excluded from the polyglot surface; only the async callback overload is exported.")] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder AddDockerfileBuilder(this IDistributedApplicationBuilder builder, string name, string contextPath, System.Action callback, string? stage = null) { throw null; } - [AspireExport(Description = "Adds a container resource built from a programmatically generated Dockerfile")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder AddDockerfileBuilder(this IDistributedApplicationBuilder builder, string name, string contextPath, System.Func callback, string? stage = null) { throw null; } - [AspireExportIgnore(Reason = "DockerfileFactoryContext exposes IServiceProvider and IResource — .NET runtime types not usable from polyglot hosts.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the async callback overload.")] public static ApplicationModel.IResourceBuilder AddDockerfileFactory(this IDistributedApplicationBuilder builder, string name, string contextPath, System.Func dockerfileFactory, string? stage = null) { throw null; } - [AspireExportIgnore(Reason = "DockerfileFactoryContext exposes IServiceProvider and IResource — .NET runtime types not usable from polyglot hosts.")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDockerfileFactory(this IDistributedApplicationBuilder builder, string name, string contextPath, System.Func> dockerfileFactory, string? stage = null) { throw null; } - [AspireExport(Description = "Configures the resource to be published as a container")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsContainer(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Adds a bind mount")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithBindMount(this ApplicationModel.IResourceBuilder builder, string source, string target, bool isReadOnly = false) where T : ApplicationModel.ContainerResource { throw null; } @@ -146,7 +141,7 @@ public static ApplicationModel.IResourceBuilder WithBuildArg(this Applicat public static ApplicationModel.IResourceBuilder WithBuildArg(this ApplicationModel.IResourceBuilder builder, string name, object? value) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport("withParameterBuildSecret", MethodName = "withBuildSecret", Description = "Adds a build secret from a parameter resource")] + [AspireExport("withParameterBuildSecret", MethodName = "withBuildSecret")] public static ApplicationModel.IResourceBuilder WithBuildSecret(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder value) where T : ApplicationModel.ContainerResource { throw null; } @@ -166,11 +161,11 @@ public static ApplicationModel.IResourceBuilder WithContainerFiles(this Ap public static ApplicationModel.IResourceBuilder WithContainerFiles(this ApplicationModel.IResourceBuilder builder, string destinationPath, string sourcePath, int? defaultOwner = null, int? defaultGroup = null, System.IO.UnixFileMode? umask = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container name")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerName(this ApplicationModel.IResourceBuilder builder, string name) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Adds a network alias for the container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerNetworkAlias(this ApplicationModel.IResourceBuilder builder, string alias) where T : ApplicationModel.ContainerResource { throw null; } @@ -182,15 +177,15 @@ public static ApplicationModel.IResourceBuilder WithContainerRuntimeArgs(t public static ApplicationModel.IResourceBuilder WithContainerRuntimeArgs(this ApplicationModel.IResourceBuilder builder, System.Func callback) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Adds runtime arguments for the container")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerRuntimeArgs(this ApplicationModel.IResourceBuilder builder, params string[] args) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Configures the resource to use a Dockerfile")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDockerfile(this ApplicationModel.IResourceBuilder builder, string contextPath, string? dockerfilePath = null, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the base image for a Dockerfile build")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithDockerfileBaseImage(this ApplicationModel.IResourceBuilder builder, string? buildImage = null, string? runtimeImage = null) where T : ApplicationModel.IResource { throw null; } @@ -200,48 +195,48 @@ public static ApplicationModel.IResourceBuilder WithDockerfileBaseImage(th public static ApplicationModel.IResourceBuilder WithDockerfileBuilder(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Action callback, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Configures the resource to use a programmatically generated Dockerfile")] + [AspireExport] [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDOCKERFILEBUILDER001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static ApplicationModel.IResourceBuilder WithDockerfileBuilder(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Func callback, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExportIgnore(Reason = "DockerfileFactoryContext exposes IServiceProvider and IResource — .NET runtime types not usable from polyglot hosts.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the async callback overload.")] public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Func dockerfileFactory, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExportIgnore(Reason = "DockerfileFactoryContext exposes IServiceProvider and IResource — .NET runtime types not usable from polyglot hosts.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Func> dockerfileFactory, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Configures endpoint proxy support")] + [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container entrypoint")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithEntrypoint(this ApplicationModel.IResourceBuilder builder, string entrypoint) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container image")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImage(this ApplicationModel.IResourceBuilder builder, string image, string? tag = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container image pull policy")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImagePullPolicy(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ImagePullPolicy pullPolicy) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container image registry")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImageRegistry(this ApplicationModel.IResourceBuilder builder, string? registry) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the image SHA256 digest")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImageSHA256(this ApplicationModel.IResourceBuilder builder, string sha256) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the container image tag")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImageTag(this ApplicationModel.IResourceBuilder builder, string tag) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport(Description = "Sets the lifetime behavior of the container resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithLifetime(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ContainerLifetime lifetime) where T : ApplicationModel.ContainerResource { throw null; } @@ -299,7 +294,7 @@ public virtual void Dispose() { } public void Run() { } - [AspireExport("run", Description = "Runs the distributed application")] + [AspireExport("run", RunSyncOnBackgroundThread = true)] public virtual System.Threading.Tasks.Task RunAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; } public virtual System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; } @@ -471,34 +466,34 @@ public sealed partial class DistributedApplicationOptions [System.Diagnostics.CodeAnalysis.Experimental("ASPIREDOTNETTOOL", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static partial class DotnetToolResourceExtensions { - [AspireExport(Description = "Adds a .NET tool resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddDotnetTool(this IDistributedApplicationBuilder builder, string name, string packageId) { throw null; } [AspireExportIgnore(Reason = "Open generic IResource constraint — not ATS-compatible.")] public static ApplicationModel.IResourceBuilder AddDotnetTool(this IDistributedApplicationBuilder builder, T resource) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Ignores existing NuGet feeds")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolIgnoreExistingFeeds(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Ignores failed NuGet sources")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolIgnoreFailedSources(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Sets the tool package ID")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolPackage(this ApplicationModel.IResourceBuilder builder, string packageId) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Allows prerelease tool versions")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolPrerelease(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Adds a NuGet source for the tool")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolSource(this ApplicationModel.IResourceBuilder builder, string source) where T : ApplicationModel.DotnetToolResource { throw null; } - [AspireExport(Description = "Sets the tool version")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithToolVersion(this ApplicationModel.IResourceBuilder builder, string version) where T : ApplicationModel.DotnetToolResource { throw null; } } @@ -514,10 +509,10 @@ public static partial class ExecutableResourceBuilderExtensions [AspireExportIgnore(Reason = "Uses object[] parameter which is not ATS-compatible. String[] overload is exported.")] public static ApplicationModel.IResourceBuilder AddExecutable(this IDistributedApplicationBuilder builder, string name, string command, string workingDirectory, params object[]? args) { throw null; } - [AspireExport(Description = "Adds an executable resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddExecutable(this IDistributedApplicationBuilder builder, string name, string command, string workingDirectory, params string[]? args) { throw null; } - [AspireExport(Description = "Publishes an executable as a Docker file", RunSyncOnBackgroundThread = true)] + [AspireExport(RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this ApplicationModel.IResourceBuilder builder, System.Action>? configure) where T : ApplicationModel.ExecutableResource { throw null; } @@ -529,11 +524,11 @@ public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this A public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.ExecutableResource { throw null; } - [AspireExport("withExecutableCommand", Description = "Sets the executable command")] + [AspireExport("withExecutableCommand")] public static ApplicationModel.IResourceBuilder WithCommand(this ApplicationModel.IResourceBuilder builder, string command) where T : ApplicationModel.ExecutableResource { throw null; } - [AspireExport(Description = "Sets the executable working directory")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithWorkingDirectory(this ApplicationModel.IResourceBuilder builder, string workingDirectory) where T : ApplicationModel.ExecutableResource { throw null; } } @@ -615,7 +610,7 @@ public partial interface IDistributedApplicationBuilder ApplicationModel.IResourceBuilder AddResource(T resource) where T : ApplicationModel.IResource; - [AspireExport(Description = "Builds the distributed application")] + [AspireExport] DistributedApplication Build(); ApplicationModel.IResourceBuilder CreateResourceBuilder(T resource) where T : ApplicationModel.IResource; @@ -657,15 +652,20 @@ public partial class InputsDialogInteractionOptions : InteractionOptions } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport(ExposeProperties = true)] public sealed partial class InputsDialogValidationContext { public required System.Threading.CancellationToken CancellationToken { get { throw null; } init { } } public required InteractionInputCollection Inputs { get { throw null; } init { } } + [AspireExportIgnore(Reason = "IServiceProvider is not part of the polyglot validation surface.")] public required System.IServiceProvider Services { get { throw null; } init { } } public void AddValidationError(InteractionInput input, string errorMessage) { } + + [AspireExport("InputsDialogValidationContext.addValidationError", MethodName = "addValidationError")] + public void AddValidationError(string inputName, string errorMessage) { } } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] @@ -679,6 +679,7 @@ public enum InputType } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireDto] [System.Diagnostics.DebuggerDisplay("Name = {Name}, InputType = {InputType}, Required = {Required}, Value = {Value}")] public sealed partial class InteractionInput { @@ -710,6 +711,7 @@ public sealed partial class InteractionInput } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] [System.Diagnostics.DebuggerDisplay("Count = {Count}")] public sealed partial class InteractionInputCollection : System.Collections.Generic.IReadOnlyList, System.Collections.Generic.IEnumerable, System.Collections.IEnumerable, System.Collections.Generic.IReadOnlyCollection { @@ -725,10 +727,21 @@ public InteractionInputCollection(System.Collections.Generic.IReadOnlyList GetEnumerator() { throw null; } + public int GetInt32(string name) { throw null; } + + public string? GetString(string name) { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + [AspireExport("InteractionInputCollection.toArray", MethodName = "toArray")] + public InteractionInput[] ToArray() { throw null; } + public bool TryGetByName(string name, out InteractionInput? input) { throw null; } } @@ -802,9 +815,9 @@ public partial interface IUserSecretsManager void GetOrSetSecret(Microsoft.Extensions.Configuration.IConfigurationManager configuration, string name, System.Func valueGenerator); [AspireExportIgnore(Reason = "JsonObject is not ATS-compatible. Use the ATS helper overload that accepts a JSON string.")] System.Threading.Tasks.Task SaveStateAsync(System.Text.Json.Nodes.JsonObject state, System.Threading.CancellationToken cancellationToken = default); - [AspireExport(Description = "Attempts to delete a user secret value")] + [AspireExport] bool TryDeleteSecret(string name); - [AspireExport(Description = "Attempts to set a user secret value")] + [AspireExport] bool TrySetSecret(string name, string value); } @@ -856,7 +869,7 @@ public sealed partial class LoadInputContext public static partial class McpServerResourceBuilderExtensions { [System.Diagnostics.CodeAnalysis.Experimental("ASPIREMCP001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Configures an MCP server endpoint on the resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithMcpServer(this ApplicationModel.IResourceBuilder builder, string? path = "/mcp", string? endpointName = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } } @@ -948,7 +961,7 @@ public static partial class ParameterResourceBuilderExtensions [AspireExportIgnore(Reason = "Polyglot app hosts use the internal addParameter dispatcher export.")] public static ApplicationModel.IResourceBuilder AddParameter(this IDistributedApplicationBuilder builder, string name, string value, bool publishValueAsDefault = false, bool secret = false) { throw null; } - [AspireExport(Description = "Adds a parameter sourced from configuration")] + [AspireExport] public static ApplicationModel.IResourceBuilder AddParameterFromConfiguration(this IDistributedApplicationBuilder builder, string name, string configurationKey, bool secret = false) { throw null; } public static void ConfigureConnectionStringManifestPublisher(ApplicationModel.IResourceBuilder builder) { } @@ -959,7 +972,7 @@ public static void ConfigureConnectionStringManifestPublisher(ApplicationModel.I public static ApplicationModel.ParameterResource CreateParameter(IDistributedApplicationBuilder builder, string name, bool secret) { throw null; } - [AspireExport(Description = "Publishes the resource as a connection string")] + [AspireExport] public static ApplicationModel.IResourceBuilder PublishAsConnectionString(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.ContainerResource, ApplicationModel.IResourceWithConnectionString { throw null; } @@ -967,7 +980,7 @@ public static ApplicationModel.IResourceBuilder PublishAsConnectionString( [AspireExportIgnore(Reason = "Complex Func delegate with InteractionInput — not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithCustomInput(this ApplicationModel.IResourceBuilder builder, System.Func createInput) { throw null; } - [AspireExport(Description = "Sets a parameter description")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDescription(this ApplicationModel.IResourceBuilder builder, string description, bool enableMarkdown = false) { throw null; } } @@ -1002,17 +1015,17 @@ public static partial class ProjectResourceBuilderExtensions public static ApplicationModel.IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, string name) where TProject : IProjectMetadata, new() { throw null; } - [AspireExport(Description = "Disables forwarded headers for the project")] + [AspireExport] public static ApplicationModel.IResourceBuilder DisableForwardedHeaders(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", Description = "Publishes a project as a Docker file with optional container configuration", RunSyncOnBackgroundThread = true)] + [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this ApplicationModel.IResourceBuilder builder, System.Action>? configure = null) where T : ApplicationModel.ProjectResource { throw null; } [AspireExportIgnore(Reason = "Uses Func which is not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithEndpointsInEnvironment(this ApplicationModel.IResourceBuilder builder, System.Func filter) { throw null; } - [AspireExport(Description = "Sets the number of replicas")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithReplicas(this ApplicationModel.IResourceBuilder builder, int replicas) { throw null; } } @@ -1033,41 +1046,41 @@ public static partial class RequiredCommandResourceExtensions public static ApplicationModel.IResourceBuilder WithRequiredCommand(this ApplicationModel.IResourceBuilder builder, string command, System.Func> validationCallback, string? helpLink = null) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Adds a required command dependency")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithRequiredCommand(this ApplicationModel.IResourceBuilder builder, string command, string? helpLink = null) where T : ApplicationModel.IResource { throw null; } } public static partial class ResourceBuilderExtensions { - [AspireExport(Description = "Configures resource for HTTP/2")] + [AspireExport] public static ApplicationModel.IResourceBuilder AsHttp2Service(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Clears all container file sources")] + [AspireExport] public static ApplicationModel.IResourceBuilder ClearContainerFilesSources(this ApplicationModel.IResourceBuilder builder) where T : IResourceWithContainerFiles { throw null; } - [AspireExport(Description = "Excludes the resource from the deployment manifest")] + [AspireExport] public static ApplicationModel.IResourceBuilder ExcludeFromManifest(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Excludes the resource from MCP server exposure")] + [AspireExport] public static ApplicationModel.IResourceBuilder ExcludeFromMcp(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Gets a connection property by key")] + [AspireExport] public static ApplicationModel.ReferenceExpression GetConnectionProperty(this ApplicationModel.IResourceWithConnectionString resource, string key) { throw null; } [AspireExportIgnore(Reason = "NetworkIdentifier is not ATS-compatible.")] - public static ApplicationModel.EndpointReference GetEndpoint(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.NetworkIdentifier contextNetworkID) + public static ApplicationModel.EndpointReference GetEndpoint(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.NetworkIdentifier contextNetworkId) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Gets an endpoint reference")] + [AspireExport] public static ApplicationModel.EndpointReference GetEndpoint(this ApplicationModel.IResourceBuilder builder, string name) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport("publishWithContainerFilesFromResource", MethodName = "publishWithContainerFiles", Description = "Configures the resource to copy container files from the specified source during publishing")] + [AspireExport("publishWithContainerFilesFromResource", MethodName = "publishWithContainerFiles")] public static ApplicationModel.IResourceBuilder PublishWithContainerFiles(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder source, string destinationPath) where T : ApplicationModel.IContainerFilesDestinationResource { throw null; } @@ -1084,7 +1097,7 @@ public static ApplicationModel.IResourceBuilder WaitFor(this ApplicationMo public static ApplicationModel.IResourceBuilder WaitFor(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder dependency) where T : ApplicationModel.IResourceWithWaitSupport { throw null; } - [AspireExport("waitForResourceCompletion", MethodName = "waitForCompletion", Description = "Waits for resource completion")] + [AspireExport("waitForResourceCompletion", MethodName = "waitForCompletion")] public static ApplicationModel.IResourceBuilder WaitForCompletion(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder dependency, int exitCode = 0) where T : ApplicationModel.IResourceWithWaitSupport { throw null; } @@ -1096,7 +1109,7 @@ public static ApplicationModel.IResourceBuilder WaitForStart(this Applicat public static ApplicationModel.IResourceBuilder WaitForStart(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder dependency) where T : ApplicationModel.IResourceWithWaitSupport { throw null; } - [AspireExport("withArgsCallback", Description = "Sets command-line arguments via callback")] + [AspireExport("withArgsCallback")] public static ApplicationModel.IResourceBuilder WithArgs(this ApplicationModel.IResourceBuilder builder, System.Action callback) where T : ApplicationModel.IResourceWithArgs { throw null; } @@ -1108,7 +1121,7 @@ public static ApplicationModel.IResourceBuilder WithArgs(this ApplicationM public static ApplicationModel.IResourceBuilder WithArgs(this ApplicationModel.IResourceBuilder builder, params object[] args) where T : ApplicationModel.IResourceWithArgs { throw null; } - [AspireExport(Description = "Adds arguments")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithArgs(this ApplicationModel.IResourceBuilder builder, params string[] args) where T : ApplicationModel.IResourceWithArgs { throw null; } @@ -1120,7 +1133,7 @@ public static ApplicationModel.IResourceBuilder WithCertificateAuthor public static ApplicationModel.IResourceBuilder WithCertificateTrustConfiguration(this ApplicationModel.IResourceBuilder builder, System.Func callback) where TResource : ApplicationModel.IResourceWithArgs, ApplicationModel.IResourceWithEnvironment { throw null; } - [AspireExport(Description = "Sets the certificate trust scope")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCertificateTrustScope(this ApplicationModel.IResourceBuilder builder, ApplicationModel.CertificateTrustScope scope) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } @@ -1128,11 +1141,11 @@ public static ApplicationModel.IResourceBuilder WithCertificateTrustS public static ApplicationModel.IResourceBuilder WithChildRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResource child) where T : ApplicationModel.IResource { throw null; } - [AspireExport("withBuilderChildRelationship", MethodName = "withChildRelationship", Description = "Sets a child relationship")] + [AspireExport("withBuilderChildRelationship", MethodName = "withChildRelationship")] public static ApplicationModel.IResourceBuilder WithChildRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder child) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Adds a resource command")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithCommand(this ApplicationModel.IResourceBuilder builder, string name, string displayName, System.Func> executeCommand, ApplicationModel.CommandOptions? commandOptions = null) where T : ApplicationModel.IResource { throw null; } @@ -1140,7 +1153,7 @@ public static ApplicationModel.IResourceBuilder WithCommand(this Applicati public static ApplicationModel.IResourceBuilder WithCommand(this ApplicationModel.IResourceBuilder builder, string name, string displayName, System.Func> executeCommand, System.Func? updateState = null, string? displayDescription = null, object? parameter = null, string? confirmationMessage = null, string? iconName = null, ApplicationModel.IconVariant? iconVariant = null, bool isHighlighted = false) where T : ApplicationModel.IResource { throw null; } - [AspireExportIgnore(Reason = "IComputeEnvironmentResource is a specialized interface — not ATS-compatible.")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithComputeEnvironment(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder computeEnvironmentResource) where T : ApplicationModel.IComputeResource { throw null; } @@ -1156,7 +1169,7 @@ public static ApplicationModel.IResourceBuilder WithConnectionProperty(thi public static ApplicationModel.IResourceBuilder WithConnectionStringRedirection(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceWithConnectionString resource) where T : ApplicationModel.IResourceWithConnectionString { throw null; } - [AspireExport(Description = "Sets the source directory for container files")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithContainerFilesSource(this ApplicationModel.IResourceBuilder builder, string sourcePath) where T : IResourceWithContainerFiles { throw null; } @@ -1165,27 +1178,38 @@ public static ApplicationModel.IResourceBuilder WithContainerFilesSource(t public static ApplicationModel.IResourceBuilder WithDebugSupport(this ApplicationModel.IResourceBuilder builder, System.Func launchConfigurationProducer, string launchConfigurationType, System.Action? argsCallback = null) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Configures developer certificate trust")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithDeveloperCertificateTrust(this ApplicationModel.IResourceBuilder builder, bool trust) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } - [AspireExport(Description = "Adds a network endpoint")] - public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool isProxied, bool? isExternal, System.Net.Sockets.ProtocolType? protocol) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool isProxied, bool? isExternal) where T : ApplicationModel.IResourceWithEndpoints { throw null; } + [AspireExport] + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool? isProxied = null, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) + where T : ApplicationModel.IResourceWithEndpoints { throw null; } + + [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool? isProxied, bool? isExternal) + where T : ApplicationModel.IResourceWithEndpoints { throw null; } + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withEndpointCallback export, which exposes EndpointUpdateContext instead of EndpointAnnotation.")] public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, string endpointName, System.Action callback, bool createIfNotExists = true) where T : ApplicationModel.IResourceWithEndpoints { throw null; } + [AspireExport] + public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) { throw null; } + [AspireExportIgnore(Reason = "Polyglot app hosts use the async callback overload.")] public static ApplicationModel.IResourceBuilder WithEnvironment(this ApplicationModel.IResourceBuilder builder, System.Action callback) where T : ApplicationModel.IResourceWithEnvironment { throw null; } - [AspireExport("withEnvironmentCallback", Description = "Sets environment variables via callback")] + [AspireExport("withEnvironmentCallback")] public static ApplicationModel.IResourceBuilder WithEnvironment(this ApplicationModel.IResourceBuilder builder, System.Func callback) where T : ApplicationModel.IResourceWithEnvironment { throw null; } @@ -1205,7 +1229,7 @@ public static ApplicationModel.IResourceBuilder WithEnvironment(this Appli public static ApplicationModel.IResourceBuilder WithEnvironment(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder parameter) where T : ApplicationModel.IResourceWithEnvironment { throw null; } - [AspireExportIgnore(Reason = "Specialized overload — withReference covers this use case.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withEnvironment dispatcher export.")] public static ApplicationModel.IResourceBuilder WithEnvironment(this ApplicationModel.IResourceBuilder builder, string name, ApplicationModel.IResourceBuilder externalService) where T : ApplicationModel.IResourceWithEnvironment { throw null; } @@ -1229,18 +1253,30 @@ public static ApplicationModel.IResourceBuilder WithEnvironment(this Appli public static ApplicationModel.IResourceBuilder WithEnvironment(this ApplicationModel.IResourceBuilder builder, string name, TValue value) where T : ApplicationModel.IResourceWithEnvironment where TValue : ApplicationModel.IValueProvider, ApplicationModel.IManifestExpressionProvider { throw null; } - [AspireExport(Description = "Prevents resource from starting automatically")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithExplicitStart(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Makes HTTP endpoints externally accessible")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithExternalHttpEndpoints(this ApplicationModel.IResourceBuilder builder) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Adds a health check by key")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHealthCheck(this ApplicationModel.IResourceBuilder builder, string key) where T : ApplicationModel.IResource { throw null; } + [AspireExport] + public static ApplicationModel.IResourceBuilder WithHidden(this ApplicationModel.IResourceBuilder builder) + where T : ApplicationModel.IResource { throw null; } + + [AspireExportIgnore(Reason = "Use ATS-friendly overload that supports a single exit code or multiple exit codes.")] + public static ApplicationModel.IResourceBuilder WithHiddenOnCompletion(this ApplicationModel.IResourceBuilder builder, int exitCode) + where T : ApplicationModel.IResource { throw null; } + + [AspireExportIgnore(Reason = "Uses params array overload; use ATS-friendly overload for polyglot SDKs.")] + public static ApplicationModel.IResourceBuilder WithHiddenOnCompletion(this ApplicationModel.IResourceBuilder builder, params int[] exitCodes) + where T : ApplicationModel.IResource { throw null; } + [AspireExportIgnore(Reason = "Use the ATS-specific withHttpCommand export.")] public static ApplicationModel.IResourceBuilder WithHttpCommand(this ApplicationModel.IResourceBuilder builder, string path, string displayName, System.Func? endpointSelector, string? commandName = null, ApplicationModel.HttpCommandOptions? commandOptions = null) where TResource : ApplicationModel.IResourceWithEndpoints { throw null; } @@ -1249,15 +1285,19 @@ public static ApplicationModel.IResourceBuilder WithHttpCommand WithHttpCommand(this ApplicationModel.IResourceBuilder builder, string path, string displayName, string? endpointName = null, string? commandName = null, ApplicationModel.HttpCommandOptions? commandOptions = null) where TResource : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Adds an HTTP endpoint")] - public static ApplicationModel.IResourceBuilder WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static ApplicationModel.IResourceBuilder WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? name, string? env, bool isProxied) + where T : ApplicationModel.IResourceWithEndpoints { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "Func delegate — not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithHttpHealthCheck(this ApplicationModel.IResourceBuilder builder, System.Func? endpointSelector, string? path = null, int? statusCode = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Adds an HTTP health check")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithHttpHealthCheck(this ApplicationModel.IResourceBuilder builder, string? path = null, int? statusCode = null, string? endpointName = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } @@ -1282,19 +1322,23 @@ public static ApplicationModel.IResourceBuilder WithHttpsCertificateC where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport("withParameterHttpsDeveloperCertificate", MethodName = "withHttpsDeveloperCertificate", Description = "Configures HTTPS with a developer certificate")] + [AspireExport("withParameterHttpsDeveloperCertificate", MethodName = "withHttpsDeveloperCertificate")] public static ApplicationModel.IResourceBuilder WithHttpsDeveloperCertificate(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder? password = null) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } - [AspireExport(Description = "Adds an HTTPS endpoint")] - public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? name, string? env, bool isProxied) + where T : ApplicationModel.IResourceWithEndpoints { throw null; } + + [AspireExport] + public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use the WithHttpHealthCheck method instead.")] public static ApplicationModel.IResourceBuilder WithHttpsHealthCheck(this ApplicationModel.IResourceBuilder builder, string? path = null, int? statusCode = null, string? endpointName = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Sets the icon for the resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithIconName(this ApplicationModel.IResourceBuilder builder, string iconName, ApplicationModel.IconVariant iconVariant = ApplicationModel.IconVariant.Filled) where T : ApplicationModel.IResource { throw null; } @@ -1304,10 +1348,15 @@ public static ApplicationModel.IResourceBuilder WithImagePushOptions(this where T : ApplicationModel.IComputeResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] - [AspireExport(Description = "Sets image push options via callback")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithImagePushOptions(this ApplicationModel.IResourceBuilder builder, System.Func callback) where T : ApplicationModel.IComputeResource { throw null; } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] + public static ApplicationModel.IResourceBuilder WithLifetimeOf(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder sourceBuilder) + where T : ApplicationModel.IResource where TSource : ApplicationModel.IResource { throw null; } + [AspireExportIgnore(Reason = "ManifestPublishingContext exposes Utf8JsonWriter and DistributedApplicationExecutionContext — .NET runtime types not usable from polyglot hosts.")] public static ApplicationModel.IResourceBuilder WithManifestPublishingCallback(this ApplicationModel.IResourceBuilder builder, System.Action callback) where T : ApplicationModel.IResource { throw null; } @@ -1317,18 +1366,43 @@ public static ApplicationModel.IResourceBuilder WithManifestPublishingCallbac where T : ApplicationModel.IResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Removes HTTPS certificate configuration")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithoutHttpsCertificate(this ApplicationModel.IResourceBuilder builder) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] + public static ApplicationModel.IResourceBuilder WithParentProcessLifetime(this ApplicationModel.IResourceBuilder builder, int parentProcessId) + where T : ApplicationModel.IResource { throw null; } + [AspireExportIgnore(Reason = "Raw IResource interface — not ATS-compatible.")] public static ApplicationModel.IResourceBuilder WithParentRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResource parent) where T : ApplicationModel.IResource { throw null; } - [AspireExport("withBuilderParentRelationship", MethodName = "withParentRelationship", Description = "Sets the parent relationship")] + [AspireExport("withBuilderParentRelationship", MethodName = "withParentRelationship")] public static ApplicationModel.IResourceBuilder WithParentRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder parent) where T : ApplicationModel.IResource { throw null; } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] + public static ApplicationModel.IResourceBuilder WithPersistentLifetime(this ApplicationModel.IResourceBuilder builder) + where T : ApplicationModel.IResource { throw null; } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExportIgnore(Reason = "Process command factories are C# callbacks and cannot be represented in polyglot app hosts.")] + public static ApplicationModel.IResourceBuilder WithProcessCommand(this ApplicationModel.IResourceBuilder builder, string commandName, string displayName, System.Func processSpecFactory, ApplicationModel.ProcessCommandOptions? commandOptions = null) + where TResource : ApplicationModel.IResource { throw null; } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExportIgnore(Reason = "Process command factories are C# callbacks and cannot be represented in polyglot app hosts.")] + public static ApplicationModel.IResourceBuilder WithProcessCommand(this ApplicationModel.IResourceBuilder builder, string commandName, string displayName, System.Func> processSpecFactory, ApplicationModel.ProcessCommandOptions? commandOptions = null) + where TResource : ApplicationModel.IResource { throw null; } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExportIgnore(Reason = "Process commands start local processes from AppHost callbacks and cannot be represented in polyglot app hosts.")] + public static ApplicationModel.IResourceBuilder WithProcessCommand(this ApplicationModel.IResourceBuilder builder, string commandName, string displayName, string executablePath, System.Collections.Generic.IReadOnlyList? arguments = null, ApplicationModel.ProcessCommandOptions? commandOptions = null) + where TResource : ApplicationModel.IResource { throw null; } + [AspireExportIgnore(Reason = "Polyglot app hosts use the generic withReference dispatcher export.")] public static ApplicationModel.IResourceBuilder WithReference(this ApplicationModel.IResourceBuilder builder, ApplicationModel.EndpointReference endpointReference) where TDestination : ApplicationModel.IResourceWithEnvironment { throw null; } @@ -1373,20 +1447,25 @@ public static ApplicationModel.IResourceBuilder WithReferenceRelationship( public static ApplicationModel.IResourceBuilder WithRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResource resource, string type) where T : ApplicationModel.IResource { throw null; } - [AspireExport("withBuilderRelationship", MethodName = "withRelationship", Description = "Adds a relationship to another resource")] + [AspireExport("withBuilderRelationship", MethodName = "withRelationship")] public static ApplicationModel.IResourceBuilder WithRelationship(this ApplicationModel.IResourceBuilder builder, ApplicationModel.IResourceBuilder resourceBuilder, string type) where T : ApplicationModel.IResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] - [AspireExport(Description = "Sets the remote image name for publishing")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithRemoteImageName(this ApplicationModel.IResourceBuilder builder, string remoteImageName) where T : ApplicationModel.IComputeResource { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES003", UrlFormat = "https://aka.ms/aspire/diagnostics#{0}")] - [AspireExport(Description = "Sets the remote image tag for publishing")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithRemoteImageTag(this ApplicationModel.IResourceBuilder builder, string remoteImageTag) where T : ApplicationModel.IComputeResource { throw null; } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExport] + public static ApplicationModel.IResourceBuilder WithSessionLifetime(this ApplicationModel.IResourceBuilder builder) + where T : ApplicationModel.IResource { throw null; } + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withUrl dispatcher export.")] public static ApplicationModel.IResourceBuilder WithUrl(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ReferenceExpression url, string? displayText = null) where T : ApplicationModel.IResource { throw null; } @@ -1399,7 +1478,7 @@ public static ApplicationModel.IResourceBuilder WithUrl(this ApplicationMo public static ApplicationModel.IResourceBuilder WithUrl(this ApplicationModel.IResourceBuilder builder, string url, string? displayText = null) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Customizes the URL for a specific endpoint via callback")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithUrlForEndpoint(this ApplicationModel.IResourceBuilder builder, string endpointName, System.Action callback) where T : ApplicationModel.IResource { throw null; } @@ -1407,7 +1486,7 @@ public static ApplicationModel.IResourceBuilder WithUrlForEndpoint(this Ap public static ApplicationModel.IResourceBuilder WithUrlForEndpoint(this ApplicationModel.IResourceBuilder builder, string endpointName, System.Func callback) where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport(Description = "Customizes displayed URLs via callback")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithUrls(this ApplicationModel.IResourceBuilder builder, System.Action callback) where T : ApplicationModel.IResource { throw null; } @@ -1464,7 +1543,7 @@ public AfterResourcesCreatedEvent(System.IServiceProvider services, DistributedA [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Endpoint.Name}, UriString = {UriString}")] public partial class AllocatedEndpoint { - public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port, EndpointBindingMode bindingMode, string? targetPortExpression = null, NetworkIdentifier? networkID = null) { } + public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port, EndpointBindingMode bindingMode, string? targetPortExpression = null, NetworkIdentifier? networkId = null) { } public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port, EndpointBindingMode bindingMode, string? targetPortExpression = null) { } @@ -1493,7 +1572,7 @@ public AllocatedEndpoint(EndpointAnnotation endpoint, string address, int port, public static partial class AspireStoreExtensions { - [AspireExport(Description = "Gets a deterministic file path for the specified file contents")] + [AspireExport] public static string GetFileNameWithContent(this IAspireStore aspireStore, string filenameTemplate, string sourceFilename) { throw null; } } @@ -1638,13 +1717,16 @@ public CommandLineArgsCallbackContext(System.Collections.Generic.IList a public Microsoft.Extensions.Logging.ILogger Logger { get { throw null; } init { } } - [AspireExport(Description = "Gets the resource associated with this callback")] + [AspireExport] public IResource Resource { get { throw null; } } } [AspireDto] public partial class CommandOptions { + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public System.Collections.Generic.IReadOnlyList Arguments { get { throw null; } set { } } + public string? ConfirmationMessage { get { throw null; } set { } } public string? Description { get { throw null; } set { } } @@ -1655,9 +1737,15 @@ public partial class CommandOptions public bool IsHighlighted { get { throw null; } set { } } + [System.Obsolete("Use Arguments to describe invocation arguments and ExecuteCommandContext.Arguments to read them.")] public object? Parameter { get { throw null; } set { } } public System.Func? UpdateState { get { throw null; } set { } } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public System.Func? ValidateArguments { get { throw null; } set { } } + + public ResourceCommandVisibility Visibility { get { throw null; } set { } } } [AspireDto] @@ -1693,7 +1781,11 @@ public static partial class CommandResults public static ExecuteCommandResult Success(string message, CommandResultData value) { throw null; } + public static ExecuteCommandResult Success(string message, string result, CommandResultFormat resultFormat, bool displayImmediately) { throw null; } + public static ExecuteCommandResult Success(string message, string result, CommandResultFormat resultFormat = CommandResultFormat.Text) { throw null; } + + public static ExecuteCommandResult Success(string message) { throw null; } } public sealed partial class ConnectionPropertyAnnotation : IResourceAnnotation @@ -1752,13 +1844,13 @@ public ContainerBuildOptionsCallbackAnnotation(System.Func BuildArguments { get { throw null; } } + public string? BuildContextIgnoreContent { get { throw null; } set { } } + public System.Collections.Generic.Dictionary BuildSecrets { get { throw null; } } public string ContextPath { get { throw null; } } @@ -2191,6 +2285,8 @@ public DockerfileBuildAnnotation(string contextPath, string dockerfilePath, stri public string? Stage { get { throw null; } } + public System.Threading.Tasks.Task EmitDockerfileArtifactsAsync(DockerfileFactoryContext context, string? dockerfilePath = null) { throw null; } + public System.Threading.Tasks.Task MaterializeDockerfileAsync(DockerfileFactoryContext context, System.Threading.CancellationToken cancellationToken) { throw null; } } @@ -2222,10 +2318,12 @@ public DockerfileBuilderCallbackContext(IResource resource, Docker.DockerfileBui public System.IServiceProvider Services { get { throw null; } } } + [AspireExport] public sealed partial class DockerfileFactoryContext { public System.Threading.CancellationToken CancellationToken { get { throw null; } init { } } + [AspireExport] public required IResource Resource { get { throw null; } init { } } public required System.IServiceProvider Services { get { throw null; } init { } } @@ -2261,9 +2359,13 @@ public sealed partial class EmulatorResourceAnnotation : IResourceAnnotation [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] public sealed partial class EndpointAnnotation : IResourceAnnotation { - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkID, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkId, string? uriScheme, string? transport, string? name, int? port, int? targetPort, bool? isExternal, bool isProxied) { } - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkId, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } + + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme, string? transport, string? name, int? port, int? targetPort, bool? isExternal, bool isProxied) { } + + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } public NetworkEndpointSnapshotList AllAllocatedEndpoints { get { throw null; } } @@ -2276,6 +2378,8 @@ public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriS public bool ExcludeReferenceEndpoint { get { throw null; } set { } } + public bool? IsExplicitlyProxied { get { throw null; } set { } } + public bool IsExternal { get { throw null; } set { } } public bool IsProxied { get { throw null; } set { } } @@ -2353,11 +2457,11 @@ public enum EndpointProperty [System.Diagnostics.DebuggerDisplay("Resource = {Resource.Name}, EndpointName = {EndpointName}, IsAllocated = {IsAllocated}")] public sealed partial class EndpointReference : IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences { - public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID) { } + public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkId) { } public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint) { } - public EndpointReference(IResourceWithEndpoints owner, string endpointName, NetworkIdentifier? contextNetworkID = null) { } + public EndpointReference(IResourceWithEndpoints owner, string endpointName, NetworkIdentifier? contextNetworkId = null) { } public EndpointReference(IResourceWithEndpoints owner, string endpointName) { } @@ -2399,19 +2503,33 @@ public EndpointReference(IResourceWithEndpoints owner, string endpointName) { } public string Url { get { throw null; } } - [AspireExport(Description = "Gets a conditional expression that resolves to the enabledValue when TLS is enabled on the endpoint, or to the disabledValue otherwise.")] + [AspireExport] public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, ReferenceExpression disabledValue) { throw null; } [AspireExportIgnore] public System.Threading.Tasks.ValueTask GetValueAsync(ValueProviderContext context, System.Threading.CancellationToken cancellationToken = default) { throw null; } - [AspireExport(Description = "Gets the URL of the endpoint asynchronously")] + [AspireExport] public System.Threading.Tasks.ValueTask GetValueAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; } - [AspireExport(Description = "Gets the specified property expression of the endpoint")] + [AspireExport] public EndpointReferenceExpression Property(EndpointProperty property) { throw null; } } + [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Resource = {Resource.Name}, EndpointNames = {UseAllEndpoints ? \"(All)\" : string.Join(\", \", EndpointNames)}")] + public sealed partial class EndpointReferenceAnnotation : IResourceAnnotation + { + public EndpointReferenceAnnotation(IResourceWithEndpoints resource) { } + + public NetworkIdentifier ContextNetworkId { get { throw null; } set { } } + + public System.Collections.Generic.ISet EndpointNames { get { throw null; } } + + public IResourceWithEndpoints Resource { get { throw null; } } + + public bool UseAllEndpoints { get { throw null; } set { } } + } + [AspireExport(ExposeProperties = true)] [System.Diagnostics.DebuggerDisplay("EndpointExpression = {ValueExpression}, Property = {Property}, Endpoint = {Endpoint.EndpointName}")] public partial class EndpointReferenceExpression : IExpressionValue, IValueProvider, IManifestExpressionProvider, IValueWithReferences @@ -2457,12 +2575,12 @@ public EnvironmentCallbackContext(DistributedApplicationExecutionContext executi [AspireUnion(new[] { typeof(string), typeof(ReferenceExpression) })] public System.Collections.Generic.Dictionary EnvironmentVariables { get { throw null; } } - [AspireExport(Description = "Gets the execution context for this callback invocation")] + [AspireExport] public DistributedApplicationExecutionContext ExecutionContext { get { throw null; } } public Microsoft.Extensions.Logging.ILogger Logger { get { throw null; } set { } } - [AspireExport(Description = "Gets the resource associated with this callback")] + [AspireExport] public IResource Resource { get { throw null; } } } @@ -2497,12 +2615,15 @@ public ExecutableResource(string name, string command, string workingDirectory) [AspireExport(ExposeProperties = true)] public sealed partial class ExecuteCommandContext { + public required InteractionInputCollection Arguments { get { throw null; } init { } } + public required System.Threading.CancellationToken CancellationToken { get { throw null; } init { } } public required Microsoft.Extensions.Logging.ILogger Logger { get { throw null; } init { } } public required string ResourceName { get { throw null; } init { } } + [AspireExportIgnore(Reason = "IServiceProvider is not usable from polyglot command callbacks.")] public required System.IServiceProvider ServiceProvider { get { throw null; } init { } } } @@ -2534,14 +2655,14 @@ internal ExecutionConfigurationBuilder() { } public static partial class ExecutionConfigurationBuilderExtensions { - [AspireExport(Description = "Adds an arguments configuration gatherer")] + [AspireExport] public static IExecutionConfigurationBuilder WithArgumentsConfig(this IExecutionConfigurationBuilder builder) { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport(Description = "Adds a certificate trust configuration gatherer")] + [AspireExport] public static IExecutionConfigurationBuilder WithCertificateTrustConfig(this IExecutionConfigurationBuilder builder, System.Func configContextFactory) { throw null; } - [AspireExport(Description = "Adds an environment variables configuration gatherer")] + [AspireExport] public static IExecutionConfigurationBuilder WithEnvironmentVariablesConfig(this IExecutionConfigurationBuilder builder) { throw null; } [System.Diagnostics.CodeAnalysis.Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] @@ -2928,7 +3049,6 @@ public partial interface IResourceWithConnectionString : IResource, IExpressionV System.Threading.Tasks.ValueTask GetConnectionStringAsync(System.Threading.CancellationToken cancellationToken = default); } - [System.Diagnostics.CodeAnalysis.Experimental("ASPIREATS001")] public partial interface IResourceWithCustomWithReference : IResource where TSelf : IResource, IResourceWithCustomWithReference { IResourceBuilder? TryWithReference(IResourceBuilder builder, IResourceBuilder source, string? connectionName = null, bool optional = false, string? name = null) @@ -3044,6 +3164,12 @@ public LaunchProfileAnnotation(string launchProfileName) { } public string LaunchProfileName { get { throw null; } } } + public enum Lifetime + { + Session = 0, + Persistent = 1 + } + public readonly partial struct LogLine : System.IEquatable { private readonly object _dummy; @@ -3153,16 +3279,16 @@ public partial record NetworkEndpointSnapshot(ValueSnapshot S public partial class NetworkEndpointSnapshotList : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable { - public void AddOrUpdateAllocatedEndpoint(NetworkIdentifier networkID, AllocatedEndpoint endpoint) { } + public void AddOrUpdateAllocatedEndpoint(NetworkIdentifier networkId, AllocatedEndpoint endpoint) { } - public System.Threading.Tasks.Task GetAllocatedEndpointAsync(NetworkIdentifier networkID, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Threading.Tasks.Task GetAllocatedEndpointAsync(NetworkIdentifier networkId, System.Threading.CancellationToken cancellationToken = default) { throw null; } public System.Collections.Generic.IEnumerator GetEnumerator() { throw null; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } [System.Obsolete("This method is for internal use only and will be marked internal in a future Aspire release. Use AddOrUpdateAllocatedEndpoint instead.")] - public bool TryAdd(NetworkIdentifier networkID, ValueSnapshot snapshot) { throw null; } + public bool TryAdd(NetworkIdentifier networkId, ValueSnapshot snapshot) { throw null; } } public partial record NetworkIdentifier(string Value) @@ -3205,6 +3331,28 @@ public ParameterResource(string name, System.Func cal public System.Threading.Tasks.ValueTask GetValueAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Mode = {Mode}")] + public sealed partial class PersistenceAnnotation : IResourceAnnotation + { + public required PersistenceMode Mode { get { throw null; } set { } } + + public int? ParentProcessId { get { throw null; } set { } } + + public System.DateTime? ParentProcessTimestamp { get { throw null; } set { } } + + public IResource? SourceResource { get { throw null; } set { } } + } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public enum PersistenceMode + { + Session = 0, + Persistent = 1, + Resource = 2, + ParentProcess = 3 + } + public sealed partial class PortAllocator : IPortAllocator { public PortAllocator(int startPort = 8000) { } @@ -3238,6 +3386,60 @@ public enum ProbeType Liveness = 2 } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public partial class ProcessCommandOptions : CommandOptions + { + public bool DisplayImmediately { get { throw null; } set { } } + + public System.Func>? GetCommandResult { get { throw null; } set { } } + + public int MaxOutputLineCount { get { throw null; } set { } } + + public System.Collections.Generic.IReadOnlyList SuccessExitCodes { get { throw null; } set { } } + } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public sealed partial class ProcessCommandResultContext + { + public required System.Threading.CancellationToken CancellationToken { get { throw null; } init { } } + + public required int ExitCode { get { throw null; } init { } } + + public required Microsoft.Extensions.Logging.ILogger Logger { get { throw null; } init { } } + + public required System.Collections.Generic.IReadOnlyList Output { get { throw null; } init { } } + + public required ProcessCommandSpec ProcessCommandSpec { get { throw null; } init { } } + + public required string ResourceName { get { throw null; } init { } } + + public required System.IServiceProvider ServiceProvider { get { throw null; } init { } } + + public required int TotalOutputLineCount { get { throw null; } init { } } + + public string GetFormattedOutput(int maxLines = 50, string outputDescription = "Command output") { throw null; } + } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPROCESSCOMMAND001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public sealed partial class ProcessCommandSpec + { + public ProcessCommandSpec(string executablePath) { } + + public System.Collections.Generic.IReadOnlyList Arguments { get { throw null; } init { } } + + public System.Collections.Generic.IDictionary EnvironmentVariables { get { throw null; } init { } } + + public string ExecutablePath { get { throw null; } } + + public bool InheritEnvironmentVariables { get { throw null; } init { } } + + public bool KillEntireProcessTree { get { throw null; } init { } } + + public string? StandardInputContent { get { throw null; } init { } } + + public string? WorkingDirectory { get { throw null; } init { } } + } + [System.Diagnostics.DebuggerDisplay("{DebuggerToString(),nq}")] public partial class ProjectResource : Resource, IResourceWithEnvironment, IResource, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithEndpoints, IResourceWithWaitSupport, IResourceWithProbes, IComputeResource, IContainerFilesDestinationResource { @@ -3312,7 +3514,7 @@ internal ReferenceExpression() { } public System.Threading.Tasks.ValueTask GetValueAsync(ValueProviderContext context, System.Threading.CancellationToken cancellationToken) { throw null; } - [AspireExport(Description = "Gets the resolved string value of the reference expression asynchronously")] + [AspireExport] public System.Threading.Tasks.ValueTask GetValueAsync(System.Threading.CancellationToken cancellationToken) { throw null; } [System.Runtime.CompilerServices.InterpolatedStringHandler] @@ -3355,7 +3557,7 @@ public void Append(in ReferenceExpressionBuilderInterpolatedStringHandler handle [System.Obsolete("ReferenceExpression instances can't be used in interpolated string with a custom format. Duplicate the inner expression in-place.", true)] public void AppendFormatted(ReferenceExpression valueProvider, string format) { } - [AspireExport(Description = "Appends a formatted string value to the reference expression")] + [AspireExport] public void AppendFormatted(string? value, string? format = null) { } public void AppendFormatted(string? value) { } @@ -3366,13 +3568,13 @@ public void AppendFormatted(T valueProvider, string? format) public void AppendFormatted(T valueProvider) where T : IValueProvider, IManifestExpressionProvider { } - [AspireExport(Description = "Appends a literal string to the reference expression")] + [AspireExport] public void AppendLiteral(string value) { } - [AspireExport(Description = "Appends a value provider to the reference expression")] + [AspireExport] public void AppendValueProvider(object valueProvider, string? format = null) { } - [AspireExport(Description = "Builds the reference expression")] + [AspireExport] public ReferenceExpression Build() { throw null; } [System.Runtime.CompilerServices.InterpolatedStringHandler] @@ -3517,8 +3719,13 @@ public enum ResourceAnnotationMutationBehavior [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] public sealed partial class ResourceCommandAnnotation : IResourceAnnotation { + public ResourceCommandAnnotation(string name, string displayName, System.Func updateState, System.Func> executeCommand, string? displayDescription, System.Collections.Generic.IReadOnlyList? arguments, string? confirmationMessage, string? iconName, IconVariant? iconVariant, bool isHighlighted, ResourceCommandVisibility visibility = ResourceCommandVisibility.UI | ResourceCommandVisibility.Api, System.Func? validateArguments = null) { } + public ResourceCommandAnnotation(string name, string displayName, System.Func updateState, System.Func> executeCommand, string? displayDescription, object? parameter, string? confirmationMessage, string? iconName, IconVariant? iconVariant, bool isHighlighted) { } + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public System.Collections.Generic.IReadOnlyList Arguments { get { throw null; } } + public string? ConfirmationMessage { get { throw null; } } public string? DisplayDescription { get { throw null; } } @@ -3535,23 +3742,36 @@ public ResourceCommandAnnotation(string name, string displayName, System.Func UpdateState { get { throw null; } } + + [System.Diagnostics.CodeAnalysis.Experimental("ASPIREINTERACTION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + public System.Func? ValidateArguments { get { throw null; } } + + public ResourceCommandVisibility Visibility { get { throw null; } } } public partial class ResourceCommandService { internal ResourceCommandService() { } + public System.Threading.Tasks.Task ExecuteCommandAsync(IResource resource, string commandName, InteractionInputCollection arguments, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Threading.Tasks.Task ExecuteCommandAsync(IResource resource, string commandName, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Threading.Tasks.Task ExecuteCommandAsync(string resourceId, string commandName, InteractionInputCollection arguments, System.Threading.CancellationToken cancellationToken = default) { throw null; } + public System.Threading.Tasks.Task ExecuteCommandAsync(string resourceId, string commandName, System.Threading.CancellationToken cancellationToken = default) { throw null; } } [System.Diagnostics.DebuggerDisplay(null, Name = "{Name}")] public sealed partial record ResourceCommandSnapshot(string Name, ResourceCommandState State, string DisplayName, string? DisplayDescription, object? Parameter, string? ConfirmationMessage, string? IconName, IconVariant? IconVariant, bool IsHighlighted) { + public System.Collections.Generic.IReadOnlyList Arguments { get { throw null; } init { } } + + public ResourceCommandVisibility Visibility { get { throw null; } init { } } } public enum ResourceCommandState @@ -3561,6 +3781,14 @@ public enum ResourceCommandState Hidden = 2 } + [System.Flags] + public enum ResourceCommandVisibility + { + None = 0, + UI = 1, + Api = 2 + } + public enum ResourceDependencyDiscoveryMode { Recursive = 0, @@ -3607,13 +3835,13 @@ public static partial class ResourceExtensions public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource, IComputeEnvironmentResource? targetComputeEnvironment = null) { throw null; } [AspireExportIgnore(Reason = "Network-specific endpoint lookup is not part of the ATS surface.")] - public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkID) { throw null; } + public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkId) { throw null; } [AspireExportIgnore(Reason = "Resource handle endpoint lookup is not part of the ATS surface; use builder-based endpoint exports instead.")] public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName) { throw null; } [AspireExportIgnore(Reason = "Network-specific endpoint enumeration is not part of the ATS surface.")] - public static System.Collections.Generic.IEnumerable GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkID) { throw null; } + public static System.Collections.Generic.IEnumerable GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkId) { throw null; } [AspireExportIgnore(Reason = "Resource handle endpoint enumeration is not part of the ATS surface; use builder-based endpoint exports instead.")] public static System.Collections.Generic.IEnumerable GetEndpoints(this IResourceWithEndpoints resource) { throw null; } @@ -3853,27 +4081,29 @@ public ResourceUrlsCallbackContext(DistributedApplicationExecutionContext execut public System.Threading.CancellationToken CancellationToken { get { throw null; } } - [AspireExport(Description = "Gets the execution context for this callback invocation")] + [AspireExport] public DistributedApplicationExecutionContext ExecutionContext { get { throw null; } } public Microsoft.Extensions.Logging.ILogger Logger { get { throw null; } set { } } - [AspireExport(Description = "Gets the resource associated with these URLs")] + [AspireExport] public IResource Resource { get { throw null; } } public System.Collections.Generic.List Urls { get { throw null; } } - public EndpointReference? GetEndpoint(string name, NetworkIdentifier contextNetworkID) { throw null; } + public EndpointReference? GetEndpoint(string name, NetworkIdentifier contextNetworkId) { throw null; } - [AspireExport(Description = "Gets an endpoint reference from the associated resource")] + [AspireExport] public EndpointReference? GetEndpoint(string name) { throw null; } } [AspireExport(ExposeProperties = true)] public sealed partial class UpdateCommandStateContext { + [AspireExportIgnore(Reason = "CustomResourceSnapshot contains object-valued properties that are not statically representable in polyglot SDKs. Use ResourceSnapshotData for the curated ATS projection.")] public required CustomResourceSnapshot ResourceSnapshot { get { throw null; } init { } } + [AspireExportIgnore(Reason = "IServiceProvider is not usable from polyglot command state callbacks.")] public required System.IServiceProvider ServiceProvider { get { throw null; } init { } } } @@ -4202,7 +4432,7 @@ public void SetValue(string value) { } [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static partial class DistributedApplicationPipelineExtensions { - [AspireExport(Description = "Disables publish and deploy validation for unconsumed build-only containers.")] + [AspireExport] public static IDistributedApplicationPipeline DisableBuildOnlyContainerValidation(this IDistributedApplicationPipeline pipeline) { throw null; } } @@ -4314,7 +4544,7 @@ public partial class PipelineConfigurationContext [AspireExportIgnore(Reason = "IResource parameters on callback context methods are not ATS-compatible. Use pipeline helpers instead.")] public System.Collections.Generic.IEnumerable GetSteps(ApplicationModel.IResource resource) { throw null; } - [AspireExport(Description = "Gets pipeline steps with the specified tag")] + [AspireExport] public System.Collections.Generic.IEnumerable GetSteps(string tag) { throw null; } } @@ -4353,7 +4583,7 @@ public partial class PipelineOptions [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [System.Diagnostics.DebuggerDisplay("{DebuggerToString(),nq}")] - [AspireExport] + [AspireExport(ExposeProperties = true)] public partial class PipelineStep { public required System.Func Action { get { throw null; } init { } } @@ -4366,18 +4596,19 @@ public partial class PipelineStep public System.Collections.Generic.List RequiredBySteps { get { throw null; } init { } } + [AspireExportIgnore(Reason = "The associated resource is an internal runtime link and may be null for steps that are not tied to a resource.")] public ApplicationModel.IResource? Resource { get { throw null; } set { } } public System.Collections.Generic.List Tags { get { throw null; } init { } } public void DependsOn(PipelineStep step) { } - [AspireExport(Description = "Adds a dependency on another step by name")] + [AspireExport] public void DependsOn(string stepName) { } public void RequiredBy(PipelineStep step) { } - [AspireExport(Description = "Specifies that another step requires this step by name")] + [AspireExport] public void RequiredBy(string stepName) { } } @@ -4450,7 +4681,7 @@ public partial class PipelineStepFactoryContext [System.Diagnostics.CodeAnalysis.Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public static partial class PipelineStepFactoryExtensions { - [AspireExport(Description = "Configures pipeline step dependencies via a callback")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPipelineConfiguration(this ApplicationModel.IResourceBuilder builder, System.Action callback) where T : ApplicationModel.IResource { throw null; } @@ -4474,7 +4705,7 @@ public static ApplicationModel.IResourceBuilder WithPipelineStepFactory(th public static ApplicationModel.IResourceBuilder WithPipelineStepFactory(this ApplicationModel.IResourceBuilder builder, System.Func>> factory) where T : ApplicationModel.IResource { throw null; } - [AspireExport(Description = "Adds a pipeline step to the resource")] + [AspireExport] public static ApplicationModel.IResourceBuilder WithPipelineStepFactory(this ApplicationModel.IResourceBuilder builder, string stepName, System.Func callback, string[]? dependsOn = null, string[]? requiredBy = null, string[]? tags = null, string? description = null) where T : ApplicationModel.IResource { throw null; } } @@ -4597,6 +4828,7 @@ public static partial class WellKnownPipelineTags namespace Aspire.Hosting.Publishing { + [AspireExport(ExposeProperties = true)] public sealed partial class AfterPublishEvent : Eventing.IDistributedApplicationEvent { public AfterPublishEvent(System.IServiceProvider services, ApplicationModel.DistributedApplicationModel model) { } @@ -4606,6 +4838,7 @@ public AfterPublishEvent(System.IServiceProvider services, ApplicationModel.Dist public System.IServiceProvider Services { get { throw null; } } } + [AspireExport(ExposeProperties = true)] public sealed partial class BeforePublishEvent : Eventing.IDistributedApplicationEvent { public BeforePublishEvent(System.IServiceProvider services, ApplicationModel.DistributedApplicationModel model) { } diff --git a/src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs b/src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs index 36306fbdd3f..83f42638aab 100644 --- a/src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs +++ b/src/Aspire.TypeSystem/api/Aspire.TypeSystem.cs @@ -39,6 +39,8 @@ public sealed partial class AspireValueData public sealed partial class AtsCallbackParameterInfo { + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public required string Name { get { throw null; } init { } } public required AtsTypeRef Type { get { throw null; } init { } } @@ -52,6 +54,8 @@ public sealed partial class AtsCapabilityInfo public string? Description { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public System.Collections.Generic.IReadOnlyList ExpandedTargetTypes { get { throw null; } set { } } public bool IsObsolete { get { throw null; } init { } } @@ -185,10 +189,29 @@ public enum AtsDiagnosticSeverity Error = 2 } + public sealed partial class AtsDocumentationInfo + { + public System.Collections.Generic.IReadOnlyList Parameters { get { throw null; } init { } } + + public string? Remarks { get { throw null; } init { } } + + public string? Returns { get { throw null; } init { } } + + public string? Summary { get { throw null; } init { } } + } + public sealed partial class AtsDtoPropertyInfo { + public System.Collections.Generic.IReadOnlyList? CallbackParameters { get { throw null; } init { } } + + public AtsTypeRef? CallbackReturnType { get { throw null; } init { } } + public string? Description { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + + public bool IsCallback { get { throw null; } init { } } + public bool IsOptional { get { throw null; } init { } } public required string Name { get { throw null; } init { } } @@ -202,6 +225,8 @@ public sealed partial class AtsDtoTypeInfo public string? Description { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public required string Name { get { throw null; } init { } } public required System.Collections.Generic.IReadOnlyList Properties { get { throw null; } init { } } @@ -213,17 +238,30 @@ public sealed partial class AtsEnumTypeInfo { public System.Type? ClrType { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public required string Name { get { throw null; } init { } } public required string TypeId { get { throw null; } init { } } + public System.Collections.Generic.IReadOnlyList ValueInfos { get { throw null; } init { } } + public required System.Collections.Generic.IReadOnlyList Values { get { throw null; } init { } } } + public sealed partial class AtsEnumValueInfo + { + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + + public required string Name { get { throw null; } init { } } + } + public sealed partial class AtsExportedValueInfo { public string? Description { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public required string OwningAssemblyName { get { throw null; } init { } } public required System.Collections.Generic.IReadOnlyList PathSegments { get { throw null; } init { } } @@ -233,6 +271,13 @@ public sealed partial class AtsExportedValueInfo public required System.Text.Json.Nodes.JsonNode? Value { get { throw null; } init { } } } + public sealed partial class AtsParameterDocumentationInfo + { + public required string Description { get { throw null; } init { } } + + public required string Name { get { throw null; } init { } } + } + public sealed partial class AtsParameterInfo { public System.Collections.Generic.IReadOnlyList? CallbackParameters { get { throw null; } init { } } @@ -241,6 +286,8 @@ public sealed partial class AtsParameterInfo public object? DefaultValue { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public bool IsCallback { get { throw null; } init { } } public bool IsNullable { get { throw null; } init { } } @@ -275,6 +322,8 @@ public sealed partial class AtsTypeInfo [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type? ClrType { get { throw null; } init { } } + public AtsDocumentationInfo? Documentation { get { throw null; } init { } } + public bool HasExposeMethods { get { throw null; } init { } } public bool HasExposeProperties { get { throw null; } init { } } @@ -312,6 +361,9 @@ public sealed partial class AtsTypeRef public bool IsInterface { get { throw null; } init { } } + [System.Text.Json.Serialization.JsonIgnore(Condition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault)] + public bool? IsNullable { get { throw null; } init { } } + public bool IsReadOnly { get { throw null; } init { } } public bool IsResourceBuilder { get { throw null; } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 95bf20eb97a..aee31ca53ab 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,7 @@ true true - 13.2.2 + 13.3.5 diff --git a/src/Shared/ComputeEnvironmentEndpointResolver.cs b/src/Shared/ComputeEnvironmentEndpointResolver.cs new file mode 100644 index 00000000000..23dfdcf47ce --- /dev/null +++ b/src/Shared/ComputeEnvironmentEndpointResolver.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting; + +/// +/// Helper for resolving endpoint references that point at a resource deployed to a different +/// compute environment than the one currently generating deployment artifacts. +/// +/// +/// Compute environment publishers (App Service, Azure Container Apps, Kubernetes) resolve +/// endpoint references against their own local endpoint map. When a resource references an +/// endpoint owned by a resource deployed to a different compute environment (for example a +/// Foundry hosted agent deployed to an AzureCognitiveServicesProjectResource), the +/// endpoint is not present in the local map and the lookup fails. In that case the owning +/// compute environment knows how to express the endpoint property, so we delegate to it. +/// This mirrors the inverse-direction logic used by Foundry hosted agents when they reference +/// endpoints owned by App Service/ACA/Kubernetes resources. +/// +internal static class ComputeEnvironmentEndpointResolver +{ + /// + /// Attempts to produce a for an endpoint's URL by + /// delegating to the compute environment that owns the endpoint's resource, when that + /// environment is different from the publisher's current compute environment(s). + /// + /// The endpoint reference to resolve. + /// + /// The compute environment(s) the current publisher is generating artifacts for. When the + /// endpoint's owning resource deploys to one of these, resolution is left to the local + /// endpoint map and this method returns . + /// + /// + /// When this method returns , contains the delegated reference expression. + /// + /// + /// if the endpoint is owned by a different compute environment and a + /// delegated expression was produced; otherwise . + /// + public static bool TryGetCrossEnvironmentEndpointExpression( + EndpointReference endpointReference, + IReadOnlyList currentComputeEnvironments, + [NotNullWhen(true)] out ReferenceExpression? expression) + { + ArgumentNullException.ThrowIfNull(endpointReference); + + return TryGetCrossEnvironmentEndpointExpression( + endpointReference.Property(EndpointProperty.Url), + currentComputeEnvironments, + out expression); + } + + /// + /// Attempts to produce a for an endpoint property by + /// delegating to the compute environment that owns the endpoint's resource, when that + /// environment is different from the publisher's current compute environment(s). + /// + /// The endpoint reference expression to resolve. + /// + /// The compute environment(s) the current publisher is generating artifacts for. When the + /// endpoint's owning resource deploys to one of these, resolution is left to the local + /// endpoint map and this method returns . + /// + /// + /// When this method returns , contains the delegated reference expression. + /// + /// + /// if the endpoint is owned by a different compute environment and a + /// delegated expression was produced; otherwise . + /// + public static bool TryGetCrossEnvironmentEndpointExpression( + EndpointReferenceExpression endpointReferenceExpression, + IReadOnlyList currentComputeEnvironments, + [NotNullWhen(true)] out ReferenceExpression? expression) + { + ArgumentNullException.ThrowIfNull(endpointReferenceExpression); + ArgumentNullException.ThrowIfNull(currentComputeEnvironments); + + expression = null; + + var owningResource = endpointReferenceExpression.Endpoint.Resource; + + // Resolve the compute environment the owning resource deploys to. A plain resource that is + // not deployed anywhere has none, so there is nothing to delegate to and the local lookup + // handles it. + if (!TryGetEffectiveComputeEnvironment(owningResource, out var owningComputeEnvironment)) + { + return false; + } + + // If the owning resource deploys to one of the current publisher's compute environments, the + // endpoint lives in the local endpoint map. Leave resolution to the existing local lookup so + // generated artifacts (bicep parameters, helm values, etc.) are unchanged. + foreach (var current in currentComputeEnvironments) + { + if (ReferenceEquals(current, owningComputeEnvironment)) + { + return false; + } + } + +#pragma warning disable ASPIRECOMPUTE002 // Experimental: compute environment endpoint expression + expression = owningComputeEnvironment.GetEndpointPropertyExpression(endpointReferenceExpression); +#pragma warning restore ASPIRECOMPUTE002 + + return true; + } + + /// + /// Resolves the compute environment that a resource is deployed to. A resource may be bound to a + /// compute environment explicitly (via ) or + /// implicitly through its deployment target. + /// + /// The resource whose compute environment should be resolved. + /// + /// When this method returns , contains the owning compute environment. + /// + /// + /// if a compute environment was resolved; otherwise . + /// + public static bool TryGetEffectiveComputeEnvironment( + IResource resource, + [NotNullWhen(true)] out IComputeEnvironmentResource? computeEnvironment) + { + ArgumentNullException.ThrowIfNull(resource); + + // Prefer an explicit compute environment binding, then fall back to the deployment target's + // compute environment. This matches how endpoint references are resolved elsewhere + // (Azure Front Door origins, Foundry hosted agents) so all call sites agree on "where is + // this resource deployed". + computeEnvironment = resource.GetComputeEnvironment() ?? resource.GetDeploymentTargetAnnotation()?.ComputeEnvironment; + return computeEnvironment is not null; + } +} diff --git a/src/Shared/ProcessSignaler.cs b/src/Shared/ProcessSignaler.cs index 0e3e7cb7ce6..bda6a79ad2c 100644 --- a/src/Shared/ProcessSignaler.cs +++ b/src/Shared/ProcessSignaler.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; @@ -49,20 +50,31 @@ public static void ForceKill(int pid, DateTimeOffset? expectedStartTime, ILogger public static Process? TryGetRunningProcess(int pid, DateTimeOffset? expectedStartTime, ILogger logger) { + Process? process = null; try { - var process = Process.GetProcessById(pid); - if (expectedStartTime is not null && !AreClose(expectedStartTime, process.StartTime)) + process = Process.GetProcessById(pid); + if (process.HasExited) { - logger.LogDebug("Process {Pid} start time {ProcessStartTime} does not match expected start time {ExpectedStartTime}", pid, process.StartTime, expectedStartTime); process.Dispose(); - return null; // Do not return processes that do not match the expected start time + return null; } - if (process.HasExited) + if (expectedStartTime is not null) { - process.Dispose(); - return null; + var processStartTime = process.StartTime; + if (!AreClose(expectedStartTime, processStartTime)) + { + logger.LogDebug("Process {Pid} start time {ProcessStartTime} does not match expected start time {ExpectedStartTime}", pid, processStartTime, expectedStartTime); + process.Dispose(); + return null; // Do not return processes that do not match the expected start time + } + + if (process.HasExited) + { + process.Dispose(); + return null; + } } return process; @@ -70,11 +82,23 @@ public static void ForceKill(int pid, DateTimeOffset? expectedStartTime, ILogger catch (ArgumentException) { // Process doesn't exist - already terminated. + process?.Dispose(); return null; } catch (InvalidOperationException) { // Process has already exited. + process?.Dispose(); + return null; + } + catch (Win32Exception ex) + { + // Process inspection can race with process exit. On macOS, StartTime can throw: + // Win32Exception (3): Unable to retrieve the specified information about the process or thread. It may have exited or may be privileged. + // If we cannot inspect the process enough to prove it is the expected target, do + // not signal or kill it. + logger.LogDebug(ex, "Could not inspect process {Pid}. Treating it as not running.", pid); + process?.Dispose(); return null; } } diff --git a/src/Shared/StringComparers.cs b/src/Shared/StringComparers.cs index 41ef4ed1f9a..f57c5542866 100644 --- a/src/Shared/StringComparers.cs +++ b/src/Shared/StringComparers.cs @@ -32,7 +32,7 @@ internal static class StringComparers public static StringComparer CommandName => StringComparer.Ordinal; public static StringComparer CliInputOrOutput => StringComparer.Ordinal; public static StringComparer InteractionInputName => StringComparer.OrdinalIgnoreCase; - public static StringComparer NetworkID => StringComparer.Ordinal; + public static StringComparer NetworkId => StringComparer.Ordinal; public static StringComparer NuGetPackageId => StringComparer.OrdinalIgnoreCase; public static StringComparer FullTextSearch => StringComparer.OrdinalIgnoreCase; public static StringComparer ChannelName => StringComparer.OrdinalIgnoreCase; @@ -65,7 +65,7 @@ internal static class StringComparisons public static StringComparison CommandName => StringComparison.Ordinal; public static StringComparison CliInputOrOutput => StringComparison.Ordinal; public static StringComparison InteractionInputName => StringComparison.OrdinalIgnoreCase; - public static StringComparison NetworkID => StringComparison.Ordinal; + public static StringComparison NetworkId => StringComparison.Ordinal; public static StringComparison NuGetPackageId => StringComparison.OrdinalIgnoreCase; public static StringComparison FullTextSearch => StringComparison.OrdinalIgnoreCase; public static StringComparison ChannelName => StringComparison.OrdinalIgnoreCase; diff --git a/tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs b/tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs index e83ba1aeaed..fb4a9ccbb45 100644 --- a/tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs +++ b/tests/Aspire.Acquisition.Tests/Scripts/VerifyCliArchivePowerShellTests.cs @@ -32,7 +32,6 @@ public async Task VerifyCliArchive_AcceptsCleanPerRidArchive() result.EnsureSuccessful(); Assert.Contains("aspire mock v1.0", result.Output); - Assert.Contains("'aspire new aspire-starter' created project successfully", result.Output); Assert.Contains("linux-* archive correctly omits the install-route sidecar", result.Output); Assert.Contains("All verification checks passed", result.Output); } diff --git a/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs index 88978c21244..50aa9596927 100644 --- a/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AddCommandTests.cs @@ -354,8 +354,29 @@ public async Task IntegrationSearchCommandFormatJsonUsesFuzzyIntegrationMatching } [Fact] - public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredChannel() + public async Task IntegrationSearchCommandFormatJsonWithTypeScriptAppHostPinnedToChannelAlsoSearchesImplicitChannel() { + // Regression for https://github.com/microsoft/aspire/issues/17724 + https://github.com/microsoft/aspire/issues/17725. + // + // Layer 1 (latent bug, born 2026-01-13 in PR #13705): IntegrationPackageSearchService used to + // narrow the channel set to whatever `configuredChannel` resolved to whenever the apphost was + // non-C#. This dropped the implicit channel and any other channels from discovery. + // Layer 2 (PR #17452, 2026-05-26): `aspire init` started writing `"channel": ""` into + // the scaffolded aspire.config.json for polyglot apphosts. This activated the Layer 1 bug for + // every newly-initialized TS apphost in 13.4. + // + // Fix: IntegrationPackageSearchService no longer narrows. The full channel set (implicit + + // pinned channel + any hives) is searched. + // + // This test pins the TS apphost to the "daily" channel via aspire.config.json. Pre-fix only the + // daily channel was searched and Redis 2.0.0 (daily) was the only result. Post-fix the implicit + // channel is ALSO searched, and SelectPreferredIntegrationPackage prefers the implicit channel + // when versions collide on Id, so Redis 1.0.0 (implicit) wins the dedupe. + // + // The structural guarantee asserted below — both `implicitHits` AND `dailyHits` being > 0 — is + // what defends against a regression that drops either channel from the search. Asserting only + // on the resulting Redis version is insufficient because implicit-only and daily-only searches + // both happen to produce a single result. var rawJson = string.Empty; var testInteractionService = new TestInteractionService { @@ -371,13 +392,25 @@ public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredCha } """); + // Track per-channel invocation. IntegrationPackageSearchService walks channels via + // Parallel.ForEachAsync, so callbacks may run concurrently; Interlocked guards that. + var implicitHits = 0; + var dailyHits = 0; var implicitCache = new FakeNuGetPackageCache { - GetIntegrationPackagesAsyncCallback = (_, _, _, _) => Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]) + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref implicitHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]); + } }; var dailyCache = new FakeNuGetPackageCache { - GetIntegrationPackagesAsyncCallback = (_, _, _, _) => Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "2.0.0")]) + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref dailyHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "2.0.0")]); + } }; var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => @@ -401,14 +434,22 @@ public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredCha Assert.Equal(CliExitCodes.Success, exitCode); + // Structural regression signal: BOTH channels must have been searched. + Assert.True(implicitHits > 0, "Implicit channel was not queried — discovery is dropping it."); + Assert.True(dailyHits > 0, "Daily channel was not queried — pinned channel is being dropped from discovery."); + + // Implicit channel result wins the dedupe (SelectPreferredIntegrationPackage prefers implicit). var integration = Assert.Single(ReadIntegrationResults(rawJson)); Assert.Equal("Aspire.Hosting.Redis", integration.Package); - Assert.Equal("2.0.0", integration.Version); + Assert.Equal("1.0.0", integration.Version); } [Fact] - public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredStagingChannelUnderStableCli() + public async Task IntegrationSearchCommandFormatJsonWithTypeScriptAppHostPinnedToStagingChannelAlsoSearchesImplicitChannel() { + // See companion test above for the full Layer 1 / Layer 2 regression story. + // This variant covers the staging-channel pin: a stable-shaped CLI dogfooder whose apphost + // was init'd by PR #17452 and now has `"channel": "staging"` written into aspire.config.json. var rawJson = string.Empty; var testInteractionService = new TestInteractionService { @@ -424,13 +465,23 @@ public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredSta } """); + var implicitHits = 0; + var stagingHits = 0; var implicitCache = new FakeNuGetPackageCache { - GetIntegrationPackagesAsyncCallback = (_, _, _, _) => Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]) + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref implicitHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]); + } }; var stagingCache = new FakeNuGetPackageCache { - GetIntegrationPackagesAsyncCallback = (_, _, _, _) => Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "2.0.0")]) + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref stagingHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "2.0.0")]); + } }; var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => @@ -455,9 +506,208 @@ public async Task IntegrationSearchCommandFormatJsonWithAppHostUsesConfiguredSta Assert.Equal(CliExitCodes.Success, exitCode); + Assert.True(implicitHits > 0, "Implicit channel was not queried — discovery is dropping it."); + Assert.True(stagingHits > 0, "Staging channel was not queried — pinned channel is being dropped from discovery."); + var integration = Assert.Single(ReadIntegrationResults(rawJson)); Assert.Equal("Aspire.Hosting.Redis", integration.Package); - Assert.Equal("2.0.0", integration.Version); + Assert.Equal("1.0.0", integration.Version); + } + + [Fact] + public async Task IntegrationSearchCommandFormatJsonWithTypeScriptAppHostPinnedToStableChannelStillSurfacesPrereleaseOnlyPackages() + { + // Regression for https://github.com/microsoft/aspire/issues/17725 specifically. + // + // Aspire.Hosting.Foundry has never shipped a stable version — it only exists as prerelease. + // Pre-fix, a TS apphost with `"channel": "stable"` in aspire.config.json got narrowed to the + // stable channel only. That channel is Quality.Stable, so only `prerelease: false` queries + // were issued, and Foundry never appeared in the result set. Users dogfooding the staging CLI + // (which writes `"channel": "stable"` for a stable-shaped build) could not discover Foundry. + // + // Post-fix the implicit channel (Quality.Both) is also searched, which DOES issue + // `prerelease: true` queries, and Foundry surfaces. + // + // The fake here respects the `prerelease` arg passed to GetIntegrationPackagesAsync so the + // stable channel sees Redis only, while the implicit channel sees Redis + Foundry. The + // existence of Foundry in the result is the regression signal. + var rawJson = string.Empty; + var testInteractionService = new TestInteractionService + { + DisplayRawTextCallback = text => rawJson = text + }; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts")); + File.WriteAllText(appHostFile.FullName, string.Empty); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName), """ + { + "channel": "stable" + } + """); + + // Implicit channel: Quality.Both. Returns Redis when prerelease=false, Redis+Foundry when prerelease=true. + var implicitHits = 0; + var implicitCache = new FakeNuGetPackageCache + { + GetIntegrationPackagesAsyncCallback = (_, prerelease, _, _) => + { + Interlocked.Increment(ref implicitHits); + return Task.FromResult>( + prerelease + ? [CreatePackage("Aspire.Hosting.Redis", "1.0.0"), CreatePackage("Aspire.Hosting.Foundry", "1.0.0-preview.1")] + : [CreatePackage("Aspire.Hosting.Redis", "1.0.0")]); + } + }; + // Stable channel: Quality.Stable. PackageChannel only issues prerelease=false queries against it, + // so Foundry (prerelease-only) never appears regardless of what the cache could return. + var stableHits = 0; + var stableCache = new FakeNuGetPackageCache + { + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref stableHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]); + } + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace, PackageChannelNames.Stable); + options.InteractionServiceFactory = _ => testInteractionService; + options.PackagingServiceFactory = _ => new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>([ + PackageChannel.CreateImplicitChannel(implicitCache, new TestFeatures()), + PackageChannel.CreateExplicitChannel(PackageChannelNames.Stable, PackageChannelQuality.Stable, [new PackageMapping("Aspire*", "stable")], stableCache, new TestFeatures()) + ]) + }; + }); + services.AddSingleton(new TestTypeScriptStarterProjectFactory((_, _, _) => Task.FromResult(true))); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse($"integration search foundry --apphost \"{appHostFile.FullName}\" --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + + // Both channels must be queried. The implicit channel is what surfaces Foundry (via + // prerelease=true), but the stable channel must also be searched so users who pinned to + // it don't lose stable-only packages. + Assert.True(implicitHits > 0, "Implicit channel was not queried — Foundry would not be discoverable."); + Assert.True(stableHits > 0, "Stable channel was not queried — pinned channel is being dropped from discovery."); + + var integration = Assert.Single(ReadIntegrationResults(rawJson)); + Assert.Equal("Aspire.Hosting.Foundry", integration.Package); + Assert.Equal("1.0.0-preview.1", integration.Version); + } + + [Theory] + [InlineData(null, false)] // No persisted channel — only implicit is searched, the explicit channel is excluded. + [InlineData("\"daily\"", true)] // Persisted daily channel — implicit AND daily are searched. + [InlineData("\"staging\"", true)] // Persisted staging channel — implicit AND staging are searched. Proves the gate is channel-name-opaque, + // so the post-fix behavior verified for "daily" applies equally to a staging-stamped release where + // `aspire new` would write `"channel": "staging"` into the polyglot apphost's aspire.config.json. + // (See IntegrationPackageSearchService.GetIntegrationPackagesWithChannelsAsync: the gate is + // `hasHives || !string.IsNullOrEmpty(configuredChannel)` — it never inspects the channel name.) + public async Task IntegrationSearchCommandTypeScriptAppHostPersistedChannelExpandsDiscoveryWithoutChangingPreferredResult(string? configFileChannelJson, bool expectExplicitChannelHit) + { + // Durable regression guard against re-introducing the Layer-1 narrowing bug. + // + // Pre-fix: aspire.config.json with `"channel"` set caused IntegrationPackageSearchService to + // narrow the channel set to that single channel, so the with-channel arm would have returned + // ONLY the daily channel's Redis (2.0.0) while the without-channel arm returned Redis 1.0.0. + // Post-fix two things hold simultaneously: + // (a) Both arms yield the SAME preferred Redis to the user (1.0.0, the implicit channel + // wins via SelectPreferredIntegrationPackage) — because the pin no longer overrides + // what the user sees as the top-ranked result. + // (b) The with-channel arm ALSO queries the pinned (daily) channel; the without-channel arm + // does not — because the explicit channel set is gated on `hasHives || !empty(configuredChannel)`. + // + // Both halves matter. (a) alone would pass for an implementation that incorrectly narrowed + // to implicit-only when a channel was pinned (a different regression than the original bug + // but still wrong — it would mean users who pin to `daily` lose access to packages that only + // exist on the daily feed). (b) is the new structural guarantee on top of (a). + var rawJson = string.Empty; + var testInteractionService = new TestInteractionService + { + DisplayRawTextCallback = text => rawJson = text + }; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts")); + File.WriteAllText(appHostFile.FullName, string.Empty); + if (configFileChannelJson is not null) + { + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName), $$""" + { + "channel": {{configFileChannelJson}} + } + """); + } + + var implicitHits = 0; + var dailyHits = 0; + var implicitCache = new FakeNuGetPackageCache + { + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref implicitHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "1.0.0")]); + } + }; + var dailyCache = new FakeNuGetPackageCache + { + GetIntegrationPackagesAsyncCallback = (_, _, _, _) => + { + Interlocked.Increment(ref dailyHits); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Redis", "2.0.0")]); + } + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => testInteractionService; + options.PackagingServiceFactory = _ => new TestPackagingService + { + GetChannelsAsyncCallback = _ => Task.FromResult>([ + PackageChannel.CreateImplicitChannel(implicitCache, new TestFeatures()), + PackageChannel.CreateExplicitChannel("daily", PackageChannelQuality.Both, [new PackageMapping("Aspire*", "daily")], dailyCache, new TestFeatures()) + ]) + }; + }); + services.AddSingleton(new TestTypeScriptStarterProjectFactory((_, _, _) => Task.FromResult(true))); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse($"integration search redis --apphost \"{appHostFile.FullName}\" --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + + // (a) User-visible result is identical across arms: implicit Redis 1.0.0 wins. + var integration = Assert.Single(ReadIntegrationResults(rawJson)); + Assert.Equal("Aspire.Hosting.Redis", integration.Package); + Assert.Equal("1.0.0", integration.Version); + + // (b) Per-channel search invocation differs based on whether a channel was pinned. + Assert.True(implicitHits > 0, "Implicit channel must always be searched."); + if (expectExplicitChannelHit) + { + // The explicit (daily) channel registered in the fake PackagingService gets searched + // regardless of what channel NAME the apphost pinned (the gate is channel-name-opaque — + // it only checks `!string.IsNullOrEmpty(configuredChannel)`). That's how a real CLI + // built with `AspireCliChannel=staging` (writing `"channel": "staging"` into apphosts + // via `aspire new`) will exercise the same gate path as a CLI that pinned `"daily"`. + Assert.True(dailyHits > 0, $"With-channel arm: explicit channel must also be searched when apphost pin is non-empty (configured: {configFileChannelJson})."); + } + else + { + Assert.Equal(0, dailyHits); + } } [Fact] @@ -554,6 +804,100 @@ public async Task IntegrationSearchCommandFormatJsonWithUnpinnedAppHostUsesImpli Assert.Equal("1.0.0", integration.Version); } + [Fact] + public async Task IntegrationSearchCommandStagingStampedCliWithPinnedStagingApphostQueriesBothImplicitAndStagingChannelsAndSurfacesPrereleaseOnlyPackages() + { + // High-confidence shipping-shape regression guard for #17724 and #17725. + // + // This test simulates EXACTLY what a real CLI built and shipped as staging will do when + // the user runs `aspire add ` against a polyglot apphost that `aspire new` created: + // + // * The CLI binary is stamped `AspireCliChannel=staging` -> `IdentityChannel == "staging"`. + // This triggers the real PackagingService.GetChannelsAsync to synthesize a real staging + // channel alongside implicit + stable (no fake TestPackagingService is used here). + // * `aspire new` writes `"channel": "staging"` into aspire.config.json (see + // CliTemplateFactory.TypeScriptStarterTemplate). We mirror that here. + // * There are NO PR hives. This is a real shipped install, not a dogfood/PR build. + // + // Pre-fix (the regression introduced before 13.4): the gate narrowed the search to ONLY the + // pinned staging channel. Implicit was excluded. Prerelease-only integrations (e.g., + // Aspire.Hosting.Foundry) were invisible because the only feed queried was the staging + // feed, which doesn't surface them. The `aspire add kubernetes` regression had the same + // root cause: kubernetes was reachable via implicit (nuget.org) but invisible under the + // narrowed staging-only search. + // + // Post-fix invariants verified here: + // (i) BOTH implicit AND the synthesized staging channel are queried (cache call count + // is >= 2). Pre-fix this would have been exactly 1. + // (ii) A prerelease-only package returned by the cache only when prerelease=true (which + // is what Quality.Both channels request) is reachable to the user. + var rawJson = string.Empty; + var testInteractionService = new TestInteractionService + { + DisplayRawTextCallback = text => rawJson = text + }; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts")); + File.WriteAllText(appHostFile.FullName, string.Empty); + File.WriteAllText(Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName), """ + { + "channel": "staging" + } + """); + + var totalCacheCalls = 0; + var prereleaseRequested = 0; + var cache = new FakeNuGetPackageCache + { + GetIntegrationPackagesAsyncCallback = (_, prerelease, _, _) => + { + Interlocked.Increment(ref totalCacheCalls); + if (prerelease) + { + Interlocked.Increment(ref prereleaseRequested); + return Task.FromResult>([CreatePackage("Aspire.Hosting.Foundry", "13.4.0-rc.1")]); + } + return Task.FromResult>([]); + } + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + // Stamp the running CLI as the staging release identity. The real PackagingService + // (left un-overridden here) reads this from CliExecutionContext.IdentityChannel and + // synthesizes the staging channel automatically (see PackagingService.GetChannelsAsync + // -> stagingIdentityChannel branch). + options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace, PackageChannelNames.Staging); + options.InteractionServiceFactory = _ => testInteractionService; + options.NuGetPackageCacheFactory = _ => cache; + }); + services.AddSingleton(new TestTypeScriptStarterProjectFactory((_, _, _) => Task.FromResult(true))); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse($"integration search foundry --apphost \"{appHostFile.FullName}\" --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + + // (ii) The prerelease-only package is reachable to the user. + var integration = Assert.Single(ReadIntegrationResults(rawJson)); + Assert.Equal("Aspire.Hosting.Foundry", integration.Package); + Assert.Equal("13.4.0-rc.1", integration.Version); + + // (i) Both implicit AND staging were queried. Pre-fix narrowing would have produced exactly 1 call. + // Real PackagingService.GetChannelsAsync under IdentityChannel=Staging returns at least + // [implicit, stable, staging]; the IPSS gate now lets all of them through (hasHives=false, + // configuredChannel="staging" -> not empty -> gate evaluates true). At minimum the implicit + // and staging channels must have run, so we require >= 2 calls. Using `>= 2` rather than + // `== N` keeps the test robust to PackagingService adding additional explicit channels + // (e.g., stable) without weakening the regression guard. + Assert.True(totalCacheCalls >= 2, $"Expected >= 2 cache calls (both implicit and staging channels), got {totalCacheCalls}. Pre-fix narrowing would have produced 1 call."); + Assert.True(prereleaseRequested >= 1, $"Expected at least one channel to request prerelease=true (Quality.Both channels do); got {prereleaseRequested}."); + } + [Fact] public async Task IntegrationListCommandFormatJsonPrefersImplicitChannelWhenMultipleChannelsContainSameIntegration() { @@ -1123,6 +1467,76 @@ public async Task AddCommandInteractiveDoesNotPromptForVersionWhenSpecifiedVersi Assert.All(exactMatchQueries, query => Assert.Equal("Aspire.Hosting.Redis", query)); } + [Theory] + [InlineData("redis")] + [InlineData("Aspire.Hosting.Redis")] + public async Task AddCommandInteractiveDoesNotPromptForIntegrationWhenExactMatchIsFound(string integrationName) + { + var promptedForIntegration = false; + var promptedForVersion = false; + var selectedPackageName = string.Empty; + var selectedPackageVersion = string.Empty; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + options.AddCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + var prompter = new TestAddCommandPrompter(interactionService); + prompter.PromptForIntegrationCallback = (packages) => + { + promptedForIntegration = true; + throw new InvalidOperationException("Should not have been prompted for integration selection."); + }; + prompter.PromptForIntegrationVersionCallback = (packages) => + { + promptedForVersion = true; + return packages.Single(package => package.Package.Version == "13.2.0"); + }; + + return prompter; + }; + + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, invocationOptions, cancellationToken) => + { + return (0, [ + new NuGetPackage { Id = "Aspire.Hosting.Redis", Source = "nuget", Version = "13.3.0" }, + new NuGetPackage { Id = "Aspire.Hosting.Redis", Source = "nuget", Version = "13.2.0" } + ]); + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, invocationOptions, cancellationToken) => + { + selectedPackageName = packageName; + selectedPackageVersion = packageVersion; + return 0; + }; + + return runner; + }; + }); + + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse($"add {integrationName}"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + Assert.False(promptedForIntegration); + Assert.True(promptedForVersion); + Assert.Equal("Aspire.Hosting.Redis", selectedPackageName); + Assert.Equal("13.2.0", selectedPackageVersion); + } + [Fact] public async Task AddCommandSearchesEachPackageIdOnceWhenExactMatchFallsBackAcrossSharedChannel() { @@ -2286,6 +2700,12 @@ public async Task AddCommand_WithStartsWith_FindsMatchUsingFuzzySearch() return prompter; }; + // Fuzzy fallback only fires in interactive mode after the Layer-3 fix for #17724. + // The default test host environment is non-interactive (mirroring CI), so opt this + // fixture into the interactive path explicitly: the test asserts that an interactive + // user can still discover PostgreSQL by typing "postgre". + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + options.ProjectLocatorFactory = _ => new TestProjectLocator(); options.DotNetCliRunnerFactory = (sp) => @@ -2341,6 +2761,251 @@ public async Task AddCommand_WithStartsWith_FindsMatchUsingFuzzySearch() Assert.Equal("Aspire.Hosting.PostgreSQL", addedPackage); } + [Fact] + public async Task AddCommand_NonInteractive_NoExactMatchWithoutVersion_FailsInsteadOfFuzzyAutoPick_Regression17724() + { + // Regression for https://github.com/microsoft/aspire/issues/17724. + // + // Pre-fix: `aspire add kube --non-interactive` had no exact match for "kube" (none of the + // packages are literally named "kube"), so AddCommand fell back to fuzzy search. The fuzzy + // candidate list was then passed to GetPackageByInteractiveFlow, which in non-interactive + // mode auto-selected `distinctPackages.First()` (AddCommand.cs:368-369) and silently added + // the wrong package. In the user's report this was Aspire.Hosting.Azure because the + // companion Layer-1 bug (#17725 / IntegrationPackageSearchService narrowing) had filtered + // prerelease packages out, leaving Azure as the only fuzzy candidate. + // + // Fix: AddCommand now refuses to fall back to fuzzy search whenever the host is non-interactive + // and no exact match was found, regardless of whether --version was supplied. The error + // surfaces the new NonInteractiveRequiresExactPackageMatch resource so the user/script + // knows to supply the full package id or friendly name. + // + // This test uses the simpler C# project flow (TestDotNetCliRunner stub) because the bug is + // in AddCommand's non-interactive handling, not in package discovery — the discovery path is + // covered by the cross-language parity test above. The Aspire.Hosting.Azure and + // Aspire.Hosting.Kubernetes packages both fuzzy-match "kube"; pre-fix the first one + // (Aspire.Hosting.Azure, alphabetical) would have been silently picked. + var addPackageWasCalled = false; + var testInteractionService = new TestInteractionService(); + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment(); + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + return ( + 0, + new NuGetPackage[] + { + new() { Id = "Aspire.Hosting.Azure", Source = "nuget", Version = "9.2.0" }, + new() { Id = "Aspire.Hosting.Kubernetes", Source = "nuget", Version = "9.2.0" } + }); + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + addPackageWasCalled = true; + return 0; + }; + + return runner; + }; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add kube"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.FailedToAddPackage, exitCode); + Assert.False(addPackageWasCalled, "AddPackageAsync must not be called when there is no exact match in non-interactive mode."); + Assert.Contains(string.Format(AddCommandStrings.NonInteractiveRequiresExactPackageMatch, "kube"), testInteractionService.DisplayedErrors); + } + + [Fact] + public async Task AddCommand_NonInteractive_ExactMatchWithoutVersion_StillSucceeds() + { + // Companion regression guard for #17724: ensures the new non-interactive guard ONLY fires + // when there is no exact match. An exact match by package id (or friendly name) must still + // install successfully — this is the documented happy path for CI/scripted usage. + var addedPackage = string.Empty; + var testInteractionService = new TestInteractionService(); + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment(); + options.InteractionServiceFactory = _ => testInteractionService; + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + return ( + 0, + new NuGetPackage[] + { + new() { Id = "Aspire.Hosting.Azure", Source = "nuget", Version = "9.2.0" }, + new() { Id = "Aspire.Hosting.Kubernetes", Source = "nuget", Version = "9.2.0" } + }); + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + addedPackage = packageName; + return 0; + }; + + return runner; + }; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + // "kubernetes" is the friendly name (Aspire.Hosting.Kubernetes → friendlyName "kubernetes"), + // so this is an exact match and must succeed. + var result = command.Parse("add kubernetes"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + Assert.Equal("Aspire.Hosting.Kubernetes", addedPackage); + } + + [Fact] + public async Task AddCommand_Interactive_SingleFuzzyMatchPromptsBeforeAdding_Regression17724() + { + var promptedPackages = new List<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>(); + var addedPackage = string.Empty; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + options.AddCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + var prompter = new TestAddCommandPrompter(interactionService); + prompter.PromptForIntegrationCallback = (packages) => + { + promptedPackages.AddRange(packages); + return packages.Single(); + }; + + return prompter; + }; + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + return ( + 0, + new NuGetPackage[] + { + new() { Id = "Aspire.Hosting.Azure", Source = "nuget", Version = "9.2.0" } + }); + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + addedPackage = packageName; + return 0; + }; + + return runner; + }; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add kube"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + var promptedPackage = Assert.Single(promptedPackages); + Assert.Equal(0, exitCode); + Assert.Equal("Aspire.Hosting.Azure", promptedPackage.Package.Id); + Assert.Equal("Aspire.Hosting.Azure", addedPackage); + } + + [Fact] + public async Task AddCommand_Interactive_NoFuzzyMatchSinglePackagePromptsBeforeAdding() + { + var promptedPackages = new List<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>(); + var displayedSubtleMessage = string.Empty; + var addedPackage = string.Empty; + var testInteractionService = new TestInteractionService + { + DisplaySubtleMessageCallback = message => displayedSubtleMessage = message + }; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + options.InteractionServiceFactory = _ => testInteractionService; + options.AddCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + var prompter = new TestAddCommandPrompter(interactionService); + prompter.PromptForIntegrationCallback = (packages) => + { + promptedPackages.AddRange(packages); + return packages.Single(); + }; + + return prompter; + }; + options.ProjectLocatorFactory = _ => new TestProjectLocator(); + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, exactMatch, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + return ( + 0, + new NuGetPackage[] + { + new() { Id = "Aspire.Hosting.Redis", Source = "nuget", Version = "9.2.0" } + }); + }; + + runner.AddPackageAsyncCallback = (projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken) => + { + addedPackage = packageName; + return 0; + }; + + return runner; + }; + }); + using var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("add zzzzzzzzzz"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + var promptedPackage = Assert.Single(promptedPackages); + Assert.Equal(0, exitCode); + Assert.Equal(string.Format(AddCommandStrings.NoPackagesMatchedSearchTerm, "zzzzzzzzzz"), displayedSubtleMessage); + Assert.Equal("Aspire.Hosting.Redis", promptedPackage.Package.Id); + Assert.Equal("Aspire.Hosting.Redis", addedPackage); + } + [Fact] public async Task AddCommand_WithVersionAndNonExactPackageName_FailsInsteadOfUsingFuzzySearch() { @@ -2887,6 +3552,10 @@ public async Task AddCommand_WithTypo_FindsMatchUsingFuzzySearch() return new TestAddCommandPrompter(interactionService); }; + // Fuzzy fallback only fires in interactive mode after the Layer-3 fix for #17724; + // see companion comment on AddCommand_WithStartsWith_FindsMatchUsingFuzzySearch. + options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment(); + options.ProjectLocatorFactory = _ => new TestProjectLocator(); options.DotNetCliRunnerFactory = (sp) => diff --git a/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs index e96264aadf4..e5f42f3c184 100644 --- a/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs @@ -3,6 +3,7 @@ using Aspire.Cli.Commands; using Aspire.Cli.Tests.Utils; +using Aspire.Cli.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; @@ -84,6 +85,61 @@ public async Task CacheClear_ClearsAppHostInfoDiskCache() Assert.False(appHostInfoCacheDir.Exists); } + [Fact] + public async Task CacheClear_ClearsStagingNuGetPackagesCache() + { + // Pins that `aspire cache clear` wipes the SHA-keyed staging NuGet package caches under + // /.nugetpackages — produced by PrebuiltAppHostServer's temporary + // nuget.config for the staging channel. Without this, a wedged staging restore can only + // be recovered by manual filesystem surgery. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper); + using var provider = services.BuildServiceProvider(); + var executionContext = provider.GetRequiredService(); + + var stagingCacheRoot = new DirectoryInfo( + CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory)); + var firstBuildCache = stagingCacheRoot.CreateSubdirectory("deadbeef").CreateSubdirectory("aspire.hosting").CreateSubdirectory("13.4.0"); + var secondBuildCache = stagingCacheRoot.CreateSubdirectory("cafef00d").CreateSubdirectory("aspire.hosting").CreateSubdirectory("13.4.0"); + await File.WriteAllTextAsync(Path.Combine(firstBuildCache.FullName, "Aspire.Hosting.dll"), "fake").DefaultTimeout(); + await File.WriteAllTextAsync(Path.Combine(secondBuildCache.FullName, "Aspire.Hosting.dll"), "fake").DefaultTimeout(); + + var command = provider.GetRequiredService(); + var result = command.Parse("cache clear"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + // SHA-keyed subdirectories should be gone; the parent stays so the next staging restore + // can populate a fresh cache without recreating the .nugetpackages root. + Assert.False(Directory.Exists(Path.Combine(stagingCacheRoot.FullName, "deadbeef"))); + Assert.False(Directory.Exists(Path.Combine(stagingCacheRoot.FullName, "cafef00d"))); + } + + [Fact] + public async Task CacheClear_HandlesMissingStagingNuGetPackagesCache() + { + // Common case for fresh installs and non-staging users: the staging cache root simply + // doesn't exist yet. The command must still succeed. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper); + using var provider = services.BuildServiceProvider(); + var executionContext = provider.GetRequiredService(); + + var stagingCacheRoot = new DirectoryInfo( + CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory)); + Assert.False(stagingCacheRoot.Exists); + + var command = provider.GetRequiredService(); + var result = command.Parse("cache clear"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(CliExitCodes.Success, exitCode); + } + [Fact] public async Task CacheClear_HandlesNonExistentPackagesDirectory() { diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandChannelResolutionTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandChannelResolutionTests.cs index 8288dc321c1..80f81fa7e47 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandChannelResolutionTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandChannelResolutionTests.cs @@ -8,6 +8,7 @@ using Aspire.Cli.Templating; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; +using Aspire.Cli.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using NuGetPackage = Aspire.Shared.NuGetPackageCli; @@ -115,27 +116,38 @@ public async Task NewCommand_DoesNotConsultGlobalConfigurationServiceForChannelK /// /// Channel-resolution contract: when the running CLI's identity is a non-local channel /// (daily / staging / stable) and no --channel is passed, aspire new must - /// resolve the template version from the channel whose name matches the identity — not - /// from the Implicit (nuget.org) channel. Without this, a daily/staging CLI silently - /// resolves a stable nuget.org template while the per-project channel pin (written by - /// the template factories) still points at the channel-specific feed — yielding an - /// inconsistent project that aspire restore rejects with "Unable to find a stable - /// package". + /// resolve the channel whose name matches the identity — not the Implicit (nuget.org) + /// channel. Prerelease identities (daily/staging) further pin the template version to the + /// current CLI/SDK so the bundled server and restored Aspire packages stay on the same + /// version; the stable identity falls through to the highest shipped stable package because + /// the stable feed doesn't filter the running CLI's version out of search. /// [Theory] - [InlineData(PackageChannelNames.Daily, "13.4.0-preview.1.99999.1")] - [InlineData(PackageChannelNames.Stable, "13.5.0")] - public async Task NewCommand_NoChannelArg_ResolvesTemplateFromIdentityChannel(string identityChannel, string identityChannelVersion) + [InlineData(PackageChannelNames.Daily, "13.4.0-preview.1.99999.1", null)] + [InlineData(PackageChannelNames.Stable, "13.5.0", "13.5.0")] + public async Task NewCommand_NoChannelArg_ResolvesTemplateFromIdentityChannel(string identityChannel, string identityChannelVersion, string? expectedVersion) { var captured = await CaptureTemplateInputsAsync( identityChannel: identityChannel, channelOptionArg: null, identityChannelVersion: identityChannelVersion); - Assert.Equal(identityChannelVersion, captured.Version); + Assert.Equal(expectedVersion ?? VersionHelper.GetDefaultSdkVersion(), captured.Version); Assert.Equal(identityChannel, captured.Channel); } + [Fact] + public async Task NewCommand_NoChannelArg_DailyChannelWithoutExactCliVersion_PinsTemplateToCurrentCliVersion() + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: PackageChannelNames.Daily, + channelOptionArg: null, + identityChannelVersion: "13.5.0-preview.1.99999.1"); + + Assert.Equal(VersionHelper.GetDefaultSdkVersion(), captured.Version); + Assert.Equal(PackageChannelNames.Daily, captured.Channel); + } + /// /// PR-channel CLI is already covered by the local-build channel branch retained in /// . Pinned here so a future refactor doesn't regress the @@ -179,8 +191,8 @@ public async Task NewCommand_NoChannelArg_IdentityChannelNotRegistered_FallsBack /// /// Issue #17121 regression guard: a staging-identity CLI should have a registered /// staging channel from PackagingService.GetChannelsAsync, so aspire new - /// resolves templates from staging instead of falling back to the Implicit NuGet.org - /// channel. + /// resolves the channel from staging instead of falling back to the Implicit NuGet.org + /// channel, while keeping the template version pinned to the current CLI. /// [Fact] public async Task NewCommand_NoChannelArg_StagingIdentityWithStagingChannelRegistered_ResolvesTemplateFromStaging() @@ -190,14 +202,18 @@ public async Task NewCommand_NoChannelArg_StagingIdentityWithStagingChannelRegis channelOptionArg: null, identityChannelVersion: "13.4.0-rc.1.99999.1"); - Assert.Equal("13.4.0-rc.1.99999.1", captured.Version); + Assert.Equal(VersionHelper.GetDefaultSdkVersion(), captured.Version); Assert.Equal(PackageChannelNames.Staging, captured.Channel); } /// - /// Explicit --channel must always override the running CLI's identity channel — - /// so a developer on a daily CLI can still scaffold a stable-channel project for - /// reproduction or migration testing. + /// Explicit --channel stable must always override the running CLI's identity channel — + /// so a developer on a daily CLI can still scaffold a stable-channel project for reproduction + /// or migration testing. When the CLI's own version is not published on the stable feed + /// (a daily-shape or PR-shape build), the resolver falls through to the highest shipped stable + /// package rather than forcing the unpublishable CLI version into the generated apphost. + /// Pinning to the current CLI version is reserved for prerelease channels — see + /// . /// [Fact] public async Task NewCommand_ExplicitChannelArg_OverridesIdentityChannel() @@ -207,17 +223,170 @@ public async Task NewCommand_ExplicitChannelArg_OverridesIdentityChannel() channelOptionArg: PackageChannelNames.Stable, identityChannelVersion: "13.4.0-preview.1.99999.1"); - Assert.Equal("13.5.0", captured.Version); // stable channel version + // Highest shipped stable from the stable channel feed in the test fixture. + Assert.Equal("13.5.0", captured.Version); + Assert.Equal(PackageChannelNames.Stable, captured.Channel); + } + + /// + /// A shipped CLI must prefer its own SDK/template version from an explicitly selected + /// non-local channel instead of floating to a newer daily/staging package from the same feed. + /// + [Theory] + [InlineData(PackageChannelNames.Daily)] + [InlineData(PackageChannelNames.Staging)] + public async Task NewCommand_ExplicitPrereleaseChannel_PrefersCurrentCliVersionWhenAvailable(string channelName) + { + var cliVersion = VersionHelper.GetDefaultSdkVersion(); + + var captured = await CaptureTemplateInputsAsync( + identityChannel: channelName, + channelOptionArg: channelName, + identityChannelVersion: cliVersion, + identityChannelVersions: ["99.0.0-preview.1", cliVersion]); + + Assert.Equal(cliVersion, captured.Version); + Assert.Equal(channelName, captured.Channel); + } + + /// + /// SmokeTest regression guard: when a non-stable-shape CLI (PR build, daily-shape preview, etc.) + /// is invoked with --channel stable, the resolver must NOT pin the template to the + /// running CLI's version. The CLI's own version is not published on the stable feed, so a + /// generated apphost.cs referencing it would fail NuGet restore. The fallback that pins + /// to the current CLI version is reserved for prerelease channels (daily, staging) where the + /// feed search filter is what hides the otherwise-restorable shipped stable package. + /// + [Theory] + [InlineData("13.4.0-preview.1.99999.1")] // daily-shape CLI + [InlineData("13.4.0-pr.17573.gabc1234")] // PR-shape CLI (e2e SmokeTests scenario) + public async Task NewCommand_ExplicitStableChannel_NonStableCliVersion_FallsBackToHighestShippedStable(string identityChannelVersion) + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: PackageChannelNames.Daily, + channelOptionArg: PackageChannelNames.Stable, + identityChannelVersion: identityChannelVersion); + + Assert.Equal("13.5.0", captured.Version); + Assert.Equal(PackageChannelNames.Stable, captured.Channel); + } + + /// + /// Issue: aspire new aspire-starter (and aspire-starter-csharp-typescript) + /// run through the path, which delegates template + /// package resolution to TemplateNuGetConfigService.ResolveTemplatePackageAsync + /// using TemplateInputs.Channel as the RequestedChannel. When no + /// --channel is supplied, must forward the running CLI's + /// through inputs.Channel so the + /// DotNet template path searches the identity-matching feed for + /// Aspire.ProjectTemplates — symmetrical to the CLI-runtime path + /// (). + /// + /// Without this forwarding, a daily / staging / release-branch CLI silently resolves the + /// templates package from the Implicit (nuget.org) channel — for example, on a 13.4 + /// CLI it fails to discover the matching 13.4.0 template and falls back to the + /// latest stable on nuget.org. Passing --channel explicitly works because that + /// value is already forwarded into inputs.Channel. + /// + /// + /// All four shipping identity shapes are exercised — PR (developer dogfood build + /// against a hive), daily (nightly dnceng feed), staging (release-branch dnceng feed), + /// and stable (nuget.org via the explicit Stable channel registration). + /// + /// + [Theory] + [InlineData("pr-99999", "13.4.0-pr.99999.gabc123")] + [InlineData(PackageChannelNames.Daily, "13.4.0-preview.1.99999.1")] + [InlineData(PackageChannelNames.Staging, "13.4.0-rc.1.99999.1")] + [InlineData(PackageChannelNames.Stable, "13.5.0")] + public async Task NewCommand_DotNetRuntimeTemplate_NoChannelArg_ForwardsIdentityChannelToInputs(string identityChannel, string identityChannelVersion) + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: identityChannel, + channelOptionArg: null, + identityChannelVersion: identityChannelVersion, + runtime: TemplateRuntime.DotNet); + + // DotNet-runtime templates resolve the template package version themselves inside + // DotNetTemplateFactory.ApplyTemplateAsync — NewCommand does not populate inputs.Version + // for this runtime — so only inputs.Channel is asserted here. + Assert.Equal(identityChannel, captured.Channel); + } + + /// + /// Defensive: when the identity channel is something that isn't a registered channel + /// (typo, future addition, locally-built CLI without the local hive installed, etc.), + /// must NOT blindly forward the unrecognized identity into + /// inputs.Channel — doing so would make the DotNet template path throw + /// ChannelNotFoundException inside TemplateNuGetConfigService.ResolveTemplatePackageAsync. + /// Instead, leave inputs.Channel as null so the resolver consults the Implicit + /// (nuget.org) channel and the new project inherits the user's ambient NuGet configuration. + /// Mirrors the CLI-runtime contract pinned by + /// . + /// + [Fact] + public async Task NewCommand_DotNetRuntimeTemplate_NoChannelArg_IdentityChannelNotRegistered_FallsBackToNull() + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: "stalbe", // intentional typo: not registered as a channel + channelOptionArg: null, + identityChannelVersion: null, + runtime: TemplateRuntime.DotNet); + + // Implicit-fallback case: inputs.Channel stays null so DotNetTemplateFactory does not + // pin a per-project channel and the new project uses the ambient NuGet configuration. + Assert.Null(captured.Channel); + } + + /// + /// Explicit --channel on a DotNet-runtime template (aspire-starter family) must + /// still flow through inputs.Channel verbatim and override the running CLI's + /// identity. Pinned here so the identity-channel forwarding fix doesn't accidentally + /// clobber an explicit user choice (e.g. a daily CLI scaffolding a stable-channel + /// project for migration testing). + /// + [Fact] + public async Task NewCommand_DotNetRuntimeTemplate_ExplicitChannelArg_OverridesIdentityChannel() + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: PackageChannelNames.Daily, + channelOptionArg: PackageChannelNames.Stable, + identityChannelVersion: "13.4.0-preview.1.99999.1", + runtime: TemplateRuntime.DotNet); + Assert.Equal(PackageChannelNames.Stable, captured.Channel); } /// - /// Invokes with a fake CLI-runtime template that captures the - /// handed to it. This is the contract surface the four - /// shipping CLI templates (TS/Python/Go starter + empty AppHost) all read from. Its - /// Version reflects which channel won template-version resolution; its - /// Channel reflects what the template factories will persist into the new - /// project's aspire.config.json. + /// --version on a DotNet-runtime template must still flow through to + /// inputs.Version AND identity-channel forwarding must still populate + /// inputs.Channel. The version pin tells ResolveTemplatePackageAsync which + /// package to pick, but the channel pin still selects the feed that package is fetched + /// from and the per-project NuGet.config mappings the generated project will use. + /// Without both, a daily CLI passing --version 13.4.0-preview.1.99999.1 would + /// resolve the version against nuget.org (where prerelease daily builds aren't published) + /// and fail. + /// + [Fact] + public async Task NewCommand_DotNetRuntimeTemplate_VersionOverride_StillForwardsIdentityChannel() + { + var captured = await CaptureTemplateInputsAsync( + identityChannel: PackageChannelNames.Daily, + channelOptionArg: null, + identityChannelVersion: "13.4.0-preview.1.99999.1", + runtime: TemplateRuntime.DotNet, + versionOptionArg: "13.4.0-preview.1.99999.1"); + + Assert.Equal("13.4.0-preview.1.99999.1", captured.Version); + Assert.Equal(PackageChannelNames.Daily, captured.Channel); + } + + /// + /// Invokes with a fake template that captures the + /// handed to it. Its Version reflects which channel + /// won template-version resolution; its Channel reflects what the template + /// factories will persist into the new project's aspire.config.json (or use as + /// the RequestedChannel for DotNet-runtime templates). /// /// Identity baked into the CLI under test. /// Value passed via --channel, or null to omit the flag. @@ -225,22 +394,35 @@ public async Task NewCommand_ExplicitChannelArg_OverridesIdentityChannel() /// Version returned by the channel whose name matches , /// or null when that channel is not registered. /// + /// Optional list of versions exposed by the identity channel. + /// Template runtime kind. CLI runtime drives ResolveCliTemplateVersionAsync; DotNet runtime mirrors aspire-starter. private async Task CaptureTemplateInputsAsync( string identityChannel, string? channelOptionArg, - string? identityChannelVersion) + string? identityChannelVersion, + IEnumerable? identityChannelVersions = null, + TemplateRuntime runtime = TemplateRuntime.Cli, + string? versionOptionArg = null) { using var workspace = TemporaryWorkspace.Create(outputHelper); var capturedInputs = new CapturedTemplateInputs(); - // A fake CLI-runtime template that intercepts the inputs and returns success - // without invoking the heavyweight template scaffolding pipeline (RPC, codegen, - // bundled NuGet restore). The template is registered via a fake ITemplateProvider - // injected through CliServiceCollectionTestOptions.TemplateProviderFactory. + // A fake template that intercepts the inputs and returns success without invoking + // the heavyweight template scaffolding pipeline (RPC, codegen, bundled NuGet restore). + // The template is registered via a fake ITemplateProvider injected through + // CliServiceCollectionTestOptions.TemplateProviderFactory. + // + // The runtime kind switches which code path inside NewCommand.ExecuteAsync produces + // inputs.Channel: TemplateRuntime.Cli walks ResolveCliTemplateVersionAsync (which + // owns the identity-channel fallback for the CLI starters), while TemplateRuntime.DotNet + // mirrors the path used by aspire-starter / aspire-starter-csharp-typescript, which + // delegate version/feed selection to DotNetTemplateFactory → TemplateNuGetConfigService. + // NewCommand is responsible for passing the right channel into inputs.Channel in both + // cases, so this helper can exercise both with a single shape. var fakeTemplate = new CallbackTemplate( - name: "fake-cli-template", - description: "Fake CLI-runtime template for channel-resolution tests", + name: "fake-template", + description: "Fake template for channel-resolution tests", pathDeriverCallback: (ctx, projectName) => Path.Combine(ctx.WorkingDirectory.FullName, projectName), applyOptionsCallback: _ => { }, applyTemplateCallback: (_, inputs, _, _) => @@ -251,7 +433,7 @@ private async Task CaptureTemplateInputsAsync( Directory.CreateDirectory(outputPath); return Task.FromResult(new TemplateResult(CliExitCodes.Success, outputPath)); }, - runtime: TemplateRuntime.Cli, + runtime: runtime, languageId: KnownLanguageId.TypeScript); var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => @@ -260,14 +442,15 @@ private async Task CaptureTemplateInputsAsync( options.TemplateProviderFactory = _ => new SingleTemplateProvider(fakeTemplate); - options.PackagingServiceFactory = _ => BuildPackagingService(identityChannel, identityChannelVersion); + options.PackagingServiceFactory = _ => BuildPackagingService(identityChannel, identityChannelVersion, identityChannelVersions); }); using var serviceProvider = services.BuildServiceProvider(); var newCommand = serviceProvider.GetRequiredService(); var channelArg = string.IsNullOrEmpty(channelOptionArg) ? "" : $" --channel {channelOptionArg}"; - var parseResult = newCommand.Parse($"new fake-cli-template --name TestApp --output ./captured{channelArg}"); + var versionArg = string.IsNullOrEmpty(versionOptionArg) ? "" : $" --version {versionOptionArg}"; + var parseResult = newCommand.Parse($"new fake-template --name TestApp --output ./captured{channelArg}{versionArg}"); var exitCode = await parseResult.InvokeAsync().DefaultTimeout(); Assert.Equal(CliExitCodes.Success, exitCode); @@ -280,8 +463,14 @@ private async Task CaptureTemplateInputsAsync( /// pr-* explicit channels), but with deterministic per-channel template versions so /// tests can identify which channel won resolution. /// - private static IPackagingService BuildPackagingService(string identityChannel, string? identityChannelVersion) + private static IPackagingService BuildPackagingService( + string identityChannel, + string? identityChannelVersion, + IEnumerable? identityChannelVersions) { + var identityVersions = identityChannelVersions?.ToArray() + ?? (identityChannelVersion is null ? [] : [identityChannelVersion]); + // Implicit channel always returns the stable token so a "fell-through to Implicit" // outcome is distinguishable from an identity-channel pickup. var implicitCache = new FakeNuGetPackageCache @@ -313,7 +502,7 @@ [new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index. // Register a non-stable explicit channel matching the identity, when the test // scenario calls for it. Deliberately omitted in the "identity not registered" // case so fallback to Implicit can be observed. - var isDailyOrStaging = identityChannelVersion is not null && + var isDailyOrStaging = identityVersions.Length > 0 && !string.Equals(identityChannel, PackageChannelNames.Stable, StringComparison.OrdinalIgnoreCase) && !identityChannel.StartsWith("pr-", StringComparison.OrdinalIgnoreCase); if (isDailyOrStaging) @@ -322,7 +511,7 @@ [new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index. { GetTemplatePackagesAsyncCallback = (_, _, _, _) => Task.FromResult>( - [new NuGetPackage { Id = "Aspire.ProjectTemplates", Source = "nuget", Version = identityChannelVersion! }]) + identityVersions.Select(version => new NuGetPackage { Id = "Aspire.ProjectTemplates", Source = "nuget", Version = version })) }; channels.Add(PackageChannel.CreateExplicitChannel( identityChannel, diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index b2398d5d226..633bdd9ee82 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1811,7 +1811,7 @@ public async Task NewCommandWithTypeScriptStarterGeneratesSdkArtifacts() Assert.Equal(CliExitCodes.Success, exitCode); Assert.True(buildAndGenerateCalled); Assert.Equal("daily", channelSeenByProject); - Assert.Equal("9.2.0", sdkVersionSeenByProject); + Assert.Equal(VersionHelper.GetDefaultSdkVersion(), sdkVersionSeenByProject); Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "output", LanguageInfo.GeneratedFolderName, "aspire.mts"))); } diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 55f35c348f3..2681adcae1f 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -2033,6 +2033,17 @@ public async Task UpdateCommand_WhenStagingIdentityRegistersChannel_UsesStagingF { options.CliExecutionContextFactory = _ => workspace.CreateExecutionContext(identityChannel: PackageChannelNames.Staging); + // A real staging build always bakes the build's commit hash into its + // AssemblyInformationalVersion (e.g. "13.4.0-preview.1.26280.6+"), and the staging + // identity now routes to that build's SHA-specific darc-pub-microsoft-aspire- feed + // regardless of version shape. The test host assembly has no + metadata, so the + // feed could not be derived and the staging channel would never be synthesized. Provide a + // stamped informational version override so the derivation matches a real staging build. + options.ConfigurationCallback += config => + { + config[PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+2574ef57e97fc393aff67592fd442afca6a6d02f"; + }; + options.ProjectLocatorFactory = _ => new TestProjectLocator() { UseOrFindAppHostProjectFileAsyncCallback = (projectFile, _, _) => diff --git a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs index 5b25a742709..405d4ac9ae8 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs @@ -8,6 +8,7 @@ using Aspire.Cli.Tests.Utils; using Aspire.Cli.Utils; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System.Xml.Linq; @@ -65,7 +66,7 @@ public async Task GetChannelsAsync_WhenIdentityChannelIsStaging_IncludesStagingC [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json" }) .Build(); - var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance, isStableShapedCliVersion: () => false); var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); @@ -82,6 +83,508 @@ public async Task GetChannelsAsync_WhenIdentityChannelIsStaging_IncludesStagingC Assert.Equal(PackageChannelQuality.Both, stagingChannel.Quality); } + [Fact] + public async Task GetChannelsAsync_WhenIdentityChannelIsStagingOnStableShapedCli_DefaultsToStableQuality() + { + // Regression test for https://github.com/microsoft/aspire/issues/17527: during release + // stabilization the staging CLI ships with a stable-shaped version (e.g. "13.4.0"). The + // shared dotnet9 daily feed only carries prerelease-tagged 13.4.0-preview.* packages, + // so a stabilizing staging CLI must route Aspire.* to the SHA-derived darc-pub-aspire- + // feed instead — which requires defaulting the synthesized staging channel quality to + // Stable (so useSharedFeed in CreateStagingChannel resolves false). No overrideStagingFeed + // is set: the injected informational version makes the darc derivation deterministic so the + // test exercises (and asserts) the real SHA-feed routing rather than an override crutch. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Staging); + + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + new ConfigurationBuilder().Build(), + NullLogger.Instance, + isStableShapedCliVersion: () => true, + cliInformationalVersionProvider: () => "13.4.0+abcdef1234567890abcdef1234567890abcdef12"); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); + Assert.Equal(PackageChannelQuality.Stable, stagingChannel.Quality); + + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + aspireMapping.Source); + } + + [Fact] + public async Task GetChannelsAsync_WhenIdentityChannelIsStagingPrereleaseShaped_RoutesAspirePackagesToDarcFeed() + { + // Reproduces the C# vs polyglot divergence: a staging-identity CLI with a prerelease-shaped + // version (e.g. "13.4.0-preview.1.26280.6") is still an officially published release-branch + // build, so Aspire.* must resolve from its own SHA-specific darc-pub-microsoft-aspire- + // feed — NOT the shared dnceng/dotnet9 daily feed (which only carries main-branch daily + // packages). Before the fix, useSharedFeed was derived from the version shape (Both quality -> + // shared daily feed), which is what broke `aspire add` for TypeScript apphosts while C# + // apphosts (with the darc feed baked into nuget.config) resolved correctly. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Staging); + + // No overrideStagingFeed configured, so the real darc-vs-shared-daily routing is exercised. + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + new ConfigurationBuilder().Build(), + NullLogger.Instance, + isStableShapedCliVersion: () => false, + cliInformationalVersionProvider: () => "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12"); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); + Assert.Equal(PackageChannelQuality.Both, stagingChannel.Quality); + + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + aspireMapping.Source); + Assert.DoesNotContain("dotnet9", aspireMapping.Source); + + // The darc feed needs an isolated global packages folder, and it carries exactly the build's + // matching packages, so no CLI-version pin is applied. + Assert.True(stagingChannel.ConfigureGlobalPackagesFolder); + Assert.Null(stagingChannel.PinnedVersion); + } + + [Fact] + public async Task GetChannelsAsync_WhenIdentityChannelIsStagingStableShaped_RoutesAspirePackagesToDarcFeed() + { + // Regression guard for https://github.com/microsoft/aspire/issues/17527: a stable-shaped + // staging CLI ("13.4.0") must resolve Aspire.* from its SHA-specific darc feed with Stable + // quality (version filtering). The fix keeps this behavior while also covering the + // prerelease-shaped case above. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Staging); + + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + new ConfigurationBuilder().Build(), + NullLogger.Instance, + isStableShapedCliVersion: () => true, + cliInformationalVersionProvider: () => "13.4.0+abcdef1234567890abcdef1234567890abcdef12"); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); + Assert.Equal(PackageChannelQuality.Stable, stagingChannel.Quality); + + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + aspireMapping.Source); + + // Same darc-feed invariants as the prerelease-shaped case: isolated global packages folder + // and no CLI-version pin (the SHA feed already carries exactly the build's packages). + Assert.True(stagingChannel.ConfigureGlobalPackagesFolder); + Assert.Null(stagingChannel.PinnedVersion); + } + + [Fact] + public async Task GetChannelsAsync_WhenIdentityChannelIsStagingWithOverrideFeed_UsesOverrideFeed() + { + // An explicit overrideStagingFeed always wins over identity-based darc derivation. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Staging); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/nuget/v3/index.json" + }) + .Build(); + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + configuration, + NullLogger.Instance, + isStableShapedCliVersion: () => false, + cliInformationalVersionProvider: () => "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12"); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = channels.First(c => c.Name == PackageChannelNames.Staging); + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal("https://example.com/nuget/v3/index.json", aspireMapping.Source); + } + + [Fact] + public async Task GetChannelsAsync_WhenStagingIdentityCannotDeriveFeedUrl_OmitsChannelAndWarns() + { + // A staging-identity CLI whose informational version carries no '+' build metadata + // (e.g. an unstamped local/dev build) cannot derive its SHA-specific darc feed, and there is + // no override feed. Synthesis was permitted by the identity gate, so the only safe outcome is + // to omit the staging channel and surface a warning — silently routing to the shared daily + // feed would resolve the wrong (main-branch) packages, which is the bug this PR fixes. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Staging); + + var logger = new CapturingLogger(); + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + new ConfigurationBuilder().Build(), + logger, + isStableShapedCliVersion: () => false, + cliInformationalVersionProvider: () => "13.4.0-preview.1.26280.6"); // no '+' build metadata + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.DoesNotContain(PackageChannelNames.Staging, channels.Select(c => c.Name)); + // Synthesis was allowed, so the unavailable-reason API has nothing to report — the warning + // is the only diagnostic for this edge case. + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + Assert.Contains(logger.Entries, e => e.Level == LogLevel.Warning && e.Message.Contains("staging feed URL")); + } + + public enum ExpectedStagingFeed + { + Absent, + Darc, + Shared, + Override, + } + + // Locks the full ShouldUseSharedStagingFeed decision table in one place: feed PROVENANCE is + // identity-driven (staging identity and the Stable-quality feature-flag path -> SHA-specific + // darc feed), while a non-staging identity that opts into staging with Both quality keeps the + // shared dotnet9 daily feed, an explicit override always wins, and an identity with no staging + // opt-in synthesizes no channel at all. + [Theory] + [InlineData(PackageChannelNames.Staging, false, false, false, null, ExpectedStagingFeed.Darc)] // staging identity, prerelease-shaped + [InlineData(PackageChannelNames.Staging, true, false, false, null, ExpectedStagingFeed.Darc)] // staging identity, stable-shaped + [InlineData(PackageChannelNames.Staging, false, false, false, "https://example.com/o/v3/index.json", ExpectedStagingFeed.Override)] // override always wins + [InlineData(PackageChannelNames.Stable, false, false, true, null, ExpectedStagingFeed.Shared)] // stable identity + config channel=staging => Both => shared + [InlineData(PackageChannelNames.Stable, false, true, false, null, ExpectedStagingFeed.Darc)] // stable identity + feature flag only => Stable => darc + [InlineData(PackageChannelNames.Daily, false, true, false, null, ExpectedStagingFeed.Darc)] // daily identity + feature flag only => Stable => darc + [InlineData(PackageChannelNames.Local, false, false, false, null, ExpectedStagingFeed.Absent)] // local identity, no opt-in => no channel + public async Task GetChannelsAsync_StagingFeedRoutingDecisionTable( + string identityChannel, + bool isStableShaped, + bool featureEnabled, + bool configChannelStaging, + string? overrideFeed, + ExpectedStagingFeed expected) + { + const string DarcUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json"; + const string SharedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: identityChannel); + + var settings = new Dictionary(); + if (configChannelStaging) + { + settings["channel"] = PackageChannelNames.Staging; + } + if (overrideFeed is not null) + { + settings[PackagingService.OverrideStagingFeedConfigKey] = overrideFeed; + } + var configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build(); + + var features = new TestFeatures(); + if (featureEnabled) + { + features.SetFeature(KnownFeatures.StagingChannelEnabled, true); + } + + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + features, + configuration, + NullLogger.Instance, + isStableShapedCliVersion: () => isStableShaped, + cliInformationalVersionProvider: () => "13.4.0+abcdef1234567890abcdef1234567890abcdef12"); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + var stagingChannel = channels.SingleOrDefault(c => c.Name == PackageChannelNames.Staging); + + if (expected == ExpectedStagingFeed.Absent) + { + Assert.Null(stagingChannel); + return; + } + + Assert.NotNull(stagingChannel); + var aspireSource = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*").Source; + var expectedSource = expected switch + { + ExpectedStagingFeed.Darc => DarcUrl, + ExpectedStagingFeed.Shared => SharedUrl, + ExpectedStagingFeed.Override => overrideFeed, + _ => throw new InvalidOperationException($"Unexpected expectation: {expected}"), + }; + Assert.Equal(expectedSource, aspireSource); + } + + // The following tests exercise the diagnostic override mechanism (overrideCliIdentityChannel + + // overrideCliInformationalVersion) end-to-end through the REAL config-reading default providers + // (the seams are intentionally NOT injected), which is exactly the local-validation recipe in + // docs/cli-staging-validation.md. A locally built CLI bakes a 'local' identity, so without the + // overrides these scenarios would never synthesize a staging channel at all. + + [Fact] + public async Task GetChannelsAsync_WhenIdentityOverrideAndVersionOverrideSet_RoutesAspirePackagesToDarcFeed() + { + // Full local-validation recipe: a 'local' identity CLI is told (via config overrides) to behave + // like a prerelease-shaped staging build. Both overrides are required — the identity override + // makes ShouldUseSharedStagingFeed pick the darc feed, and the version override supplies the + // '+' the darc URL is derived from. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12", + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = Assert.Single(channels, c => c.Name == PackageChannelNames.Staging); + Assert.Equal(PackageChannelQuality.Both, stagingChannel.Quality); + + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + aspireMapping.Source); + Assert.DoesNotContain("dotnet9", aspireMapping.Source); + } + + [Fact] + public async Task GetChannelsAsync_WhenVersionOverrideIsStableShaped_DefaultsToStableQuality() + { + // A stable-shaped (no semver prerelease tag) version override drives the quality predicate to + // Stable, mirroring how an official stable-shaped staging build is filtered. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0+abcdef1234567890abcdef1234567890abcdef12", + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = Assert.Single(channels, c => c.Name == PackageChannelNames.Staging); + Assert.Equal(PackageChannelQuality.Stable, stagingChannel.Quality); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*").Source); + } + + [Fact] + public async Task GetChannelsAsync_WhenIdentityOverrideIsInvalid_FallsBackToRealIdentity() + { + // An unrecognized identity override (rejected by IdentityChannelReader.IsValidChannel) is + // ignored and the real 'local' identity is used, so no staging channel is synthesized despite + // the version override being present. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = "not-a-real-channel", + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12", + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.DoesNotContain(PackageChannelNames.Staging, channels.Select(c => c.Name)); + } + + [Fact] + public async Task GetChannelsAsync_WhenOverrideStagingFeedSet_WinsOverVersionOverrideDerivation() + { + // overrideStagingFeed is the most powerful escape hatch and must win over the SHA-derived darc + // URL even when the diagnostic version override is also present. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + const string OverrideFeed = "https://example.com/override/v3/index.json"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12", + [PackagingService.OverrideStagingFeedConfigKey] = OverrideFeed, + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = Assert.Single(channels, c => c.Name == PackageChannelNames.Staging); + Assert.Equal(OverrideFeed, Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*").Source); + } + + [Fact] + public async Task GetChannelsAsync_WhenStagingDiagnosticOverridesActive_EmitsWarning() + { + // Any normal CLI invocation that has the diagnostic overrides set must leave a trace in the + // logs so an overridden identity/feed can't silently resolve Aspire.* packages. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12", + }) + .Build(); + + var logger = new CapturingLogger(); + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, logger); + + await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.Contains(logger.Entries, e => e.Level == LogLevel.Warning && e.Message.Contains("diagnostic overrides are active")); + } + + [Fact] + public async Task GetChannelsAsync_WhenOnlyVersionOverrideSet_WarnsButSynthesizesNoStagingChannel() + { + // Only the version override is set, so the identity stays 'local' and no staging channel is + // synthesized. The warning must still fire — the override is active even though it had no + // routing effect, and a silent no-op would hide a misconfiguration. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliInformationalVersionConfigKey] = "13.4.0-preview.1.26280.6+abcdef1234567890abcdef1234567890abcdef12", + }) + .Build(); + + var logger = new CapturingLogger(); + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, logger); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + Assert.DoesNotContain(PackageChannelNames.Staging, channels.Select(c => c.Name)); + Assert.Contains(logger.Entries, e => e.Level == LogLevel.Warning && e.Message.Contains("diagnostic overrides are active")); + } + + [Theory] + [InlineData("13.4.0+abcd-ef1234567890", true)] // hyphen only in build metadata => stable-shaped + [InlineData("13.4.0-preview.1.26280.6+abcd-ef1234567890", false)] // semver prerelease tag => prerelease-shaped + public async Task GetChannelsAsync_VersionOverrideStableShapeIgnoresBuildMetadataHyphens(string overrideVersion, bool expectStableQuality) + { + // StripBuildMetadata removes the '+' before the prerelease-tag check, so a commit hash + // containing '-' must not be misread as a semver prerelease tag. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + [PackagingService.OverrideCliInformationalVersionConfigKey] = overrideVersion, + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); + + var stagingChannel = Assert.Single(channels, c => c.Name == PackageChannelNames.Staging); + Assert.Equal(expectStableQuality ? PackageChannelQuality.Stable : PackageChannelQuality.Both, stagingChannel.Quality); + } + + [Fact] + public void GetStagingChannelUnavailableReason_WhenIdentityOverrideIsStaging_ReturnsNull() + { + // The unavailable-reason check (cached via Lazy) must also honor the identity override, so a + // local CLI with overrideCliIdentityChannel=staging reports staging as available. + using var workspace = TemporaryWorkspace.Create(outputHelper); + var tempDir = workspace.WorkspaceRoot; + var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); + var cacheDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "cache")); + var executionContext = new CliExecutionContext(tempDir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log", identityChannel: PackageChannelNames.Local); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideCliIdentityChannelConfigKey] = PackageChannelNames.Staging, + }) + .Build(); + + var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), new TestFeatures(), configuration, NullLogger.Instance); + + Assert.Null(packagingService.GetStagingChannelUnavailableReason()); + } + [Fact] public async Task GetChannelsAsync_WhenRequestedChannelIsStaging_IncludesStagingChannel() { @@ -244,9 +747,11 @@ public async Task GetChannelsAsync_WhenChannelStagingRequestedOnNonReleaseIdenti public async Task GetChannelsAsync_WhenChannelStagingRequestedOnDailyCliWithFeatureFlag_IncludesStagingChannel() { // Back-compat: the StagingChannelEnabled feature flag is an explicit developer/test opt-in - // and continues to bypass the identity gating. Without an override feed the SHA-specific - // path needs an AssemblyInformationalVersion to resolve, which is not guaranteed in test - // hosts, so we also supply overrideStagingFeed to make the test deterministic. + // and continues to bypass the identity gating. The feature-flag-only path defaults the + // synthesized channel quality to Stable, so a non-staging identity routes Aspire.* to the + // SHA-specific darc feed (not the shared daily feed). The informational version is injected + // so the darc derivation is deterministic — no overrideStagingFeed crutch is needed, which + // lets the assertions below isolate the feature-flag gate AND the real feed routing. using var workspace = TemporaryWorkspace.Create(outputHelper); var tempDir = workspace.WorkspaceRoot; var hivesDir = new DirectoryInfo(Path.Combine(tempDir.FullName, ".aspire", "hives")); @@ -256,30 +761,26 @@ public async Task GetChannelsAsync_WhenChannelStagingRequestedOnDailyCliWithFeat var features = new TestFeatures(); features.SetFeature(KnownFeatures.StagingChannelEnabled, true); - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - [PackagingService.OverrideStagingFeedConfigKey] = "https://example.com/staging/v3/index.json" - }) - .Build(); - - var packagingService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, configuration, NullLogger.Instance); + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + features, + new ConfigurationBuilder().Build(), + NullLogger.Instance, + cliInformationalVersionProvider: () => "13.4.0+abcdef1234567890abcdef1234567890abcdef12"); var channels = await packagingService.GetChannelsAsync().DefaultTimeout(); - Assert.Contains(PackageChannelNames.Staging, channels.Select(c => c.Name)); + var stagingChannel = Assert.Single(channels, c => c.Name == PackageChannelNames.Staging); Assert.Null(packagingService.GetStagingChannelUnavailableReason()); - // Isolate the feature-flag gate itself: IsStagingChannelSynthesisAllowed short-circuits on - // overrideStagingFeed before the feature flag is ever checked, so the assertions above - // would still pass if the feature-flag branch were removed. Build a second service whose - // only opt-in is the StagingChannelEnabled feature flag (no overrideStagingFeed) and - // assert that the gate alone reports the channel as available. We deliberately do not - // call GetChannelsAsync() here because the full channel-creation path requires an - // AssemblyInformationalVersion that is not guaranteed in test hosts. - var featureFlagOnlyConfig = new ConfigurationBuilder().Build(); - var featureFlagOnlyService = new PackagingService(executionContext, new FakeNuGetPackageCache(), features, featureFlagOnlyConfig, NullLogger.Instance); - Assert.Null(featureFlagOnlyService.GetStagingChannelUnavailableReason()); + // Feature-flag-only opt-in => Stable quality => darc feed (the gate alone, with no + // overrideStagingFeed, must both permit synthesis and route to the SHA feed). + Assert.Equal(PackageChannelQuality.Stable, stagingChannel.Quality); + var aspireMapping = Assert.Single(stagingChannel.Mappings!, m => m.PackageFilter == "Aspire*"); + Assert.Equal( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json", + aspireMapping.Source); } /// diff --git a/tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs b/tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs index dcf8d9305fa..733da516be1 100644 --- a/tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/TemporaryNuGetConfigTests.cs @@ -125,6 +125,45 @@ [new PackageMapping("Aspire.*", "https://example.com/feed")], Assert.Equal(".nugetpackages", globalPackagesFolder!.Attributes!["value"]!.Value); } + [Fact] + public async Task CreateAsync_WithExplicitGlobalPackagesFolderOverride_UsesOverrideValue() + { + // Callers that need the cache to outlive the temp config (e.g. PrebuiltAppHostServer's + // staging path) supply an absolute, persistent path so BundleNuGetService manifest paths + // remain valid after TemporaryNuGetConfig.Dispose deletes the temp directory. + var overrideValue = Path.Combine(Path.GetTempPath(), "aspire-tests", "stable-cache", "deadbeef"); + + using var tempConfig = await TemporaryNuGetConfig.CreateAsync( + [new PackageMapping("Aspire.*", "https://example.com/feed")], + configureGlobalPackagesFolder: true, + globalPackagesFolderValue: overrideValue); + + var configContent = await File.ReadAllTextAsync(tempConfig.ConfigFile.FullName); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(configContent); + + var globalPackagesFolder = xmlDoc.SelectSingleNode("//config/add[@key='globalPackagesFolder']"); + Assert.NotNull(globalPackagesFolder); + Assert.Equal(overrideValue, globalPackagesFolder!.Attributes!["value"]!.Value); + } + + [Fact] + public async Task CreateAsync_WithoutConfiguredGlobalPackagesFolder_IgnoresOverride() + { + // When configureGlobalPackagesFolder is false the override is irrelevant — no + // element should be emitted at all. + using var tempConfig = await TemporaryNuGetConfig.CreateAsync( + [new PackageMapping("Aspire.*", "https://example.com/feed")], + configureGlobalPackagesFolder: false, + globalPackagesFolderValue: "/should/not/appear"); + + var configContent = await File.ReadAllTextAsync(tempConfig.ConfigFile.FullName); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(configContent); + + Assert.Null(xmlDoc.SelectSingleNode("//config/add[@key='globalPackagesFolder']")); + } + [Theory] [InlineData("https://example.com/feed")] [InlineData("/var/folders/X/hives/pr-17105/packages")] diff --git a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs index ded0ee4c0b1..34ad8ecbc9a 100644 --- a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs @@ -11,8 +11,10 @@ using Aspire.Cli.Tests.Mcp; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; +using Aspire.Cli.Utils; using Aspire.Hosting; using Aspire.Shared; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli.Tests.Projects; @@ -418,13 +420,18 @@ public async Task TryCreateTemporaryNuGetConfig_LocalIdentity_StagingRequested_E { // Pins the rubber-duck finding: dropping the temp config also drops the staging-specific // global packages folder. The emitted nuget.config must contain a element with a - // globalPackagesFolder setting when the channel was built with configureGlobalPackagesFolder. + // globalPackagesFolder setting when the channel was built with configureGlobalPackagesFolder, + // AND the value must be an absolute path that lives outside the temp config's own directory + // so the cached packages survive the temp config's recursive cleanup (otherwise restore + // hands BundleNuGetService manifest paths that the temp dispose just deleted, hanging + // aspire-managed during DI / assembly loading on macOS osx-arm64 polyglot staging builds). using var workspace = TemporaryWorkspace.Create(outputHelper); var executionContext = CreateContextWithIdentityChannel("local"); + const string channelSource = "https://pkgs.dev.azure.com/fake/v3/index.json"; var mappings = new[] { - new PackageMapping(PackageMapping.AllPackages, "https://pkgs.dev.azure.com/fake/v3/index.json") + new PackageMapping(PackageMapping.AllPackages, channelSource) }; var stagingChannel = PackageChannel.CreateExplicitChannel( name: "staging", @@ -443,7 +450,81 @@ public async Task TryCreateTemporaryNuGetConfig_LocalIdentity_StagingRequested_E .SelectMany(c => c.Elements("add")) .FirstOrDefault(a => string.Equals(a.Attribute("key")?.Value, "globalPackagesFolder", StringComparison.OrdinalIgnoreCase)); Assert.NotNull(gpf); - Assert.False(string.IsNullOrEmpty(gpf.Attribute("value")?.Value)); + var gpfValue = gpf.Attribute("value")?.Value; + Assert.False(string.IsNullOrEmpty(gpfValue)); + Assert.True(Path.IsPathFullyQualified(gpfValue), $"globalPackagesFolder value must be an absolute path. Got: {gpfValue}"); + // The temp config directory is recursively deleted on dispose; the cache must live elsewhere + // so manifest paths produced by BundleNuGetService remain valid for the AppHost's lifetime. + var tempConfigDir = result.ConfigFile.Directory!.FullName; + Assert.False( + gpfValue!.StartsWith(tempConfigDir, StringComparison.Ordinal), + $"globalPackagesFolder must not be under the temp nuget.config dir '{tempConfigDir}'. Got: {gpfValue}"); + // The cache subdirectory must be keyed by the resolved feed URL so two different staging + // feeds (e.g. two darc builds or an overrideStagingFeed setting) get distinct caches. + var expectedCacheKey = CliPathHelper.ComputeStagingFeedCacheKey(channelSource); + Assert.NotNull(expectedCacheKey); + var expectedCachePath = Path.Combine( + CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory), + expectedCacheKey); + Assert.Equal(expectedCachePath, gpfValue); + } + + [Fact] + public async Task TryCreateTemporaryNuGetConfig_StagingRequested_FromRealPackagingService_EmitsStableGlobalPackagesFolderOutsideTempDir() + { + // End-to-end pin for the staging temp-config hang fix: when the real PackagingService + // synthesizes the staging channel (here driven by overrideStagingFeed on a stable-shaped + // CLI so configureGlobalPackagesFolder lands true), the temporary nuget.config used by + // PrebuiltAppHostServer must point globalPackagesFolder at an absolute path that survives + // the TemporaryNuGetConfig.Dispose recursive delete. Otherwise BundleNuGetService restores + // staging assemblies into /.nugetpackages, bakes those paths into + // integration-package-probe-manifest.json, and aspire-managed hangs in DI/assembly loading + // when it later probes the (now deleted) paths — observed on macOS osx-arm64. + using var workspace = TemporaryWorkspace.Create(outputHelper); + + const string overrideStagingFeed = "https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"; + var executionContext = TestExecutionContextHelper.CreateExecutionContext( + workspace, + identityChannel: PackageChannelNames.Staging); + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + [PackagingService.OverrideStagingFeedConfigKey] = overrideStagingFeed + }) + .Build(); + var packagingService = new PackagingService( + executionContext, + new FakeNuGetPackageCache(), + new TestFeatures(), + configuration, + NullLogger.Instance, + isStableShapedCliVersion: () => true); + + var server = CreateServerWithPackagingService(workspace, packagingService, executionContext); + + using var result = await InvokeTryCreateTemporaryNuGetConfigAsync(server, PackageChannelNames.Staging); + + Assert.NotNull(result); + var doc = XDocument.Load(result.ConfigFile.FullName); + var gpf = doc.Descendants("config") + .SelectMany(c => c.Elements("add")) + .FirstOrDefault(a => string.Equals(a.Attribute("key")?.Value, "globalPackagesFolder", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(gpf); + var gpfValue = gpf.Attribute("value")?.Value; + Assert.False(string.IsNullOrEmpty(gpfValue)); + Assert.True(Path.IsPathFullyQualified(gpfValue), $"globalPackagesFolder value must be an absolute path. Got: {gpfValue}"); + var tempConfigDir = result.ConfigFile.Directory!.FullName; + Assert.False( + gpfValue!.StartsWith(tempConfigDir, StringComparison.Ordinal), + $"globalPackagesFolder must not be under the temp nuget.config dir '{tempConfigDir}'. Got: {gpfValue}"); + // The cache key is derived from the resolved staging feed URL so the same CLI talking to + // a different overrideStagingFeed gets a different cache bucket. + var expectedCacheKey = CliPathHelper.ComputeStagingFeedCacheKey(overrideStagingFeed); + Assert.NotNull(expectedCacheKey); + var expectedCachePath = Path.Combine( + CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory), + expectedCacheKey); + Assert.Equal(expectedCachePath, gpfValue); } [Theory] @@ -532,7 +613,8 @@ public async Task TryCreateTemporaryNuGetConfig_WithPackageSourceOverride_Preser nuGetPackageCache: new FakeNuGetPackageCache(), features: new TestFeatures(), configureGlobalPackagesFolder: true); - var server = CreateServerWithChannel(workspace, stagingChannel, CreateContextWithIdentityChannel("pr-12345")); + var executionContext = CreateContextWithIdentityChannel("pr-12345"); + var server = CreateServerWithChannel(workspace, stagingChannel, executionContext); using var result = await InvokeTryCreateTemporaryNuGetConfigAsync( server, @@ -544,9 +626,27 @@ public async Task TryCreateTemporaryNuGetConfig_WithPackageSourceOverride_Preser Assert.Equal(["Aspire*"], GetPackagePatternsForSource(doc, packageSourceOverride)); Assert.Equal(["CommunityToolkit*"], GetPackagePatternsForSource(doc, channelSource)); Assert.Equal([PackageMapping.AllPackages], GetPackagePatternsForSource(doc, NuGetOrgSource)); - Assert.NotNull(doc.Descendants("config") + var gpf = doc.Descendants("config") .SelectMany(c => c.Elements("add")) - .FirstOrDefault(a => string.Equals(a.Attribute("key")?.Value, "globalPackagesFolder", StringComparison.OrdinalIgnoreCase))); + .FirstOrDefault(a => string.Equals(a.Attribute("key")?.Value, "globalPackagesFolder", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(gpf); + // Same stability requirement as the channel-only branch: the override path must outlive + // the temp nuget.config so BundleNuGetService's manifest paths remain valid after dispose. + var gpfValue = gpf.Attribute("value")?.Value; + Assert.False(string.IsNullOrEmpty(gpfValue)); + Assert.True(Path.IsPathFullyQualified(gpfValue), $"globalPackagesFolder value must be an absolute path. Got: {gpfValue}"); + var tempConfigDir = result.ConfigFile.Directory!.FullName; + Assert.False( + gpfValue!.StartsWith(tempConfigDir, StringComparison.Ordinal), + $"globalPackagesFolder must not be under the temp nuget.config dir '{tempConfigDir}'. Got: {gpfValue}"); + // The cache key is derived from the --source override, not the channel's own mappings, + // so users running multiple overrides against the same CLI get distinct cache buckets. + var expectedCacheKey = CliPathHelper.ComputeStagingFeedCacheKey(packageSourceOverride); + Assert.NotNull(expectedCacheKey); + var expectedCachePath = Path.Combine( + CliPathHelper.GetStagingNuGetPackagesDirectory(executionContext.AspireHomeDirectory), + expectedCacheKey); + Assert.Equal(expectedCachePath, gpfValue); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs b/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs index b25d247fc27..010311d0ac6 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs @@ -54,6 +54,94 @@ public void CreateUnixDomainSocketPath_UsesRandomizedIdentifier() Assert.Matches("^h[A-Za-z0-9_-]{8}$", Path.GetFileName(socketPath2)); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("\t\n")] + public void ComputeStagingFeedCacheKey_ReturnsNull_ForNullOrWhitespace(string? feedUrl) + { + Assert.Null(CliPathHelper.ComputeStagingFeedCacheKey(feedUrl)); + } + + [Fact] + public void ComputeStagingFeedCacheKey_DefaultsToEightLowercaseHexChars() + { + var key = CliPathHelper.ComputeStagingFeedCacheKey("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"); + + Assert.NotNull(key); + Assert.Matches("^[0-9a-f]{8}$", key); + } + + [Fact] + public void ComputeStagingFeedCacheKey_IsDeterministic_ForSameInput() + { + const string feedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"; + + var first = CliPathHelper.ComputeStagingFeedCacheKey(feedUrl); + var second = CliPathHelper.ComputeStagingFeedCacheKey(feedUrl); + + Assert.Equal(first, second); + } + + [Fact] + public void ComputeStagingFeedCacheKey_DifferentUrls_ProduceDifferentKeys() + { + var a = CliPathHelper.ComputeStagingFeedCacheKey("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"); + var b = CliPathHelper.ComputeStagingFeedCacheKey("https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-cafef00d/nuget/v3/index.json"); + + Assert.NotEqual(a, b); + } + + [Fact] + public void ComputeStagingFeedCacheKey_NormalizesWhitespaceAndCasing() + { + // Trim + lowercase normalization keeps the cache from fragmenting when the same feed + // shows up with stray whitespace from a config file or with a mixed-case hostname. + const string baseUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json"; + + var baseKey = CliPathHelper.ComputeStagingFeedCacheKey(baseUrl); + var spacedKey = CliPathHelper.ComputeStagingFeedCacheKey(" " + baseUrl + "\t\n"); + var upperKey = CliPathHelper.ComputeStagingFeedCacheKey(baseUrl.ToUpperInvariant()); + + Assert.Equal(baseKey, spacedKey); + Assert.Equal(baseKey, upperKey); + } + + [Theory] + [InlineData(1)] + [InlineData(4)] + [InlineData(16)] + public void ComputeStagingFeedCacheKey_RespectsExplicitLength(int length) + { + var key = CliPathHelper.ComputeStagingFeedCacheKey( + "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-deadbeef/nuget/v3/index.json", + length); + + Assert.NotNull(key); + Assert.Equal(length, key.Length); + Assert.Matches($"^[0-9a-f]{{{length}}}$", key); + } + + [Fact] + public void ComputeStagingFeedCacheKey_LengthAboveHashWidth_ReturnsFullHash() + { + // XxHash3 is 64 bits -> 16 hex chars. Asking for more than 16 must not crash and must + // return all available hash chars rather than padding with garbage. + var key = CliPathHelper.ComputeStagingFeedCacheKey("https://example/index.json", length: 999); + + Assert.NotNull(key); + Assert.Equal(16, key.Length); + Assert.Matches("^[0-9a-f]{16}$", key); + } + + [Fact] + public void ComputeStagingFeedCacheKey_NonZeroLength_RejectsZeroOrNegative() + { + Assert.Null(CliPathHelper.ComputeStagingFeedCacheKey("https://example/index.json", length: 0)); + Assert.Null(CliPathHelper.ComputeStagingFeedCacheKey("https://example/index.json", length: -1)); + } + [Theory] [InlineData("script")] [InlineData("localhive")] diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index e1081821c39..a9a4b318fe4 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -576,7 +576,14 @@ public ISolutionLocator CreateDefaultSolutionLocatorFactory(IServiceProvider ser var nuGetPackageCache = serviceProvider.GetRequiredService(); var features = serviceProvider.GetRequiredService(); var configuration = serviceProvider.GetRequiredService(); - return new PackagingService(executionContext, nuGetPackageCache, features, configuration, NullLogger.Instance); + // Force prerelease-shaped CLI version semantics in tests so PackagingService's + // identity-staging quality default does not depend on whether the test-host assembly + // was produced under StabilizePackageVersion=true. Without this, tests that rely on + // the shared-daily routing for `staging` identity (quality=Both → useSharedFeed=true) + // would fail under the stabilization-check job which builds with a stable-shaped + // version (no '-' suffix) baked in. Tests that specifically exercise the stable-shape + // branch construct PackagingService directly with isStableShapedCliVersion: () => true. + return new PackagingService(executionContext, nuGetPackageCache, features, configuration, NullLogger.Instance, isStableShapedCliVersion: () => false); }; public Func DiskCacheFactory { get; set; } = (IServiceProvider serviceProvider) => new NullDiskCache(); diff --git a/tests/Aspire.Cli.Tests/Utils/VersionHelperTests.cs b/tests/Aspire.Cli.Tests/Utils/VersionHelperTests.cs index ef217314d55..534de25d5fd 100644 --- a/tests/Aspire.Cli.Tests/Utils/VersionHelperTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/VersionHelperTests.cs @@ -28,6 +28,71 @@ public void TryGetCurrentCliVersionMatch_WithPrHivesAndNoChannel_ReturnsCurrentC Assert.Equal(cliVersion, match); } + [Theory] + [InlineData("daily")] + [InlineData("staging")] + [InlineData("stable")] + public void TryGetCurrentCliVersionMatch_WithNamedChannel_ReturnsCurrentCliVersion(string channelName) + { + var cliVersion = VersionHelper.GetDefaultSdkVersion(); + var candidates = new[] + { + "99.0.0", + cliVersion, + }; + + var result = VersionHelper.TryGetCurrentCliVersionMatch( + candidates, + version => version, + out var match, + channelName: channelName, + hasPrHives: false); + + Assert.True(result); + Assert.Equal(cliVersion, match); + } + + [Fact] + public void TryGetCurrentCliVersionMatch_WithNamedChannelAndNoExactMatch_ReturnsFalse() + { + var candidates = new[] + { + "99.0.0", + "98.0.0", + }; + + var result = VersionHelper.TryGetCurrentCliVersionMatch( + candidates, + version => version, + out var match, + channelName: "daily", + hasPrHives: false); + + Assert.False(result); + Assert.Null(match); + } + + [Fact] + public void TryGetCurrentCliVersionMatch_WithNoChannelAndNoPrHives_ReturnsFalse() + { + var cliVersion = VersionHelper.GetDefaultSdkVersion(); + var candidates = new[] + { + "99.0.0", + cliVersion, + }; + + var result = VersionHelper.TryGetCurrentCliVersionMatch( + candidates, + version => version, + out var match, + channelName: null, + hasPrHives: false); + + Assert.False(result); + Assert.Null(match); + } + [Theory] [InlineData("pr-16820", true)] [InlineData("run-25422767716", true)] diff --git a/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesFoundryReferenceTests.cs b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesFoundryReferenceTests.cs new file mode 100644 index 00000000000..112e54fb25e --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Kubernetes.Tests/AzureKubernetesFoundryReferenceTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPIREAZURE003 +#pragma warning disable ASPIREPIPELINES003 + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; +using Aspire.Hosting.Tests; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting.Azure.Tests; + +public class AzureKubernetesFoundryReferenceTests +{ + [Fact] + public async Task EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments() + { + using var tempDir = new TestTempDirectory(); + using var builder = TestDistributedApplicationBuilder.Create( + DistributedApplicationOperation.Publish, + tempDir.Path); + + builder.Services.AddSingleton(); + + var aks = builder.AddAzureKubernetesEnvironment("aks"); + + var project = builder.AddFoundry("foundry") + .AddProject("project"); + + // The agent app is deployed to the Foundry project compute environment via AsHostedAgent. + var agent = builder.AddProject("agent", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints(); + agent.AsHostedAgent(project); + + // The web app is deployed to Azure Kubernetes and references the Foundry hosted agent. + // The Kubernetes publisher must delegate endpoint resolution to the Foundry compute + // environment rather than looking the agent up in its own (local) endpoint map, which + // does not contain the cross-environment agent. See issue #17749. + // WithReference(agent) exercises the bare EndpointReference branch; the explicit + // Property(Url) environment variable exercises the EndpointReferenceExpression branch. + builder.AddProject("web", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints() + .WithComputeEnvironment(aks) + .WithReference(agent) + .WithEnvironment("AGENT_URL", agent.GetEndpoint("http").Property(EndpointProperty.Url)); + + await using var app = builder.Build(); + await app.RunAsync(); + + // The resolved environment variable values are emitted into the Helm chart's values.yaml. + // The agent endpoint must resolve to the Foundry project endpoint composed with the deployed + // hosted agent path because hosted-agent deployment creates the Foundry agent version with the + // wrapper resource name. + var valuesPath = Directory.EnumerateFiles(tempDir.Path, "values.yaml", SearchOption.AllDirectories).Single(); + var values = await File.ReadAllTextAsync(valuesPath); + + Assert.Contains("AGENT_HTTP: \"{project.outputs.endpoint}/agents/agent-ha\"", values); + Assert.Contains("AGENT_URL: \"{project.outputs.endpoint}/agents/agent-ha\"", values); + Assert.Contains("services__agent__http__0: \"{project.outputs.endpoint}/agents/agent-ha\"", values); + } + + private sealed class Project : IProjectMetadata + { + public string ProjectPath => "project"; + } +} diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs index 2c5c185f59b..d8730e90796 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureAppServiceTests.cs @@ -6,6 +6,7 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Foundry; using Aspire.Hosting.Pipelines; using Aspire.Hosting.Utils; using Aspire.TestUtilities; @@ -225,6 +226,59 @@ await Verify(manifest.ToString(), "json") .AppendContentAsFile(bicep, "bicep"); } + [Fact] + public async Task EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var appServiceEnv = builder.AddAzureAppServiceEnvironment("env"); + + var project = builder.AddFoundry("foundry") + .AddProject("project"); + + // The agent app is deployed to the Foundry project compute environment via AsHostedAgent. + var agent = builder.AddProject("agent", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints(); + agent.AsHostedAgent(project); + + // The web app is deployed to App Service and references the Foundry hosted agent. The App + // Service publisher must delegate endpoint resolution to the Foundry compute environment + // rather than looking the agent up in its own (App Service) endpoint map. See issue #17749. + // WithReference(agent) exercises the bare EndpointReference branch; the explicit + // Property(Url) environment variable exercises the EndpointReferenceExpression branch. + var web = builder.AddProject("web", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints() + .WithComputeEnvironment(appServiceEnv) + .WithReference(agent) + .WithEnvironment("AGENT_URL", agent.GetEndpoint("http").Property(EndpointProperty.Url)); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + SetFoundryProjectOutputs(project.Resource); + + web.Resource.TryGetLastAnnotation(out var target); + + var resource = target?.DeploymentTarget as AzureProvisioningResource; + + Assert.NotNull(resource); + + var (manifest, bicep) = await GetManifestWithBicep(resource); + + await Verify(manifest.ToString(), "json") + .AppendContentAsFile(bicep, "bicep"); + } + + private static void SetFoundryProjectOutputs(AzureCognitiveServicesProjectResource project) + { + project.Outputs["endpoint"] = "https://account.services.ai.azure.com/api/projects/my-project"; + project.Outputs["APPLICATION_INSIGHTS_CONNECTION_STRING"] = ""; + project.ProvisioningTaskCompletionSource?.TrySetResult(); + } + [Fact] public async Task AzureAppServiceSupportBaitAndSwitchResources() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index 7fbcb2451d4..1fa7da1cf95 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -10,6 +10,7 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.AppContainers; +using Aspire.Hosting.Foundry; using Aspire.Hosting.Pipelines; using Aspire.Hosting.Utils; using Azure.Provisioning; @@ -120,6 +121,59 @@ await Verify(manifest.ToString(), "json") .AppendContentAsFile(bicep, "bicep"); } + [Fact] + public async Task EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var acaEnv = builder.AddAzureContainerAppEnvironment("env"); + + var project = builder.AddFoundry("foundry") + .AddProject("project"); + + // The agent app is deployed to the Foundry project compute environment via AsHostedAgent. + var agent = builder.AddProject("agent", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints(); + agent.AsHostedAgent(project); + + // The web app is deployed to Azure Container Apps and references the Foundry hosted agent. + // The ACA publisher must delegate endpoint resolution to the Foundry compute environment + // rather than looking the agent up in its own endpoint map. See issue #17749. + // WithReference(agent) exercises the bare EndpointReference branch; the explicit + // Property(Url) environment variable exercises the EndpointReferenceExpression branch. + var web = builder.AddProject("web", launchProfileName: null) + .WithHttpEndpoint() + .WithExternalHttpEndpoints() + .WithComputeEnvironment(acaEnv) + .WithReference(agent) + .WithEnvironment("AGENT_URL", agent.GetEndpoint("http").Property(EndpointProperty.Url)); + + using var app = builder.Build(); + + await ExecuteBeforeStartHooksAsync(app, default); + + SetFoundryProjectOutputs(project.Resource); + + var target = web.Resource.GetDeploymentTargetAnnotation(); + + var resource = target?.DeploymentTarget as AzureProvisioningResource; + + Assert.NotNull(resource); + + var (manifest, bicep) = await GetManifestWithBicep(resource); + + await Verify(manifest.ToString(), "json") + .AppendContentAsFile(bicep, "bicep"); + } + + private static void SetFoundryProjectOutputs(AzureCognitiveServicesProjectResource project) + { + project.Outputs["endpoint"] = "https://account.services.ai.azure.com/api/projects/my-project"; + project.Outputs["APPLICATION_INSIGHTS_CONNECTION_STRING"] = ""; + project.ProvisioningTaskCompletionSource?.TrySetResult(); + } + [Fact] public async Task AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index f0b79fa665d..01062779b87 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -371,7 +371,7 @@ public async Task WithPostgresMcpOnAzureDatabaseRunAsContainerAddsMcpResource() c.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); }); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs index 889b2c1b964..63dfc210cfd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourcePreparerTests.cs @@ -538,6 +538,204 @@ public async Task ViteAppDoesNotGetManagedIdentity() Assert.DoesNotContain(model.Resources, r => r.Name == "frontend-identity"); } + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_PublishMode_GrantsRolesOnImpliedTargetToConsumer() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + + // A non-Azure compute resource (the "agent" node app) that fronts the storage account: any + // resource referencing it should be granted a role on storage even though storage is only a + // transitive dependency that the IAzureResource-only reference walk cannot reach. + var agent = builder.AddContainer("agent", "img:latest") + .WithHttpEndpoint(); + agent.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation( + storage.Resource, + new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) })); + + var consumer = builder.AddProject("api", launchProfileName: null) + .WithReference(agent.GetEndpoint("http")); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + Assert.True(consumer.Resource.TryGetLastAnnotation(out var consumerRoleAssignments)); + Assert.Equal(storage.Resource, consumerRoleAssignments.Target); + Assert.Single(consumerRoleAssignments.Roles, role => role.Id == StorageBuiltInRole.StorageBlobDataReader.ToString()); + } + + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_RunMode_AppliesRolesToGlobalRolesResource() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + + var agent = builder.AddContainer("agent", "img:latest") + .WithHttpEndpoint(); + agent.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation( + storage.Resource, + new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) })); + + builder.AddProject("api", launchProfileName: null) + .WithReference(agent.GetEndpoint("http")); + + using var app = builder.Build(); + var model = app.Services.GetRequiredService(); + await ExecuteBeforeStartHooksAsync(app, default); + + var storageRoles = Assert.Single(model.Resources.OfType(), r => r.Name == "storage-roles"); + Assert.Same(storage.Resource, storageRoles.TargetAzureResource); + } + + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_ConsumerNotReferencingFrontingResource_GetsNoRole() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + + var agent = builder.AddContainer("agent", "img:latest") + .WithHttpEndpoint(); + agent.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation( + storage.Resource, + new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) })); + + // This compute resource does not reference the fronting agent, so it must not be granted any + // role on the implied storage target. + var bystander = builder.AddProject("bystander", launchProfileName: null); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + Assert.False(bystander.Resource.TryGetLastAnnotation(out _)); + } + + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_SameTargetFromTwoDependencies_DedupesRoles() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + + // Two fronting resources (e.g. two hosted agents on the same Foundry account) each imply the + // same role on the same target. A consumer referencing both must end up with a single role + // assignment for that role, otherwise two RoleAssignment bicep resources collide on the same + // identifier ("{prefix}_{roleName}") and bicep compilation fails. + var role = new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) }; + + var agent1 = builder.AddContainer("agent1", "img:latest").WithHttpEndpoint(); + agent1.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation(storage.Resource, role)); + + var agent2 = builder.AddContainer("agent2", "img:latest").WithHttpEndpoint(); + agent2.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation(storage.Resource, role)); + + var consumer = builder.AddProject("api", launchProfileName: null) + .WithReference(agent1.GetEndpoint("http")) + .WithReference(agent2.GetEndpoint("http")); + + using var app = builder.Build(); + var model = app.Services.GetRequiredService(); + await ExecuteBeforeStartHooksAsync(app, default); + + var roleAssignmentResource = Assert.Single(model.Resources.OfType(), r => r.Name == "api-roles-storage"); + Assert.Same(storage.Resource, roleAssignmentResource.TargetAzureResource); + Assert.Same(consumer.Resource, roleAssignmentResource.OwnerResource); + + // The generated bicep must contain exactly one role assignment, not two duplicates of the same role. + var manifest = await GetManifestWithBicep(roleAssignmentResource, skipPreparer: true); + var roleAssignmentCount = System.Text.RegularExpressions.Regex.Matches(manifest.BicepText, "Microsoft.Authorization/roleAssignments@").Count; + Assert.Equal(1, roleAssignmentCount); + } + + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_ConsumerWithExplicitRoleAssignment_DoesNotReintroduceSuppressedDefaults() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + var blobs = storage.AddBlobs("blobs"); + + // A fronting resource (e.g. a Foundry hosted agent's node app) implies the Reader role on storage. + var agent = builder.AddContainer("agent", "img:latest").WithHttpEndpoint(); + agent.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation( + storage.Resource, + new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) })); + + // The consumer references storage directly (so its default role assignments would normally be + // applied) but declares an explicit WithRoleAssignments, which intentionally suppresses those + // defaults. The implied-reference hook must add only its Reader role and must NOT re-introduce the + // suppressed defaults - that was the "pit of failure" the union-of-defaults pattern would have caused. + var consumer = builder.AddProject("api", launchProfileName: null) + .WithRoleAssignments(storage, StorageBuiltInRole.StorageBlobDelegator) + .WithReference(blobs) + .WithReference(agent.GetEndpoint("http")); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var roleIds = consumer.Resource.Annotations.OfType() + .Where(a => a.Target == storage.Resource) + .SelectMany(a => a.Roles) + .Select(r => r.Id) + .ToHashSet(); + + // Explicit role + implied Reader are present. + Assert.Contains(StorageBuiltInRole.StorageBlobDelegator.ToString(), roleIds); + Assert.Contains(StorageBuiltInRole.StorageBlobDataReader.ToString(), roleIds); + + // The suppressed defaults must not be re-introduced by the implied-reference hook. + Assert.DoesNotContain(StorageBuiltInRole.StorageBlobDataContributor.ToString(), roleIds); + Assert.DoesNotContain(StorageBuiltInRole.StorageTableDataContributor.ToString(), roleIds); + Assert.DoesNotContain(StorageBuiltInRole.StorageQueueDataContributor.ToString(), roleIds); + } + + [Fact] + public async Task ReferenceRoleAssignmentAnnotation_ConsumerWithDirectReference_KeepsDefaultsAndAddsImpliedRole() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + builder.AddAzureContainerAppEnvironment("env"); + + var storage = builder.AddAzureStorage("storage"); + var blobs = storage.AddBlobs("blobs"); + + // A fronting resource (e.g. a Foundry hosted agent's node app) implies the Reader role on storage. + var agent = builder.AddContainer("agent", "img:latest").WithHttpEndpoint(); + agent.Resource.Annotations.Add(new ReferenceRoleAssignmentAnnotation( + storage.Resource, + new HashSet { new(StorageBuiltInRole.StorageBlobDataReader.ToString(), nameof(StorageBuiltInRole.StorageBlobDataReader)) })); + + // The consumer references storage directly without declaring explicit role assignments, so the + // account defaults still apply. The implied-reference hook must add its Reader role alongside the + // defaults (the caller - the preparer - owns defaults; the hook never replaces or removes them). + var consumer = builder.AddProject("api", launchProfileName: null) + .WithReference(blobs) + .WithReference(agent.GetEndpoint("http")); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var roleIds = consumer.Resource.Annotations.OfType() + .Where(a => a.Target == storage.Resource) + .SelectMany(a => a.Roles) + .Select(r => r.Id) + .ToHashSet(); + + // Defaults are preserved by the preparer's normal reference walk. + Assert.Contains(StorageBuiltInRole.StorageBlobDataContributor.ToString(), roleIds); + Assert.Contains(StorageBuiltInRole.StorageTableDataContributor.ToString(), roleIds); + Assert.Contains(StorageBuiltInRole.StorageQueueDataContributor.ToString(), roleIds); + + // The implied Reader role is added on top of the defaults. + Assert.Contains(StorageBuiltInRole.StorageBlobDataReader.ToString(), roleIds); + } + private sealed class Project : IProjectMetadata { public string ProjectPath => "project"; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs index 0710044f720..d9124efcf24 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs @@ -1,4 +1,5 @@ #pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. diff --git a/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs b/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs index 4bde4eea3ce..95fad316307 100644 --- a/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep new file mode 100644 index 00000000000..f70d350d3a9 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep @@ -0,0 +1,148 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param env_outputs_azure_container_registry_endpoint string + +param env_outputs_planid string + +param env_outputs_azure_container_registry_managed_identity_id string + +param env_outputs_azure_container_registry_managed_identity_client_id string + +param web_containerimage string + +param web_containerport string + +param project_outputs_endpoint string + +param web_identity_outputs_id string + +param web_identity_outputs_clientid string + +param env_outputs_azure_app_service_dashboard_uri string + +param env_outputs_azure_website_contributor_managed_identity_id string + +param env_outputs_azure_website_contributor_managed_identity_principal_id string + +resource mainContainer 'Microsoft.Web/sites/sitecontainers@2025-03-01' = { + name: 'main' + properties: { + authType: 'UserAssigned' + image: web_containerimage + isMain: true + targetPort: web_containerport + userManagedIdentityClientId: env_outputs_azure_container_registry_managed_identity_client_id + } + parent: webapp +} + +resource webapp 'Microsoft.Web/sites@2025-03-01' = { + name: take('${toLower('web')}-${uniqueString(resourceGroup().id)}', 60) + location: location + properties: { + serverFarmId: env_outputs_planid + keyVaultReferenceIdentity: web_identity_outputs_id + siteConfig: { + numberOfWorkers: 30 + linuxFxVersion: 'SITECONTAINERS' + acrUseManagedIdentityCreds: true + acrUserManagedIdentityID: env_outputs_azure_container_registry_managed_identity_client_id + appSettings: [ + { + name: 'WEBSITES_PORT' + value: web_containerport + } + { + name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY' + value: 'in_memory' + } + { + name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED' + value: 'true' + } + { + name: 'HTTP_PORTS' + value: web_containerport + } + { + name: 'AGENT_HTTP' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'services__agent__http__0' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'AGENT_URL' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'AZURE_CLIENT_ID' + value: web_identity_outputs_clientid + } + { + name: 'AZURE_TOKEN_CREDENTIALS' + value: 'ManagedIdentityCredential' + } + { + name: 'ASPIRE_ENVIRONMENT_NAME' + value: 'env' + } + { + name: 'OTEL_SERVICE_NAME' + value: 'web' + } + { + name: 'OTEL_EXPORTER_OTLP_PROTOCOL' + value: 'grpc' + } + { + name: 'OTEL_EXPORTER_OTLP_ENDPOINT' + value: 'http://localhost:6001' + } + { + name: 'WEBSITE_ENABLE_ASPIRE_OTEL_SIDECAR' + value: 'true' + } + { + name: 'OTEL_COLLECTOR_URL' + value: env_outputs_azure_app_service_dashboard_uri + } + { + name: 'OTEL_CLIENT_ID' + value: env_outputs_azure_container_registry_managed_identity_client_id + } + ] + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${env_outputs_azure_container_registry_managed_identity_id}': { } + '${web_identity_outputs_id}': { } + } + } +} + +resource web_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) + properties: { + principalId: env_outputs_azure_website_contributor_managed_identity_principal_id + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772') + principalType: 'ServicePrincipal' + } + scope: webapp +} + +resource slotConfigNames 'Microsoft.Web/sites/config@2025-03-01' = { + name: 'slotConfigNames' + properties: { + appSettingNames: [ + 'AGENT_HTTP' + 'services__agent__http__0' + 'OTEL_SERVICE_NAME' + ] + } + parent: webapp +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json new file mode 100644 index 00000000000..cd9161f896c --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json @@ -0,0 +1,18 @@ +{ + "type": "azure.bicep.v0", + "path": "web-website.module.bicep", + "params": { + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_planid": "{env.outputs.planId}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "env_outputs_azure_container_registry_managed_identity_client_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID}", + "web_containerimage": "{web.containerImage}", + "web_containerport": "{web.containerPort}", + "project_outputs_endpoint": "{project.outputs.endpoint}", + "web_identity_outputs_id": "{web-identity.outputs.id}", + "web_identity_outputs_clientid": "{web-identity.outputs.clientId}", + "env_outputs_azure_app_service_dashboard_uri": "{env.outputs.AZURE_APP_SERVICE_DASHBOARD_URI}", + "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", + "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" + } +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep new file mode 100644 index 00000000000..c6f7220f3f5 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.bicep @@ -0,0 +1,99 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param env_outputs_azure_container_apps_environment_default_domain string + +param env_outputs_azure_container_apps_environment_id string + +param env_outputs_azure_container_registry_endpoint string + +param env_outputs_azure_container_registry_managed_identity_id string + +param web_containerimage string + +param web_identity_outputs_id string + +param web_containerport string + +param project_outputs_endpoint string + +param web_identity_outputs_clientid string + +resource web 'Microsoft.App/containerApps@2025-10-02-preview' = { + name: 'web' + location: location + properties: { + configuration: { + activeRevisionsMode: 'Single' + ingress: { + external: true + targetPort: int(web_containerport) + transport: 'http' + } + registries: [ + { + server: env_outputs_azure_container_registry_endpoint + identity: env_outputs_azure_container_registry_managed_identity_id + } + ] + runtime: { + dotnet: { + autoConfigureDataProtection: true + } + } + } + environmentId: env_outputs_azure_container_apps_environment_id + template: { + containers: [ + { + image: web_containerimage + name: 'web' + env: [ + { + name: 'OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY' + value: 'in_memory' + } + { + name: 'ASPNETCORE_FORWARDEDHEADERS_ENABLED' + value: 'true' + } + { + name: 'HTTP_PORTS' + value: web_containerport + } + { + name: 'AGENT_HTTP' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'services__agent__http__0' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'AGENT_URL' + value: '${project_outputs_endpoint}/agents/agent-ha' + } + { + name: 'AZURE_CLIENT_ID' + value: web_identity_outputs_clientid + } + { + name: 'AZURE_TOKEN_CREDENTIALS' + value: 'ManagedIdentityCredential' + } + ] + } + ] + scale: { + minReplicas: 1 + } + } + } + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${web_identity_outputs_id}': { } + '${env_outputs_azure_container_registry_managed_identity_id}': { } + } + } +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json new file mode 100644 index 00000000000..9bba0f4e346 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointReferenceToFoundryHostedAgentIsResolvedAcrossComputeEnvironments.verified.json @@ -0,0 +1,15 @@ +{ + "type": "azure.bicep.v0", + "path": "web-containerapp.module.bicep", + "params": { + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "web_containerimage": "{web.containerImage}", + "web_identity_outputs_id": "{web-identity.outputs.id}", + "web_containerport": "{web.containerPort}", + "project_outputs_endpoint": "{project.outputs.endpoint}", + "web_identity_outputs_clientid": "{web-identity.outputs.clientId}" + } +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs index 7ac8fdec09f..17ef7854130 100644 --- a/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs +++ b/tests/Aspire.Hosting.Docker.Tests/DockerComposePublisherTests.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREPIPELINES003 +#pragma warning disable ASPIRECONTAINERRUNTIME001 using System.Text.RegularExpressions; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Docker.Resources.ComposeNodes; using Aspire.Hosting.Publishing; +using Aspire.Hosting.Tests.Publishing; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -812,6 +814,8 @@ public async Task PrepareStep_ResolvesContainerImageReferenceViaIValueProvider() var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path, step: "prepare-docker-compose"); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => (IContainerRuntimeResolver)sp.GetRequiredService()); builder.AddDockerComposeEnvironment("docker-compose"); diff --git a/tests/Aspire.Hosting.Foundry.Tests/HostedAgentConfigurationTests.cs b/tests/Aspire.Hosting.Foundry.Tests/HostedAgentConfigurationTests.cs index 27406e4a25f..2b027da493d 100644 --- a/tests/Aspire.Hosting.Foundry.Tests/HostedAgentConfigurationTests.cs +++ b/tests/Aspire.Hosting.Foundry.Tests/HostedAgentConfigurationTests.cs @@ -98,6 +98,21 @@ public void ToProjectsAgentVersionCreationOptions_ThrowsForInvalidEnvironmentVar ex.Message); } + [Fact] + public void ToProjectsAgentVersionCreationOptions_ThrowsForReservedEnvironmentVariableNames() + { + var config = new HostedAgentConfiguration("myimage:latest"); + config.EnvironmentVariables["PORT"] = "8000"; + config.EnvironmentVariables["AGENT_NAME"] = "agent"; + config.EnvironmentVariables["FOUNDRY_MODE"] = "hosted"; + + var ex = Assert.Throws(() => config.ToProjectsAgentVersionCreationOptions("target")); + + Assert.Equal( + "Foundry hosted agent for target resource 'target' contains environment variable names that are reserved by Foundry Hosted Agents. Reserved name(s): 'AGENT_NAME', 'FOUNDRY_MODE', 'PORT'", + ex.Message); + } + [Fact] public void DefaultMetadata_ContainsDeployedByAndOn() { diff --git a/tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs b/tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs index cb1ac78563d..cff2d20891e 100644 --- a/tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs +++ b/tests/Aspire.Hosting.Foundry.Tests/HostedAgentExtensionTests.cs @@ -5,9 +5,12 @@ using System.Runtime.CompilerServices; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Azure.AI.Projects.Agents; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Hosting.Foundry.Tests; @@ -74,11 +77,62 @@ public void AsHostedAgent_InRunMode_ConfiguresSendMessageCommand() var resource = builder.Resources.Single(r => r.Name == "agent"); var command = Assert.Single(resource.Annotations.OfType()); Assert.Equal("Send Message", command.DisplayName); + Assert.EndsWith("-/responses", command.Name); Assert.Equal("ChatSparkle", command.IconName); Assert.Equal(IconVariant.Regular, command.IconVariant); Assert.True(command.IsHighlighted); } + [Fact] + public async Task AsHostedAgent_InRunMode_WithInvocationsProtocol_ConfiguresEndpointAndCommand() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + var project = builder.AddFoundry("account") + .AddProject("my-project"); + builder.AddPythonApp("agent", "./app.py", "main:app") + .AsHostedAgent(project, configuration => + { + configuration.ContainerProtocolVersions.Clear(); + configuration.ContainerProtocolVersions.Add(new ProtocolVersionRecord(ProjectsAgentProtocol.Invocations, "1.0.0")); + }); + + using var app = builder.Build(); + + var resource = builder.Resources.Single(r => r.Name == "agent"); + var command = Assert.Single(resource.Annotations.OfType()); + Assert.EndsWith("-/invocations", command.Name); + + var urlsCallback = Assert.Single(resource.Annotations.OfType()); + var url = new ResourceUrlAnnotation + { + Url = "http://localhost:1234", + Endpoint = ((IResourceWithEndpoints)resource).GetEndpoint("http") + }; + var urls = new List { url }; + var context = new ResourceUrlsCallbackContext( + app.Services.GetRequiredService(), + resource, + urls); + + await urlsCallback.Callback(context); + + Assert.Equal("Invocations Endpoint", url.DisplayText); + Assert.Equal("http://localhost:1234/invocations", url.Url); + } + + [Fact] + public void AsHostedAgent_InRunMode_WrapsConfigurationCallbackFailures() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + + var ex = Assert.Throws(() => + builder.AddPythonApp("agent", "./app.py", "main:app") + .AsHostedAgent(configuration => configuration.Cpu = 4.0m)); + + Assert.Contains("run mode", ex.Message); + Assert.IsType(ex.InnerException); + } + [Fact] public void AsHostedAgent_InPublishMode_DoesNotValidateRegion() { @@ -189,7 +243,15 @@ public void AsHostedAgent_WithOptions_AppliesAllPropertiesToConfiguration() Cpu = 1m, Memory = 2m, Metadata = { ["scenario"] = "unit-test" }, - EnvironmentVariables = { ["MY_VAR"] = "my-value" } + EnvironmentVariables = { ["MY_VAR"] = "my-value" }, + Protocols = + { + new HostedAgentProtocolVersion + { + Protocol = "invocations", + Version = "1.0.0" + } + } }; builder.AddPythonApp("agent", "./app.py", "main:app") @@ -207,6 +269,75 @@ public void AsHostedAgent_WithOptions_AppliesAllPropertiesToConfiguration() Assert.Equal(2m, configuration.Memory); Assert.Equal("unit-test", configuration.Metadata["scenario"]); Assert.Equal("my-value", configuration.EnvironmentVariables["MY_VAR"]); + var protocol = Assert.Single(configuration.ContainerProtocolVersions); + Assert.Equal(ProjectsAgentProtocol.Invocations, protocol.Protocol); + Assert.Equal("1.0.0", protocol.Version); + } + + [Fact] + public async Task GetResolvedEnvironmentVariables_DoesNotForwardFoundryReservedTargetEnvironmentVariables() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var agent = builder.AddExecutable("agent", "python", ".") + .WithEnvironment("PORT", "8000") + .WithEnvironment("AGENT_NAME", "agent") + .WithEnvironment("FOUNDRY_MODE", "hosted") + .WithEnvironment("MY_VAR", "my-value"); + + using var app = builder.Build(); + var hostedAgent = new AzureHostedAgentResource("agent-ha", agent.Resource); + + var envVars = await AzureHostedAgentResource.GetResolvedEnvironmentVariablesAsync( + app.Services.GetRequiredService(), + hostedAgent, + agent.Resource, + NullLogger.Instance, + CancellationToken.None); + + Assert.DoesNotContain("PORT", envVars.Keys); + Assert.DoesNotContain("AGENT_NAME", envVars.Keys); + Assert.DoesNotContain("FOUNDRY_MODE", envVars.Keys); + Assert.Equal("my-value", envVars["MY_VAR"]); + } + + [Theory] + [InlineData("", "1.0.0", nameof(HostedAgentProtocolVersion.Protocol))] + [InlineData("invocations", "", nameof(HostedAgentProtocolVersion.Version))] + public void AsHostedAgent_WithInvalidProtocolOptions_ThrowsWithPropertyName(string protocol, string version, string expectedParamName) + { + var options = new HostedAgentOptions + { + Protocols = + { + new HostedAgentProtocolVersion + { + Protocol = protocol, + Version = version + } + } + }; + + var ex = Assert.Throws(() => options.ApplyTo(new HostedAgentConfiguration("test-image"))); + Assert.Equal(expectedParamName, ex.ParamName); + } + + [Fact] + public void GetAgentEndpointProtocols_MapsContainerProtocolsToEndpointProtocols() + { + var endpointProtocols = AzureHostedAgentResource.GetAgentEndpointProtocols( + [ + new ProtocolVersionRecord(ProjectsAgentProtocol.Invocations, "1.0.0"), + new ProtocolVersionRecord(ProjectsAgentProtocol.Responses, "1.0.0"), + new ProtocolVersionRecord(ProjectsAgentProtocol.ActivityProtocol, "1.0.0"), + new ProtocolVersionRecord(ProjectsAgentProtocol.Invocations, "1.1.0") + ]); + + Assert.Collection( + endpointProtocols, + protocol => Assert.Equal(AgentEndpointProtocol.Invocations, protocol), + protocol => Assert.Equal(AgentEndpointProtocol.Responses, protocol), + protocol => Assert.Equal(AgentEndpointProtocol.Activity, protocol)); } [Fact] @@ -317,6 +448,88 @@ public async Task FoundryProject_DefaultRegistryDoesNotAddGlobalRegistryTargets( [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")] private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken); + [Fact] + public void AsHostedAgent_StampsReferenceRoleAssignmentAnnotationOnTarget_WithAzureAIUserRole() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var project = builder.AddFoundry("account") + .AddProject("my-project"); + + builder.AddPythonApp("agent", "./app.py", "main:app") + .AsHostedAgent(project); + + var hostedAgent = Assert.Single(builder.Resources.OfType()); + var account = Assert.Single(builder.Resources.OfType()); + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. + var annotation = Assert.Single(hostedAgent.Target.Annotations.OfType()); + Assert.Same(account, annotation.Target); + Assert.Contains(annotation.Roles, role => + string.Equals(role.Id, AzureHostedAgentResource.AzureAIUserRoleDefinitionId, StringComparison.OrdinalIgnoreCase)); +#pragma warning restore ASPIREAZURE003 + } + + [Fact] + public void AsHostedAgent_ReferenceRoleAssignmentAnnotation_GrantsOnlyAzureAIUserRole() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var project = builder.AddFoundry("account") + .AddProject("my-project"); + + builder.AddPythonApp("agent", "./app.py", "main:app") + .AsHostedAgent(project); + + var hostedAgent = Assert.Single(builder.Resources.OfType()); + var account = Assert.Single(builder.Resources.OfType()); + Assert.True(account.TryGetLastAnnotation(out var defaults)); + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. + var annotation = Assert.Single(hostedAgent.Target.Annotations.OfType()); + + // The implied grant is least-privilege: only "Azure AI User" is required to invoke the agent. + var role = Assert.Single(annotation.Roles); + Assert.Equal(AzureHostedAgentResource.AzureAIUserRoleDefinitionId, role.Id, ignoreCase: true); + + // The account's default data-plane roles must NOT be folded in here. A consumer that references + // the account directly still receives them via the preparer's normal walk, and a consumer that + // explicitly suppresses them must keep them suppressed. + foreach (var defaultRole in defaults.Roles) + { + Assert.DoesNotContain(annotation.Roles, r => string.Equals(r.Id, defaultRole.Id, StringComparison.OrdinalIgnoreCase)); + } +#pragma warning restore ASPIREAZURE003 + } + + [Fact] + public void AsHostedAgent_MultipleHostedAgents_EachTargetCarriesItsOwnReferenceRoleAssignment() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + var project = builder.AddFoundry("account") + .AddProject("my-project"); + var otherProject = builder.AddFoundry("account2") + .AddProject("other-project"); + + builder.AddPythonApp("agent", "./app.py", "main:app") + .AsHostedAgent(project); + builder.AddPythonApp("agent2", "./app.py", "main:app") + .AsHostedAgent(otherProject); + + var hostedAgents = builder.Resources.OfType().ToList(); + Assert.Equal(2, hostedAgents.Count); + + var account = Assert.Single(builder.Resources.OfType(), r => r.Name == "account"); + var account2 = Assert.Single(builder.Resources.OfType(), r => r.Name == "account2"); + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. + var targets = hostedAgents + .Select(a => Assert.Single(a.Target.Annotations.OfType()).Target) + .ToList(); + + Assert.Contains(account, targets); + Assert.Contains(account2, targets); +#pragma warning restore ASPIREAZURE003 + } + private sealed class Project : IProjectMetadata { public string ProjectPath => "project"; diff --git a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs index 7d5c7449d1a..4bf0292ed27 100644 --- a/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs +++ b/tests/Aspire.Hosting.JavaScript.Tests/AddNodeAppTests.cs @@ -564,7 +564,7 @@ public async Task WithReferenceDispatchesNodeAppServiceReference() .WithEndpoint("http", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5031); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "nodeapp.dev.internal", 5031, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "nodeapp.dev.internal", 5031, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var consumer = builder.AddContainer("consumer", "fake"); diff --git a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs index 6eb39f1c5c0..723a0b01480 100644 --- a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs +++ b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs @@ -99,7 +99,7 @@ public async Task MilvusClientAppWithReferenceContainsConnectionStrings() .WithEndpoint("grpc", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs index fd64a45cb51..dcae09ba2b7 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs @@ -195,7 +195,7 @@ public async Task WithReferenceDispatchesPostgresDatabaseReference() .WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 2000, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 2000, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var database = postgres.AddDatabase("db"); var consumer = appBuilder.AddContainer("consumer", "fake"); diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs index 987bc894af0..c68d64b347d 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs @@ -78,7 +78,7 @@ public async Task WithPostgresMcpOnDatabaseSetsDatabaseUriEnvironmentVariable() .WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .AddDatabase("db") .WithPostgresMcp(); diff --git a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs index f7d9bec19c2..0dd939873e8 100644 --- a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs +++ b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs @@ -179,12 +179,12 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings() .WithEndpoint("grpc", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithEndpoint("http", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) @@ -223,12 +223,12 @@ public async Task WithReferenceDispatchesQdrantReference() .WithEndpoint("grpc", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithEndpoint("http", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var consumer = appBuilder.AddContainer("consumer", "fake"); diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index 6cfea90b8c9..22524207136 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -278,17 +278,17 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables() redis1.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); redis2.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); redis3.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5003); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var redisInsight = Assert.Single(builder.Resources.OfType()); @@ -706,7 +706,7 @@ public async Task RedisInsightEnvironmentCallbackIsIdempotent() .WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379); - e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithRedisInsight(); diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index e8e803885f9..fbf3f7ff342 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -48,6 +48,7 @@ + diff --git a/tests/Aspire.Hosting.Tests/ComputeEnvironmentEndpointResolverTests.cs b/tests/Aspire.Hosting.Tests/ComputeEnvironmentEndpointResolverTests.cs new file mode 100644 index 00000000000..830a02ff526 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ComputeEnvironmentEndpointResolverTests.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; + +namespace Aspire.Hosting.Tests; + +public class ComputeEnvironmentEndpointResolverTests +{ + [Fact] + public async Task OwningResourceInDifferentEnvironment_DelegatesToOwningEnvironment() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + var owningEnv = builder.AddResource(new TestComputeEnvironmentResource("owning")); + var agent = builder.AddResource(new TestComputeResource("agent")); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(owningEnv.Resource) { ComputeEnvironment = owningEnv.Resource }); + + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); + + Assert.True(resolved); + Assert.NotNull(expression); + // The owning environment (TestComputeEnvironmentResource) maps the host to "{name}.example.com". + Assert.Equal("http://agent.example.com:8080", await expression.GetValueAsync(default).DefaultTimeout()); + } + + [Fact] + public async Task EndpointReferenceOverload_DelegatesToOwningEnvironment() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + var owningEnv = builder.AddResource(new TestComputeEnvironmentResource("owning")); + var agent = builder.AddResource(new TestComputeResource("agent")); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(owningEnv.Resource) { ComputeEnvironment = owningEnv.Resource }); + + // The EndpointReference overload uses EndpointProperty.Url internally. + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint, [currentEnv.Resource], out var expression); + + Assert.True(resolved); + Assert.NotNull(expression); + Assert.Equal("http://agent.example.com:8080", await expression.GetValueAsync(default).DefaultTimeout()); + } + + [Fact] + public void OwningResourceDeploysToCurrentEnvironment_ReturnsFalse() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + var agent = builder.AddResource(new TestComputeResource("agent")); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); + + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); + + Assert.False(resolved); + Assert.Null(expression); + } + + [Fact] + public void OwningResourceBoundToCurrentEnvironmentWithoutDeploymentTarget_ReturnsFalseViaBackstop() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + // Bind via WithComputeEnvironment only (no DeploymentTargetAnnotation). The effective + // environment resolves to the current environment and the ReferenceEquals backstop catches it. + var agent = builder.AddResource(new TestComputeResource("agent")) + .WithComputeEnvironment(currentEnv); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); + + Assert.False(resolved); + Assert.Null(expression); + } + + [Fact] + public void OwningResourceHasNoComputeEnvironment_ReturnsFalse() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + // No binding and no deployment target: nothing to delegate to. + var agent = builder.AddResource(new TestComputeResource("agent")); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); + + Assert.False(resolved); + Assert.Null(expression); + } + + [Fact] + public void OwningResourceBoundToCurrentEnvironmentWithMultipleDeploymentTargets_ReturnsFalseAndDoesNotThrow() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + var otherEnv = builder.AddResource(new TestComputeEnvironmentResource("other")); + // Explicit binding to the current environment plus deployment targets for both environments. + // The binding lets GetComputeEnvironment() short-circuit to the current environment without + // hitting the parameterless deployment-target resolver that throws on multi-target ambiguity. + var agent = builder.AddResource(new TestComputeResource("agent")) + .WithComputeEnvironment(currentEnv); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(otherEnv.Resource) { ComputeEnvironment = otherEnv.Resource }); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + + var resolved = ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out var expression); + + Assert.False(resolved); + Assert.Null(expression); + } + + [Fact] + public void OwningResourceUnboundWithMultipleDeploymentTargets_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var currentEnv = builder.AddResource(new TestComputeEnvironmentResource("current")); + var otherEnv = builder.AddResource(new TestComputeEnvironmentResource("other")); + // No binding: an unbound resource with more than one deployment target is ambiguous. + // TryGetEffectiveComputeEnvironment falls back to the parameterless GetDeploymentTargetAnnotation() + // which throws. This documents that the resolver does not swallow that ambiguity; the pipeline + // rejects this configuration earlier in practice. + var agent = builder.AddResource(new TestComputeResource("agent")); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(currentEnv.Resource) { ComputeEnvironment = currentEnv.Resource }); + agent.Resource.Annotations.Add(new DeploymentTargetAnnotation(otherEnv.Resource) { ComputeEnvironment = otherEnv.Resource }); + var endpoint = AddHttpEndpoint(agent.Resource, port: 8080, targetPort: 5000); + + Assert.Throws(() => + ComputeEnvironmentEndpointResolver.TryGetCrossEnvironmentEndpointExpression( + endpoint.Property(EndpointProperty.Url), [currentEnv.Resource], out _)); + } + + private static EndpointReference AddHttpEndpoint(TestComputeResource resource, int? port, int? targetPort) + { + var endpoint = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http", port: port, targetPort: targetPort); + resource.Annotations.Add(endpoint); + + return new EndpointReference(resource, endpoint); + } + + private sealed class TestComputeEnvironmentResource(string name) : Resource(name), IComputeEnvironmentResource + { +#pragma warning disable ASPIRECOMPUTE002 + public ReferenceExpression GetHostAddressExpression(EndpointReference endpointReference) => + ReferenceExpression.Create($"{endpointReference.Resource.Name}.example.com"); +#pragma warning restore ASPIRECOMPUTE002 + } + + private sealed class TestComputeResource(string name) : Resource(name), IComputeResource, IResourceWithEndpoints + { + } +} diff --git a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs index 2419809d65d..20ebd634d62 100644 --- a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs @@ -297,7 +297,7 @@ public void AllocatedEndpoint_ThrowsWhenNetworkIdDoesNotMatch() annotation, "localhost", 8080, EndpointBindingMode.SingleAddress, targetPortExpression: null, - networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork); var ex = Assert.Throws(() => annotation.AllocatedEndpoint = mismatchedEndpoint); } diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index 086b5f427f1..068b4facd6d 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -230,7 +230,7 @@ public async Task EnvironmentVariableExpressions() { ep.AllocatedEndpoint = new AllocatedEndpoint(ep, "localhost", 17454); - ep.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(ep, "container1.dev.internal", 10005, EndpointBindingMode.SingleAddress, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + ep.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(ep, "container1.dev.internal", 10005, EndpointBindingMode.SingleAddress, networkId: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var endpoint = container.GetEndpoint("primary"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Foundry/TypeScript/apphost.mts b/tests/PolyglotAppHosts/Aspire.Hosting.Foundry/TypeScript/apphost.mts index 2479a1aeba8..936eb550345 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Foundry/TypeScript/apphost.mts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Foundry/TypeScript/apphost.mts @@ -103,6 +103,11 @@ const server = http.createServer((req, res) => { res.end(JSON.stringify({ output: 'hello from validation app host' })); return; } + if (req.url === '/invocations') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ response: 'hello from validation app host' })); + return; + } res.writeHead(404); res.end(); }); @@ -115,7 +120,8 @@ await hostedAgent.asHostedAgent(project, { cpu: 1, memory: 2, metadata: { scenario: 'validation' }, - environmentVariables: { VALIDATION_MODE: 'true' } + environmentVariables: { VALIDATION_MODE: 'true' }, + protocols: [{ protocol: 'invocations', version: '1.0.0' }] }); const api = await builder.addContainer('api', 'nginx');